-
Notifications
You must be signed in to change notification settings - Fork 1.1k
feat(core): WordPress-style date tokens in url_pattern ({year}/{month}/{day}) #1526
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| --- | ||
| "emdash": minor | ||
| "@emdash-cms/admin": minor | ||
| --- | ||
|
|
||
| Adds WordPress-style date tokens to collection URL patterns. `url_pattern` now supports `{year}`, `{month}`, `{day}`, `{hour}`, `{minute}`, `{second}` (resolved from the entry's publish date, zero-padded) alongside `{slug}` and `{id}` — so you can reproduce permalinks like `/{year}/{month}/{day}/{slug}.html`. The tokens resolve in sitemap canonical URLs and the admin "View published" links. |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -37,9 +37,36 @@ export function resolveLocaleChain(explicit?: string): string[] { | |
| } | ||
|
|
||
| const REPEATED_SLASHES = /\/{2,}/g; | ||
| const DATE_TOKEN = /\{(year|month|day|hour|minute|second)\}/g; | ||
| const pad2 = (n: number) => String(n).padStart(2, "0"); | ||
|
|
||
| /** | ||
| * Interpolate a collection `url_pattern` with a row's slug and id. | ||
| * Substitute WordPress-style date tokens (`{year}`, `{month}`, `{day}`, | ||
| * `{hour}`, `{minute}`, `{second}`) from a publish date. Month/day/time parts | ||
| * are zero-padded, mirroring WordPress (`%monthnum%`, `%day%`, ...). Tokens are | ||
| * left untouched when no valid date is available, so callers without a date | ||
| * (or unpublished entries) never produce a half-resolved URL. | ||
| */ | ||
| function applyDateTokens(path: string, date: string | Date | null | undefined): string { | ||
| const d = date == null ? null : new Date(date); | ||
| if (!d || Number.isNaN(d.getTime())) return path; | ||
| const parts: Record<string, string> = { | ||
| year: String(d.getUTCFullYear()), | ||
| month: pad2(d.getUTCMonth() + 1), | ||
| day: pad2(d.getUTCDate()), | ||
| hour: pad2(d.getUTCHours()), | ||
| minute: pad2(d.getUTCMinutes()), | ||
| second: pad2(d.getUTCSeconds()), | ||
| }; | ||
| return path.replace(DATE_TOKEN, (match, key: string) => parts[key] ?? match); | ||
| } | ||
|
|
||
| /** | ||
| * Interpolate a collection `url_pattern` with a row's slug, id and publish date. | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. [suggestion] The PR description already flags this as a follow-up, but worth confirming here:
Contributor
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Done in 721f982 rather than deferring — the menus resolver now selects |
||
| * | ||
| * Supported tokens: `{slug}`, `{id}`, and the date tokens `{year}`, | ||
| * `{month}`, `{day}`, `{hour}`, `{minute}`, `{second}` (resolved from `date`, | ||
| * for WordPress-style permalinks like `/{year}/{month}/{day}/{slug}.html`). | ||
| * | ||
| * Falls back to `/{collection}/{slug}` when no pattern is configured. | ||
| * Does NOT apply any locale prefix — pass the result through | ||
|
|
@@ -51,12 +78,15 @@ export function interpolateUrlPattern(options: { | |
| collection: string; | ||
| slug: string; | ||
| id: string; | ||
| /** Publish date used for date tokens; tokens stay literal when absent. */ | ||
| date?: string | Date | null; | ||
| }): string { | ||
| const { pattern, collection, slug, id } = options; | ||
| const { pattern, collection, slug, id, date } = options; | ||
| const basePattern = pattern ?? `/${encodeURIComponent(collection)}/{slug}`; | ||
| let path = basePattern | ||
| .replace("{slug}", encodeURIComponent(slug)) | ||
| .replace("{id}", encodeURIComponent(id)); | ||
| .replaceAll("{slug}", encodeURIComponent(slug)) | ||
| .replaceAll("{id}", encodeURIComponent(id)); | ||
| path = applyDateTokens(path, date); | ||
| path = path.replace(REPEATED_SLASHES, "/"); | ||
| if (path.length > 1 && path.endsWith("/")) path = path.slice(0, -1); | ||
| if (!path.startsWith("/")) path = `/${path}`; | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
[needs fixing] This PR introduces date tokens to
url_pattern, butRedirectRepository.createAutoRedirect(packages/core/src/database/repositories/redirect.ts:352) still interpolates patterns with only string.replace("{slug}", oldSlug).replace("{id}", contentId).For a pattern like
/{year}/{month}/{day}/{slug}.html, a slug change creates an auto-redirect whose source and destination contain literal braces:A real request to
/2023/05/08/old-slug.htmlwill never match that source, so the old URL 404s instead of redirecting. This is a direct regression risk for the new date-token feature.Fix direction:
published_atintocreateAutoRedirect(select it increateSlugChangeRedirector flow it from the caller).oldUrl/newUrlthrough the sharedinterpolateUrlPattern(or the sameapplyDateTokenslogic) so date tokens resolve consistently with canonical URLs.createAutoRedirectwith a date-token pattern.There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed in 721f982 —
createSlugChangeRedirectnow selects the entry'spublished_atandcreateAutoRedirectbuilds both URLs through the sharedinterpolateUrlPattern(which also normalizes slashes/encoding). Two new repository tests: date tokens resolve from the publish date, and stay literal when there is none.