From 6b9a4e504e60211d2e30017c1f755fefa1c7dd24 Mon Sep 17 00:00:00 2001 From: dittnamn Date: Thu, 30 Jul 2026 20:51:47 +0200 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=8E=A8=20Added=20an=20option=20to=20u?= =?UTF-8?q?se=20unicode=20slugs=20in=20URL:s?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit fixes https://github.com/TryGhost/Ghost/issues/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 https://github.com/TryGhost/SDK/pull/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 https://github.com/TryGhost/SDK/pull/1011 and https://github.com/TryGhost/framework/pull/860 are required. --- .../settings/advanced/labs/beta-features.tsx | 42 ++++++++++++++++++- apps/admin-x-settings/src/typings.d.ts | 2 +- apps/admin/src/vite-env.d.ts | 2 +- ghost/admin/app/components/tags/tag-form.js | 2 +- ghost/admin/app/controllers/lexical-editor.js | 2 +- ghost/admin/app/models/setting.js | 5 +++ ghost/admin/app/services/feature.js | 1 + ghost/admin/app/services/slug-generator.js | 11 ++++- .../core/core/frontend/helpers/navigation.js | 4 +- .../frontend/services/data/entry-lookup.js | 5 +++ .../core/frontend/services/data/fetch-data.js | 5 +++ .../services/routing/controllers/channel.js | 3 +- .../routing/controllers/collection.js | 3 +- .../services/routing/controllers/rss.js | 3 +- ghost/core/core/server/api/endpoints/slugs.js | 12 +++++- .../utils/serializers/input/settings.js | 1 + .../input/utils/settings-key-group-mapper.js | 1 + .../input/utils/settings-key-type-mapper.js | 1 + .../server/data/exporter/export-filename.js | 4 +- .../default-settings/default-settings.json | 9 ++++ .../models/base/plugins/generate-slug.js | 6 ++- ghost/core/core/server/models/post.js | 14 ++++--- ghost/core/core/server/models/tag.js | 10 ++++- ghost/core/core/server/models/user.js | 8 +++- ghost/core/core/shared/labs.js | 1 + .../services/data/entry-lookup.test.js | 27 ++++++++++++ 26 files changed, 156 insertions(+), 28 deletions(-) diff --git a/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx b/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx index 991660808a8..74751728bdd 100644 --- a/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx +++ b/apps/admin-x-settings/src/components/settings/advanced/labs/beta-features.tsx @@ -1,24 +1,44 @@ import FeatureToggle from './feature-toggle'; import LabItem from './lab-item'; import React, {useState} from 'react'; -import {Button, FileUpload, List, showToast} from '@tryghost/admin-x-design-system'; +import {Button, FileUpload, List, Select, showToast} from '@tryghost/admin-x-design-system'; import {downloadRedirects, useUploadRedirects} from '@tryghost/admin-x-framework/api/redirects'; import {downloadRoutes, useUploadRoutes} from '@tryghost/admin-x-framework/api/routes'; -import {getSettingValue} from '@tryghost/admin-x-framework/api/settings'; +import {getSettingValue, useEditSettings} from '@tryghost/admin-x-framework/api/settings'; import {useGlobalData} from '../../../providers/global-data-provider'; import {useHandleError} from '@tryghost/admin-x-framework/hooks'; const IS_AUTOMATIONS_BETA_ACTIVE = true; +const SLUG_SEPARATORS = [ + { + value: '-', + label: 'Dashes [-]', + hint: 'The default mode (e.g. /example-ghost-post/).' + }, + { + value: '_', + label: 'Underscores [_]', + hint: 'A mode with better readability and clearer distinction (e.g. /example_ghost_post/).' + }, + { + value: ' ', + label: 'Spaces [ ]', + hint: 'A natural mode, but might look foreign in URL:s (eg. /example ghost post/). Also, many browsers will show spaces as %20 in the URL:s.' + } +]; + const BetaFeatures: React.FC = () => { const {settings} = useGlobalData(); const {mutateAsync: uploadRedirects} = useUploadRedirects(); const {mutateAsync: uploadRoutes} = useUploadRoutes(); + const {mutateAsync: editSettings} = useEditSettings(); const handleError = useHandleError(); const [redirectsUploading, setRedirectsUploading] = useState(false); const [routesUploading, setRoutesUploading] = useState(false); const labs = JSON.parse(getSettingValue(settings, 'labs') || '{}'); const isAutomationsEnabled = !!labs.automations; + const slugSeparator = getSettingValue(settings, 'slug_separator') || '-'; return ( @@ -45,6 +65,24 @@ const BetaFeatures: React.FC = () => { action={} detail={<>Adds the excerpt input below the post title in the editor} title='Show post excerpt inline' /> + + + option.value === slugSeparator)} - onSelect={async (option) => { - await editSettings([{ - key: 'slug_separator', - value: option?.value || '-' - }]); - }} - /> + value={slugSeparator ?? '-'} + onValueChange={async (value) => { + await editSettings([{ + key: 'slug_separator', value + }]); + }}> + + {getSlugSeparatorOptionLabel(slugSeparator)} + + {renderSlugSeparatorOptions()} + } detail={<>Use Unicode letters and numbers in URL slugs instead of transliterating them (e.g /smörgåsbord/ instead of /smorgasbord/), which may add benefits for SEO. You can also select another slug separator to adjust the look of the URL:s.} title='International slugs' />