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 stale and removed stale labels 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

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant