Skip to content

fix(core): resolve collection url_pattern for content preview links#1745

Open
swissky wants to merge 2 commits into
emdash-cms:mainfrom
swissky:fix/preview-url-respects-url-pattern
Open

fix(core): resolve collection url_pattern for content preview links#1745
swissky wants to merge 2 commits into
emdash-cms:mainfrom
swissky:fix/preview-url-respects-url-pattern

Conversation

@swissky

@swissky swissky commented Jul 3, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

The content Preview endpoint (POST /_emdash/api/content/{collection}/{id}/preview-url) defaulted to a hard-coded /{collection}/{id} path. On any site whose content is served at a custom permalink — e.g. a posts collection with url_pattern: "/blog/{slug}" — the admin "Preview" button produced a link that 404s, because the real route is /blog/{slug}, not /posts/{id}.

The sitemap (sitemap-[collection].xml.ts) and the "View published" links already resolve the collection's url_pattern via the shared interpolateUrlPattern + localizePath helpers. This PR makes the preview link use the same resolution, so preview and published URLs stay consistent.

Path-resolution precedence is now (highest first):

  1. pathPattern in the request body (per-call override) — unchanged
  2. EMDASH_PREVIEW_PATH_PATTERN env (project-wide override) — unchanged
  3. the collection's configured url_patternnew
  4. the generic /{collection}/{id} fallback — unchanged (used only when no url_pattern is set)

Because it routes through interpolateUrlPattern, any future additions to that helper apply to preview links automatically — e.g. if the WordPress-style date tokens proposed in #1526 ({year}/{month}/{day}/..., still open) land there, preview links will resolve them too. Today the helper substitutes {slug} and {id}.

Fully backward-compatible: sites without a url_pattern see no change, and both explicit overrides still win.

Related: discussion #1525, PR #1526.

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes — emdash (core) clean
  • pnpm lint passes — oxlint --type-aware --deny-warnings clean on changed files
  • pnpm test passes (targeted): new preview-url-route.test.ts (3/3) plus adjacent preview, url-pattern, public-url, content-route-permissions suites (79/79)
  • pnpm format has been run
  • I have added/updated tests for my changes (route-level regression test for all three precedence branches)
  • User-visible strings in the admin UI are wrapped for translation (N/A — no admin/UI strings changed; no messages.po changes)
  • I have added a changeset (emdash, patch)
  • New features link to an approved Discussion — N/A (bug fix)

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Cursor + Claude Opus 4.8

Screenshots / test output

✓ tests/unit/api/preview-url-route.test.ts (3 tests)
  ✓ resolves the configured url_pattern into the preview link
  ✓ falls back to /{collection}/{id} when no url_pattern is configured
  ✓ lets an explicit pathPattern override the url_pattern

Example: a posts collection with url_pattern: "/blog/{slug}" and an entry hello-world now yields /blog/hello-world?_preview=… instead of /posts/{id}?_preview=….

The preview-url endpoint defaulted to a hard-coded /{collection}/{id}, so the
admin Preview button 404'd on any site with a custom permalink (e.g.
/blog/{slug}). The sitemap and "View published" links already resolve the
collection's url_pattern; preview links now do too, via the shared
interpolateUrlPattern + localizePath helpers. Falls back to /{collection}/{id}
when no pattern is configured; an explicit pathPattern or
EMDASH_PREVIEW_PATH_PATTERN still wins.
@changeset-bot

changeset-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: e766a63

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 16 packages
Name Type
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/fixture-perf-site Patch
@emdash-cms/perf-demo-site Patch
@emdash-cms/cache-demo-site Patch
@emdash-cms/do-demo-site Patch
@emdash-cms/do-solo-demo-site Patch
@emdash-cms/admin Patch
@emdash-cms/auth Patch
@emdash-cms/blocks Patch
@emdash-cms/gutenberg-to-portable-text Patch
@emdash-cms/x402 Patch
create-emdash Patch
@emdash-cms/auth-atproto Patch
@emdash-cms/plugin-embeds Patch

Not sure what this means? Click here to learn what changesets are.

Click here if you're a maintainer who wants to add another changeset to this PR

@github-actions github-actions Bot added review/needs-review No maintainer or bot review yet area/core size/M labels Jul 3, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 3, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

npm i https://pkg.pr.new/@emdash-cms/admin@1745

@emdash-cms/auth

npm i https://pkg.pr.new/@emdash-cms/auth@1745

@emdash-cms/auth-atproto

npm i https://pkg.pr.new/@emdash-cms/auth-atproto@1745

@emdash-cms/blocks

npm i https://pkg.pr.new/@emdash-cms/blocks@1745

@emdash-cms/cloudflare

npm i https://pkg.pr.new/@emdash-cms/cloudflare@1745

@emdash-cms/contentful-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/contentful-to-portable-text@1745

emdash

npm i https://pkg.pr.new/emdash@1745

create-emdash

npm i https://pkg.pr.new/create-emdash@1745

@emdash-cms/gutenberg-to-portable-text

npm i https://pkg.pr.new/@emdash-cms/gutenberg-to-portable-text@1745

@emdash-cms/plugin-cli

npm i https://pkg.pr.new/@emdash-cms/plugin-cli@1745

@emdash-cms/plugin-types

npm i https://pkg.pr.new/@emdash-cms/plugin-types@1745

@emdash-cms/registry-client

npm i https://pkg.pr.new/@emdash-cms/registry-client@1745

@emdash-cms/registry-lexicons

npm i https://pkg.pr.new/@emdash-cms/registry-lexicons@1745

@emdash-cms/sandbox-workerd

npm i https://pkg.pr.new/@emdash-cms/sandbox-workerd@1745

@emdash-cms/x402

npm i https://pkg.pr.new/@emdash-cms/x402@1745

@emdash-cms/plugin-ai-moderation

npm i https://pkg.pr.new/@emdash-cms/plugin-ai-moderation@1745

@emdash-cms/plugin-atproto

npm i https://pkg.pr.new/@emdash-cms/plugin-atproto@1745

@emdash-cms/plugin-audit-log

npm i https://pkg.pr.new/@emdash-cms/plugin-audit-log@1745

@emdash-cms/plugin-color

npm i https://pkg.pr.new/@emdash-cms/plugin-color@1745

@emdash-cms/plugin-embeds

npm i https://pkg.pr.new/@emdash-cms/plugin-embeds@1745

@emdash-cms/plugin-field-kit

npm i https://pkg.pr.new/@emdash-cms/plugin-field-kit@1745

@emdash-cms/plugin-forms

npm i https://pkg.pr.new/@emdash-cms/plugin-forms@1745

@emdash-cms/plugin-webhook-notifier

npm i https://pkg.pr.new/@emdash-cms/plugin-webhook-notifier@1745

commit: e766a63

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is a focused, backwards-compatible bug fix: the preview-URL endpoint now resolves a collection’s configured url_pattern (e.g. /blog/{slug}) before falling back to the generic /{collection}/{id} path, matching what the sitemap and published links already do. The precedence logic is implemented correctly, authorization uses the documented content:read_drafts permission, and the new route-level regression tests cover the primary slug-pattern, fallback, and body-override branches. The changeset is user-facing and accurate.

I did find one concrete code-level concern: a comment in the new code path claims date tokens (e.g. {year}/{month}/{day}) are supported, but the shared interpolateUrlPattern helper only substitutes {slug}, {id}, and {collection} — there is no date-token implementation in the codebase. The PR description makes the same unsupported claim, but the changeset does not, so it won’t leak into release notes. I also noted two test gaps: the EMDASH_PREVIEW_PATH_PATTERN env override branch and the new localizePath i18n prefix path are not exercised by the new tests.

None of these block the primary fix, so I’m leaving comment-level feedback rather than requesting changes.

// No explicit override: reuse the collection's `url_pattern` so the
// preview link points at the same route the sitemap and "View
// published" links already use (custom permalinks like `/blog/{slug}`,
// date tokens, etc.). Without this the generic `/{collection}/{id}`

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] The comment says the resolved URL pattern supports “custom permalinks like /blog/{slug}, date tokens, etc.”, but the shared interpolateUrlPattern helper used here only substitutes {slug}, {id}, and {collection} — there is no support for {year}, {month}, {day} or any other date tokens anywhere in the codebase. This will mislead future maintainers (and already misstates the PR’s scope in the description). Suggestion:

Suggested change
// date tokens, etc.). Without this the generic `/{collection}/{id}`
// preview link points at the same route the sitemap and "View
// published" links already use (custom permalinks like `/blog/{slug}`).
// Without this the generic `/{collection}/{id}` fallback 404s on any

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Fixed in e766a63 — the comment now says only "custom permalinks like /blog/{slug}". You're right that date tokens don't exist yet: they're PR #1526, which is still open. The PR description's claim was forward-looking (once #1526 lands in interpolateUrlPattern, preview links pick it up for free since they route through the same helper), but the code comment shouldn't describe features that aren't merged.

expect(url.startsWith(`/post/${id}?_preview=`)).toBe(true);
});

it("lets an explicit pathPattern override the url_pattern", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] The new tests cover the body-level pathPattern override and the collection url_pattern branch, but the EMDASH_PREVIEW_PATH_PATTERN env-override path in the route is not exercised. Because that branch is supposed to stay at the same precedence as a body override, consider adding a test that confirms it still wins over a configured url_pattern — even if it has to rely on build-time stubs for import.meta.env.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in e766a63: vi.stubEnv("EMDASH_PREVIEW_PATH_PATTERN", "/env/{id}") (stubs import.meta.env under vitest) with a collection that also has url_pattern: /blog/{slug} configured — the test asserts the env pattern wins and /blog/ doesn't leak through.

await teardownTestDatabase(db);
});

it("resolves the configured url_pattern into the preview link", async () => {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

[suggestion] The new url_pattern code path calls localizePath, which handles locale prefixes and custom path/codes mappings, but the current tests run with no i18n config so that logic is never exercised. Consider adding a test that sets an i18n config (via setI18nConfig) and verifies the preview link receives the expected locale prefix for a non-default-locale entry.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Added in e766a63: the test sets setI18nConfig({ defaultLocale: "en", locales: ["en", "de"], prefixDefaultLocale: false }) (plus _resetAstroI18nCacheForTests(), same pattern as the sitemap route tests), creates a de entry, and asserts the preview link comes back as /de/blog/hallo-welt?_preview=….

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 3, 2026
… and i18n prefix

The code comment claimed date tokens resolve through url_pattern, but
interpolateUrlPattern only substitutes {slug}/{id} (date tokens are
PR emdash-cms#1526, not yet merged) — comment now sticks to what exists. Two new
route tests close the review's coverage gaps: EMDASH_PREVIEW_PATH_PATTERN
still wins over a configured url_pattern, and a non-default-locale entry
gets its locale prefix via localizePath.
@github-actions github-actions Bot added review/needs-rereview Author pushed changes since the last review size/L and removed review/awaiting-author Reviewed; waiting on the author to respond size/M labels Jul 6, 2026
@swissky swissky added bot:review Trigger an emdashbot code review on this PR and removed bot:review Trigger an emdashbot code review on this PR labels Jul 10, 2026

@emdashbot emdashbot Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This is the right bug fix: the preview-URL endpoint now resolves a collection's configured url_pattern before falling back to the generic /{collection}/{id} path, keeping preview links consistent with the sitemap and published permalinks. The precedence order (body → env → collection url_pattern → fallback) is implemented correctly, authorization uses the documented content:read_drafts permission, and the tests cover the new behavior plus the two branches that were missing in the first pass.

The prior review's concerns are addressed: the route comments no longer imply unsupported date-token resolution, and the new tests exercise both EMDASH_PREVIEW_PATH_PATTERN and the localizePath i18n prefix.

I have one non-blocking suggestion: the try/catch around path resolution now also covers the new getCollectionInfoWithDb lookup, but any failure there is still surfaced as TOKEN_ERROR, which is misleading for schema/env failures. Making the error code and construction path more specific would help diagnostics, but it does not block the fix.


Findings

  • [suggestion] packages/core/src/astro/routes/api/content/[collection]/[id]/preview-url.ts:139

    With the new getCollectionInfoWithDb lookup moved inside the same try block, a schema/DB failure here is now reported to the client as TOKEN_ERROR with the generic message "Failed to generate preview URL". That code is accurate for token-signing failures but misleading for collection metadata or path-resolution failures.

    Consider either moving the schema lookup out of the token-generation try/catch or giving it its own catch so callers get a more useful code (e.g. PREVIEW_URL_ERROR or the same UNEXPECTED_ERROR-style code used elsewhere). Not a blocker for the primary fix.

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-rereview Author pushed changes since the last review labels Jul 13, 2026
@github-actions

Copy link
Copy Markdown
Contributor

This PR has been inactive for 14 days. It will be closed automatically in 7 days if there is no further activity.

If you're still working on this, please push an update or leave a comment.

@github-actions github-actions Bot added the stale label Jul 27, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core bot:review Trigger an emdashbot code review on this PR review/awaiting-author Reviewed; waiting on the author to respond size/L stale

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant