Skip to content

🎨 Changed and improved the slug generation - #1011

Open
dittnamn wants to merge 6 commits into
TryGhost:mainfrom
dittnamn:slug-upgrade
Open

🎨 Changed and improved the slug generation#1011
dittnamn wants to merge 6 commits into
TryGhost:mainfrom
dittnamn:slug-upgrade

Conversation

@dittnamn

Copy link
Copy Markdown

ref towards TryGhost/Ghost#3224 etc.
The old slug generation using unidecode had a lot of issues with erroneous transliteration for many languages, which has been the reason for many discussions and requests for change over the years. By replacing unidecode with anyascii, a lot of these issues should be solved, however not perfectly.

Over the years, the initial reasons for not allowing anything other than ascii letters and numbers in the slugs have been fixed. The Ghost databases, routes, links, loading, etc. now support unicode characters in URL:s. Browser support is fully working and many large sites, including Wikipedia, use unicode characters in URL:s.

With just a few modifications in the Ghost sources, an option for full unicode slug support could therefore be added for users who want it. To work towards this, an extra option has been added to the slugify function, to allow the disabling of the transliteration part of the slug generation.

Another option, allowing the change of slug part separator was also added. Due to how the filtering work, the possible options are currently just spaces, dashes and underscores, but more options could possibly be added in the future. Dots as separators could in theory make good looking slugs, but should be avoided due to the risk of filename mixups.

Due to the slightly different transliteration method, some of the tests have been revised and some new ones were added as well. Note that anyascii has some quirks compared to unidecode, but in total this is an improvement for most languages.

  • There's a clear use-case for this code change
  • Commit message has a short title & references relevant issues
  • The build will pass (run yarn test and yarn lint)

ref towards TryGhost/Ghost#3224 etc.
The old slug generation using unidecode had a lot of issues with
erroneous transliteration for many languages, which has been the reason
for many discussions and requests for change over the years. By
replacing unidecode with anyascii, a lot of these issues should be
solved, however not perfectly.

Over the years, the initial reasons for not allowing anything other than
ascii letters and numbers in the slugs have been fixed. The Ghost
databases, routes, links, loading, etc. now support unicode characters
in URL:s. Browser support is fully working and many large sites,
including Wikipedia, use unicode characters in URL:s.

With just a few modifications in the Ghost sources, an option for full
unicode slug support could therefore be added for users who want it. To
work towards this, an extra option has been added to the slugify
function, to allow the disabling of the transliteration part of the slug
generation.

Another option, allowing the change of slug part separator was also
added. Due to how the filtering work, the possible options are currently
just spaces, dashes and underscores, but more options could possibly be
added in the future. Dots as separators could in theory make good
looking slugs, but should be avoided due to the risk of filename mixups.

Due to the slightly different transliteration method, some of the tests
have been revised and some new ones were added as well. Note that
anyascii has some quirks compared to unidecode, but in total this is an
improvement for most languages.
@coderabbitai

coderabbitai Bot commented Jul 19, 2026

Copy link
Copy Markdown

Review Change Stack

Note

Reviews paused

It looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the reviews.auto_review.auto_pause_after_reviewed_commits setting.

Use the following commands to manage reviews:

  • @coderabbitai resume to resume automatic reviews.
  • @coderabbitai review to trigger a single review.

Use the checkboxes below for quick actions:

  • ▶️ Resume reviews
  • 🔍 Trigger review

Walkthrough

Slugification now uses any-ascii instead of unidecode, supports configurable separators, and optionally skips transliteration. Character filtering, Unicode normalization, reserved-character replacement, camelCase splitting, separator cleanup, and Zalgo mitigation were revised. Dependencies and tests were updated accordingly.

Estimated code review effort: 3 (Moderate) | ~20 minutes

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title is broad, but it clearly describes the main slug-generation change in the pull request.
Description check ✅ Passed The description directly discusses the slugify transliteration and separator changes made in this pull request.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
packages/string/test/slugify.test.js (1)

94-96: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low value

Prevent potential test pollution by scoping options locally.

Assigning to options without declaring it locally modifies a shared variable from the outer suite scope. This can pollute subsequent tests that rely on the default options, causing them to run with unintended configurations.

  • packages/string/test/slugify.test.js#L94-L96: Use const options = {noTransliteration: true}; and const result = ...
  • packages/string/test/slugify.test.js#L101-L103: Use const options = {separator: ' '}; and const result = ...
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/string/test/slugify.test.js` around lines 94 - 96, Scope the test
variables locally in both affected cases: packages/string/test/slugify.test.js
lines 94-96 and 101-103. In each test, declare options and result with const
inside the test block, replacing assignments to the shared outer-scope variables
while preserving the existing option values and slugify calls.
packages/string/lib/slugify.js (1)

17-18: 🔒 Security & Privacy | 🔵 Trivial | 💤 Low value

Enforce allowed values for options.separator.

If options.separator can be controlled by external input, passing special replacement patterns (like $&) could bypass character sanitization, as replace would substitute the matched invalid character with itself. Additionally, passing an empty string "" would unexpectedly fall back to "-" due to the || operator.

Consider validating the separator against the documented allowed values.

🛡️ Proposed fix
-    // If the separator isn't set, default to `-`
-    const separator = options.separator || '-';
+    // Ensure the separator is one of the allowed values, default to `-`
+    const separator = [' ', '_', '-'].includes(options.separator) ? options.separator : '-';
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/string/lib/slugify.js` around lines 17 - 18, Update the separator
initialization in slugify to validate options.separator against the documented
allowed separator values before using it as a replacement string. Preserve an
explicitly supported empty-string value instead of treating it as unset, and
fall back to "-" only when the value is absent or invalid.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/string/lib/slugify.js`:
- Line 1: Update the any-ascii dependency usage in packages/string so slugify.js
loads successfully under CommonJS: either pin any-ascii to a CommonJS-compatible
version or migrate the package to ESM, ensuring the existing slugify behavior
remains unchanged.

---

Nitpick comments:
In `@packages/string/lib/slugify.js`:
- Around line 17-18: Update the separator initialization in slugify to validate
options.separator against the documented allowed separator values before using
it as a replacement string. Preserve an explicitly supported empty-string value
instead of treating it as unset, and fall back to "-" only when the value is
absent or invalid.

In `@packages/string/test/slugify.test.js`:
- Around line 94-96: Scope the test variables locally in both affected cases:
packages/string/test/slugify.test.js lines 94-96 and 101-103. In each test,
declare options and result with const inside the test block, replacing
assignments to the shared outer-scope variables while preserving the existing
option values and slugify calls.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: fd58d1cd-a449-42d6-8881-230376909d5d

📥 Commits

Reviewing files that changed from the base of the PR and between dbb79b4 and 8cf91ee.

📒 Files selected for processing (3)
  • packages/string/lib/slugify.js
  • packages/string/package.json
  • packages/string/test/slugify.test.js

Comment thread packages/string/lib/slugify.js
The slugify options were renamed to describe their usage better. Also, a
NFC normalization has been added to make sure combining marks aren't
lost in the conversion and to make sure seemingly identical slugs won't
be generated when the unicode slugs are enabled.

Extra types of common apostrophes are also now just removed instead of
turned into separators, to make sure "what‘s" is turned into "whats"
instead of "what-s".

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/string/lib/slugify.js`:
- Around line 12-18: The slugify function’s slugSeparator currently accepts
undocumented values. In the exported slugify function, validate the resolved
separator against the documented choices space, underscore, and hyphen before
replacement processing; throw `@tryghost/errors`’ IncorrectUsageError for
unsupported values, adding the package import if needed.
- Around line 23-31: The Unicode filter in slugify must preserve combining
marks. In packages/string/lib/slugify.js lines 23-31, update the character class
used by slugify to allow Unicode marks alongside letters and numbers; in
packages/string/test/slugify.test.js lines 93-98, add a unicodeSlugs regression
case containing a non-composing combining mark and assert it remains intact in
the generated slug.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: a689935d-c40b-4150-910d-e0777d973763

📥 Commits

Reviewing files that changed from the base of the PR and between 8cf91ee and 05f11a8.

📒 Files selected for processing (2)
  • packages/string/lib/slugify.js
  • packages/string/test/slugify.test.js

Comment thread packages/string/lib/slugify.js Outdated
Comment thread packages/string/lib/slugify.js Outdated
dittnamn added a commit to dittnamn/Ghost-framework that referenced this pull request Jul 30, 2026
ref towards TryGhost/Ghost#3224 etc.
In line with the changes in TryGhost/SDK#1011 a
change in the validator is necessary to allow unicode slugs to be used.
The validator now allows all Unicode letters and numbers, along with
spaces, underscores and dashes that can be used as slug separators.

Along with the changes in the slugify() function, equivalent changes
were made to the security.safe() function to allow it to use the same
parameters as slugify() and to make sure the tests will still work with
the new transliteration library.

However, the security.safe() function is barely used inside Ghost, and
could easily be replaced to use slugify() directly. A TODO notice has
been added, so that it could potentially be removed in the future.
dittnamn added a commit to dittnamn/Ghost that referenced this pull request Jul 30, 2026
fixes TryGhost#3224
The slugs generated by Ghost have had a lot of issues in how they've
been transliterated. Through a change in @tryghost/string a new
transliteration library, any-ascii, has been proposed to replace
unidecode, see TryGhost/SDK#1011

However, to get a fully internationalized site along with SEO optimized
URL:s, that change isn't fully enough. The current change adds an option
in Labs where it's possible to enable Unicode slugs instead of
performing the transliteration. The Unicode characters in use are
limited to those that are registered as letters and numbers, which means
that emojis, special characters, etc. still will be removed.

The only "major" change that has been done in Ghost to make this work is
actually just to normalize the URL slugs before looking them up in the
database. The rest of the code changes are mostly settings to turn the
new feature on or off, and to pass the setting along all the way to the
slugify() function. Due to the rest of the system already being in
Unicode, everything seems to work as it's supposed to.

Instead of using safe() from @tryghost/security, the slug generation has
also been changed to utilize slugify() from @tryghost/string directly
instead. The routing controllers for rss, collection and channel were
previously using safe() as well to slugify the lookups before going to
the router, but the entry controller didn't do this. As the router
already checks the slugs with isSlug() before passing them further, this
was unnecessary and had the potential to break lookups, so it was simply
removed.

In addition to using Unicode slugs, there's also an option added to
switch which separator the slugs use. Previously, dashes (-) were used
as the hardcoded default, but for URL readability, underscores (_) could
be preferred, like in the URL:s Wikipedia use. There's also an option
for using spaces ( ), but this might still be seen as a bit foreign in
URL:s. Out of the major browsers, it's currently just Firefox that show
these as spaces by default, with Safari doing it in some situations.
Other browsers can show the spaces as %20.

When activated, the Unicode slugs aren't added to member tags,
newsletter, services, integrations or benefits, as these aren't user
facing.

Note that for this change to work, the changes in
TryGhost/SDK#1011 and
TryGhost/framework#860 are required.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

♻️ Duplicate comments (1)
packages/string/lib/slugify.js (1)

30-31: 🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Preserve non-composing Unicode marks end to end.

NFC does not compose every combining mark, while the current filter removes all remaining marks. Update the filter and strengthen the regression test.

  • packages/string/lib/slugify.js#L30-L31: allow \p{M} alongside letters and numbers.
  • packages/string/test/slugify.test.js#L100-L105: test an input such as a\u0308\u0323 and assert the normalized mark remains.
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@packages/string/lib/slugify.js` around lines 30 - 31, Update the slugify
filter in packages/string/lib/slugify.js lines 30-31 to allow Unicode combining
marks via \p{M} alongside letters and numbers. Strengthen the regression test in
packages/string/test/slugify.test.js lines 100-105 using input such as
a\u0308\u0323 and assert the normalized mark is preserved end to end.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Duplicate comments:
In `@packages/string/lib/slugify.js`:
- Around line 30-31: Update the slugify filter in packages/string/lib/slugify.js
lines 30-31 to allow Unicode combining marks via \p{M} alongside letters and
numbers. Strengthen the regression test in packages/string/test/slugify.test.js
lines 100-105 using input such as a\u0308\u0323 and assert the normalized mark
is preserved end to end.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 58753286-c00f-4bb5-a56e-ae641a8c3262

📥 Commits

Reviewing files that changed from the base of the PR and between 05f11a8 and 5a7c544.

📒 Files selected for processing (2)
  • packages/string/lib/slugify.js
  • packages/string/test/slugify.test.js

This makes sure Thai script, etc. can be used as slugs, but removes
overuse of combining marks that creates Zalgo text and similar.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@packages/string/lib/slugify.js`:
- Around line 30-34: Update the combining-mark cleanup in slugify to also match
runs of four or more marks at the beginning of the slug by allowing a
start-of-string prefix, while preserving the existing behavior for marks
following non-mark characters. Add a regression test covering a leading
combining-mark run when unicodeSlugs is enabled.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 73ed724e-72e5-46ee-919c-3bb9cba972d0

📥 Commits

Reviewing files that changed from the base of the PR and between 5a7c544 and e9a450b.

📒 Files selected for processing (2)
  • packages/string/lib/slugify.js
  • packages/string/test/slugify.test.js

Comment thread packages/string/lib/slugify.js
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant