Skip to content

Conversation

@myovchev
Copy link
Contributor

@myovchev myovchev commented Nov 4, 2025

Summary

Summary

Introduce a configurable option stripUrlAccents in the @apostrophecms/i18n module to control whether Latin accents are stripped from URLs (slugs) and attachment names.

  • Default remains unchanged: accents are preserved in slugs (backward compatible).
  • When @apostrophecms/i18n.options.stripUrlAccents is true:
    • New/updated document slugs (pages, pieces, slug fields) have accents removed.
    • Attachment name values are normalized without accents on upload.
    • The admin UI slug input reflects accent-stripping in real time.
  • A new task @apostrophecms/i18n:strip-slug-accents is provided to update existing document slugs and attachment names when adopting this option.

What are the specific steps to test this change?

  1. Default behavior (accents preserved)
  • Ensure stripUrlAccents is absent or set to false in @apostrophecms/i18n options.
  • Create a page and a piece titled: C'est déjà l'été.
  • Expected slugs:
    • Piece: c-est-déjà-l-été
    • Page: /c-est-déjà-l-été
  • Upload an attachment named: été-image.png
    • Expected stored name in database (aposAttachments collection): été-image
  1. Enable accent stripping for new/updated content
  • Set @apostrophecms/i18n.options.stripUrlAccents = true.
  • Create a new page and piece:
    • Title: C'est déjà l'été
    • Expected slugs:
      • Piece: c-est-deja-l-ete
      • Page: /c-est-deja-l-ete
  • Upload an attachment named: été-image.png
    • Expected stored name in database (aposAttachments collection): ete-image
  • In the admin UI, type an accented title and observe the slug field: it should show accent-free characters.
  1. Migrate existing content (optional when adopting the option)
  • With stripUrlAccents: true, run the task:
    • node app @apostrophecms/i18n:strip-slug-accents
  • Expected:
    • Existing slugs like /c-est-déjà-l-été become /c-est-deja-l-ete.
    • Attachment names like été-image become ete-image.
    • Task logs counts of updated documents and attachments.

Notes

  • The option only affects new/updated content unless the task is run.
  • Non-Latin scripts (e.g., Cyrillic) continue to be handled by sluggo; the deburring only affects Latin accent marks.

What kind of change does this PR introduce?

(Check at least one)

  • Bug fix
  • New feature
  • Refactor
  • Documentation
  • Build-related changes
  • Other

Make sure the PR fulfills these requirements:

  • It includes a) the existing issue ID being resolved, b) a convincing reason for adding this feature, or c) a clear description of the bug it resolves
  • The changelog is updated
  • Related documentation has been updated
  • Related tests have been updated

If adding a new feature without an already open issue, it's best to open a feature request issue first and wait for approval before working on it.

Other information:

@linear
Copy link

linear bot commented Nov 4, 2025

@myovchev myovchev requested a review from haroun November 5, 2025 08:40
await self.apos.migration.eachDoc({}, 5, async doc => {
const slug = doc.slug;
const req = self.apos.task.getAdminReq({
locale: doc.aposLocale?.split(':')[0] || 'en'
Copy link
Contributor

Choose a reason for hiding this comment

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

Forcing the locale to en seems wrong. We have i18n defaultLocale for that.
If you're using doc.aposLocale you can also pass it as query: { aposLocale: doc.aposLocale } and let the i18n middleware do its magic.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

aposLocale can be undefined. I'll switch to default locale though.

// If the page does not yet have a slug, add one based on the
// title; throw an error if there is no title
ensureSlug(page) {
ensureSlug(page, { stripAccents } = {}) {
Copy link
Contributor

Choose a reason for hiding this comment

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

I don't think this scales well. We should work towards ensureSlug and slugify keeping the same signature somehow and fetching the stripAccents from the i18n locales option in my opinion. If we don't do that, it means that we have to change multiple external/pro modules to support the new feature, and apostrophe users will have to do the same. There will always be a piece of code somewhere where stripAccents is not passed and it will lead to inconsistent behavior that will be hard to track.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

ensureSlug is internal method. It's called internally via event.
Utils should not fetch/know what i18n options are, so slugify is what it is - an util with an option.
Having an option for ensureSlug is a clear contract. This is not a public API so no BC is required.

I reverted option per locale because of many and bad showstoppers. I explained above that the there was an agreement to go with the simple "rule them all" option.

slugify(s, options) {
return require('sluggo')(s, options);
const { stripAccents, ...opts } = options || {};
const slug = require('sluggo')(s, opts);
Copy link
Contributor

Choose a reason for hiding this comment

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

  1. We should put that require statement at the top.
  2. Is the order of operations important here? slugify then strip accents vs strip accents then slugify.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Moving the require is out of context for this PR. (Many other utils does it).
The current order of operation makes sense. You don't want to change the behavior of sluggo with pre-filtering the input, but to filter the output of the sluggo.

// set options.stripAccents to true.
slugify(s, options) {
return require('sluggo')(s, options);
const { stripAccents, ...opts } = options || {};
Copy link
Contributor

Choose a reason for hiding this comment

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

This new option is a challenge because we need to update all the usages of the slugify method, in our modules but our clients needs to do the same. There is a default value, but this is some kind of a breaking change, mainly because it requires changes to the code for the behavior to remain consistent.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

The accent cleaning behavior can't break BC because it's a new feature. This code is fully BC.

return this.field.page && apos.i18n.locales[apos.i18n.locale].prefix;
},
stripAccents() {
return apos.i18n.stripUrlAccents === true;
Copy link
Contributor

Choose a reason for hiding this comment

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

Having this here will make it difficult to have this feature enabled/disabled per locales.

Copy link
Contributor Author

@myovchev myovchev Nov 6, 2025

Choose a reason for hiding this comment

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

This is one of the main showstoppers for per locale feature, which we don't implement with the current PR.

@myovchev myovchev requested a review from haroun November 6, 2025 14:37
@myovchev
Copy link
Contributor Author

myovchev commented Nov 6, 2025

@haroun Added a patch to address the problems

  • in the task, only accents are removed/compared, no double slugify is attempted; it should solve the problems
  • in the task, switched to the default language
  • in the task, we use manager to update the doc now, so that the URL redirects are recorded.

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.

3 participants