Skip to content

fix(menus): resolve content-picker collection items to the entry URL, not the archive#1810

Merged
ascorbic merged 1 commit into
emdash-cms:mainfrom
masonjames:fix/menu-collection-item-entry-url
Jul 6, 2026
Merged

fix(menus): resolve content-picker collection items to the entry URL, not the archive#1810
ascorbic merged 1 commit into
emdash-cms:mainfrom
masonjames:fix/menu-collection-item-entry-url

Conversation

@masonjames

Copy link
Copy Markdown
Contributor

What does this PR do?

Menu items added through the admin content picker from a custom collection (anything that isn't pages/posts) link to the collection archive instead of the picked entry.

The admin's MenuEditor maps those picks to type: "collection" with referenceCollection + referenceId, and the API schema documents that non-custom types "resolve a URL from referenceCollection + referenceId" — but resolveMenuItem()'s case "collection" ignored reference_id entirely and always returned /{collection}/. Picking "Widget Co" from a projects collection produced a nav link to /projects/.

This PR makes entry references resolve the same way page/post items do (shared resolveContentUrl, so urlPattern interpolation and per-locale resolution apply), while archive links keep their current behavior:

  • type: "collection" + reference_id → entry URL (/projects/widget-co, or the collection's urlPattern); the item is hidden if the referenced entry no longer exists, matching the page/post contract.
  • type: "collection" without reference_id → archive URL /{collection}/, unchanged.
  • type: "collection" with no reference_collection → item hidden (previously rendered href="/null/").

Found while building a theme on EmDash and auditing how each menu item type resolves. Tests cover all five shapes above; the entry-reference and url_pattern tests fail on main (expected '/projects/' to be '/projects/widget-co').

Issue: none filed.

Two small related observations from the same audit — happy to open a Discussion for either:

  • getMenu(name) is an exact-match lookup, so getMenu("primary") misses a menu saved as Primary. Would you take a documented lowercase-name convention in the docs, or a case-insensitive lookup?
  • The admin has no first-class way to add a collection-archive link (the picker only picks entries), so archive nav items end up as hand-typed custom links — often pasted as absolute URLs, which bakes the deployment origin into nav data.

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
  • pnpm lint passes
  • pnpm test passes (or targeted tests for my change)
  • pnpm format has been run
  • I have added/updated tests for my changes (if applicable)
  • User-visible strings in the admin UI are wrapped for translation (if applicable). Do not include messages.po changes except in translation PRs — a workflow extracts catalogs on merge to main. N/A: no admin UI strings changed.
  • I have added a changeset (if this PR changes a published package)
  • New features link to an approved Discussion: N/A, bug fix.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Fable 5 (Claude Code)

Screenshots / test output

  • Failing test on main before the fix: AssertionError: expected '/projects/' to be '/projects/widget-co'
  • pnpm --filter emdash exec vitest run tests/unit/menus/ → 52 passed
  • Full core suite after the fix: 314 test files, 4282 tests, all passed
  • pnpm lint:json | jq '.diagnostics | length'0 (clean before and after)
  • pnpm typecheck → pass (workspace)
  • pnpm format → ran
  • Changeset added (emdash, patch)

… not the archive

The admin content picker stores entries from custom collections as
type "collection" with referenceCollection + referenceId, but the
resolver ignored the reference_id for this type and returned the
archive URL, so every such menu item linked to /projects/ instead of
the picked entry. Entry references now resolve like page/post items
(url_pattern and locale aware); items whose entry is gone are hidden,
matching the page/post contract, as are malformed items with no
collection slug (previously rendered href="/null/").
@changeset-bot

changeset-bot Bot commented Jul 3, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 8bbeb84

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 area/core size/M review/needs-review No maintainer or bot review yet labels Jul 3, 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 fix for a clear bug. The admin content picker stores custom-collection picks as type: "collection" with referenceCollection + referenceId (a translation_group), but resolveMenuItem() was ignoring reference_id for that type and always emitting the archive URL. The PR reuses the existing resolveContentUrl helper so collection entry references now get the same urlPattern interpolation and per-locale resolution as page/post items, while preserving archive-only behavior and adding the missing /null/ guard.

I checked the diff against the runtime semantics and AGENTS.md conventions:

  • Locale filtering: resolveContentUrl already filters by locale first, then falls back deterministically; this change inherits that behavior.
  • SQL safety: no raw interpolation is introduced; the dynamic table name in resolveContentUrl uses sql.ref() after validateIdentifier().
  • Changeset: present, user-facing, and correctly scoped as a patch for emdash.
  • Tests: cover entry resolution, urlPattern, archive fallback, missing content rows, and missing reference_collection.
  • No admin UI strings changed, so Lingui/i18n is not applicable.

No findings. LGTM.

@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@1810

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 8bbeb84

@github-actions github-actions Bot added review/approved Approved; no new commits since and removed review/needs-review No maintainer or bot review yet labels Jul 4, 2026

@ascorbic ascorbic left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Thanks!

@ascorbic ascorbic merged commit dd05063 into emdash-cms:main Jul 6, 2026
46 of 47 checks passed
@emdashbot emdashbot Bot mentioned this pull request Jul 6, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/core review/approved Approved; no new commits since size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants