03 / Real package example / 7 min read

slugify: why is “!” still in my URL?

“Déjà Vu!” becomes “deja-vu!” — but you wanted “deja-vu”. Use REA to inspect the JavaScript package, find the setting that controls punctuation, and check the result.

On this page
slugify 1.6.6 / MITREA 6.4.0Checked Oct 12, 2026

THE SHORT ANSWER

lower: true only changes uppercase letters to lowercase. Add strict: true to remove punctuation too. This is an option you need to enable, not a bug in the library.

The working configuration
slugify("Déjà Vu!", { lower: true, strict: true })
// → "deja-vu"

01. The URL we wanted

A slug is the readable last part of a URL, such as deja-vu in example.com/posts/deja-vu. This example uses the real slugify 1.6.6 npm release. The results below come from running that release locally.

Try the title below. Then turn on Remove punctuation to see the difference.

Try the real package slugify 1.6.6
GENERATED URL
example.com/posts/deja-vu!

Still has a “!”? Turn on Remove punctuation.

JavaScript options{ lower: true, strict: false }

Runs in your browser. Nothing is uploaded. REA is used in the written walkthrough.

Here is the same comparison in JavaScript:

Actual package behavior
slugify("Déjà Vu!")
// → "Deja-Vu!"

slugify("Déjà Vu!", { lower: true })
// → "deja-vu!"

The accent disappeared; the exclamation mark stayed. We used REA to locate the package’s main file, read the filtering code, and check which option removes it. You can repeat those steps below.

02. Download the package and inspect it with REA

We downloaded the pinned npm tarball and unpacked it without installing or running the package. The following preparation commands work in a shell with tar:

Terminal
mkdir rea-slugify-case
cd rea-slugify-case
npm pack slugify@1.6.6
tar -xzf slugify-1.6.6.tgz

Then we ran REA’s JavaScript application analyzer on the extracted package directory:

If you prefer your coding agent, replace the path and paste this prompt:

Copy into your coding agent
Use REA to inspect /absolute/path/to/rea-slugify-case/package. I use slugify to make article URLs. Why does slugify('Déjà Vu!', { lower: true }) keep the exclamation mark? Find the entry file, show the code that allows it, and identify the option that removes it. Do not change the package.

Or collect the analysis directly in your terminal:

Terminal
npx -y rea-agents@6.4.0 analyze-javascript-application ./package --json > analysis.json

REA identified the package as slugify, version 1.6.6, with an entry point of ./slugify.js. The report recorded three relevant files and zero parse failures. It also marked overall graph coverage as partial, which we retained in the published summary.

This is a readable JavaScript distribution. REA supplied the artifact inventory and analysis context; subsequent source inspection and direct checks established the behavior. No native decompilation was involved.

03. Find the option that removes punctuation

In the shipped slugify.js, lines 33–42 normalize the string, apply locale or character-map replacements, and filter characters. The default filter explicitly allows the exclamation mark.

slugify.js · lines 41–47
// remove not allowed characters
.replace(options.remove || /[^\w\s$*_+~.()'"!\-:@]+/g, '')
}, '');

if (options.strict) {
  slug = slug.replace(/[^A-Za-z0-9\s]/g, '');
}

The strict branch runs afterward and removes characters outside letters, numbers and whitespace. Lowercasing is a separate operation at lines 57–58. That gives us a testable prediction:

Prediction
slugify("Déjà Vu!", { lower: true, strict: true })
// should return "deja-vu"

The same file also explains why spaces collapse into one separator, why & becomes and, and why the German locale changes Ä into AE.

04. Run the checks on the real package

We executed eight assertions against the unmodified 1.6.6 release, including the original question and a few surrounding behaviors.

8 / 8
Checks passed.

Original package · Node.js 22.23.1 · macOS arm64

InputOptionsObserved result
Déjà Vu!DefaultDeja-Vu!
Déjà Vu!lowerdeja-vu!
Déjà Vu!lower + strictdeja-vu
hello   world (padded)Defaulthello-world
Rock & RollDefaultRock-and-Roll
ÄpfelGerman locale + loweraepfel
Empty stringDefaultEmpty string
42 (a number)DefaultThrows “string argument expected”

Read the recorded check results, or use the interactive package demo above. The demo runs the same pinned package in your browser.

05. What we tested

For this release and these inputs, the source and observed outputs agree. If you want deja-vu, use both lower: true and strict: true.

The scope matters. Eight checks do not establish every Unicode or locale behavior. The REA graph is partial; a missing relationship in it is not proof of absence. We did not reconstruct a native binary or recover unavailable source code.

Because the shipped source is readable, ordinary source inspection is also a valid route. This case demonstrates the full habit: identify the artifact, inspect one rule, then test the claim against the original implementation.

06. Download and run this example

Download the small reproduction kit. It includes the pinned dependency, all eight assertions, and the steps to collect your own REA evidence.

Download the reproduction kit

Inside the extracted kit
npm install --ignore-scripts
node verify.mjs

The assertion runner prints each result and exits with an error if a check fails. Node.js executes the npm package only at this verification step; REA’s preceding static inspection does not execute it.

Try another tutorialFind where an app saves exported files.

Evidence: REA analysis summary · check results · inspected source · MIT license. Package by simov and contributors. Analysis and runtime checks performed locally on October 12, 2026. Public summaries omit local filesystem paths.

New to the tool? What is REA? · How we check our guides