Skip to content

fix(admin): resolve plugin admin pages opened at the plugin root#1305

Open
eyupcanakman wants to merge 1 commit into
emdash-cms:mainfrom
eyupcanakman:fix/plugin-admin-settings-path-resolution
Open

fix(admin): resolve plugin admin pages opened at the plugin root#1305
eyupcanakman wants to merge 1 commit into
emdash-cms:mainfrom
eyupcanakman:fix/plugin-admin-settings-path-resolution

Conversation

@eyupcanakman
Copy link
Copy Markdown
Contributor

What does this PR do?

Closes #281.

Opening a plugin from the Plugin Manager showed a Plugin Error. The Settings gear links to the plugin root (/plugins/<id>/, an empty route splat), but a plugin that registers its admin page at /settings has no page at the root, so usePluginPage returned null and the admin fell through to the Block Kit route, which 404s for a React plugin. The lookup also compared paths by exact key, so /settings and /settings/ were treated as different.

Fix:

  • resolvePluginPagePath now resolves the plugin root to the first registered page, so opening a plugin lands on its admin page instead of a 404. It also matches a trailing-slash variant, so /settings and /settings/ resolve to the same registration.
  • usePluginPage (page rendering) and the sidebar nav-visibility check share this resolver.
  • Update the plugin docs and skill reference, which said page paths must match exactly.

Testing: verified in a running admin (demos/plugins-demo, which wires the api-test plugin registered only at /test). Before, the Plugin Manager gear opened /plugins/api-test and rendered "Plugin Error: Plugin route not found (404)"; after, it renders the plugin's page. resolvePluginPagePath has unit coverage for the exact, trailing-slash (both directions), both-registered, root-fallback, and unregistered cases. @emdash-cms/admin typecheck, oxlint, and oxfmt pass.

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). No messages.po changes are included.
  • I have added a changeset (if this PR changes a published package)

AI-generated code disclosure

  • This PR includes AI-generated code (model: Claude Opus 4.8)

The Plugin Manager Settings gear opens a plugin at its root
(/plugins/<id>/, an empty route splat), but a plugin that registers its
admin page at /settings has no page there, so usePluginPage returned null
and the admin showed a Plugin Error 404. Resolve the plugin root to the
first registered page, treat /settings and /settings/ as the same path,
and share the resolver between page rendering and the sidebar nav.

Fixes emdash-cms#281
Copilot AI review requested due to automatic review settings June 2, 2026 20:56
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Jun 2, 2026

🦋 Changeset detected

Latest commit: 3ae9d46

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

This PR includes changesets to release 14 packages
Name Type
@emdash-cms/admin Patch
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/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

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Note

Copilot was unable to run its full agentic suite in this review.

Fixes plugin admin page resolution so plugins opened at their root (e.g. via Plugin Manager gear) don’t error, and page matching works regardless of trailing slashes.

Changes:

  • Add resolvePluginPagePath resolver with trailing-slash equivalence and root fallback to first registered page.
  • Update sidebar page visibility checks to use the same resolver.
  • Add Vitest coverage and update docs/changeset to reflect new behavior.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
skills/creating-plugins/references/admin-ui.md Updates reference comment to note trailing-slash tolerant page keys.
packages/admin/tests/lib/plugin-context.test.ts Adds unit tests for the new page-path resolution behavior.
packages/admin/src/lib/plugin-context.tsx Implements shared resolvePluginPagePath and updates usePluginPage to use it.
packages/admin/src/components/Sidebar.tsx Uses resolver when determining whether a configured admin page is available.
docs/src/content/docs/plugins/creating-native-plugins/react-admin.mdx Updates docs to explain trailing slash/root fallback behavior.
.changeset/fix-plugin-page-root-resolution.md Adds a patch changeset describing the fix and behavior change.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +56 to +62
/**
* Resolve a plugin page component by path, treating a trailing slash as equivalent and falling back to the first registered page at the root
*/
export function resolvePluginPagePath(
pages: Record<string, React.ComponentType> | undefined,
path: string,
): React.ComponentType | null {

<Aside type="caution">
Page paths in `pages` must match the `path` values in `admin.pages`. Widget keys must match the `id` values in `admin.widgets`.
Page paths in `pages` should match the `path` values in `admin.pages`. A trailing slash is treated as equivalent, so `/settings` and `/settings/` resolve to the same page. Opening a plugin at its root resolves to the first registered page. Widget keys must match the `id` values in `admin.widgets`.
Comment on lines +47 to +49
it("returns null when the plugin has no pages", () => {
expect(resolvePluginPagePath(undefined, "/settings")).toBeNull();
});
if (match) return match;
// The Plugin Manager gear opens a plugin at "/plugins/<id>/", so the root
// falls back to the first registered page when no page is keyed at "/".
if (path === "/") return Object.values(pages)[0] ?? null;
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Jun 2, 2026

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 3ae9d46

Copy link
Copy Markdown
Contributor

@emdashbot emdashbot Bot left a comment

Choose a reason for hiding this comment

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

This is a clean, well-scoped bug fix that solves a real usability problem: plugin admin pages opened from the Plugin Manager at the root (/plugins/<id>/) would 404 when the plugin registered its page at a sub-path like /settings.

Approach judgment: The right fix. Introducing a shared resolvePluginPagePath resolver that both page rendering (usePluginPage) and sidebar nav-visibility consume keeps the two in sync, which is better than fixing only the router and leaving the sidebar with the old exact-match logic. The trailing-slash equivalence is a sensible, low-surprise addition. Falling back from / to the first registered page is pragmatic and documented.

What I checked:

  • The resolver logic (exact match, trailing-slash toggle, root fallback, null handling) — all correct.
  • Edge cases: both variants registered (exact wins), empty pages object, undefined pages, non-root unregistered paths, root with a registered / page.
  • Call sites: usePluginPage in the router and resolvePluginPagePath in the sidebar are the only consumers; no stale direct lookups remain.
  • Test coverage: exact, trailing-slash both directions, both-registered preference, root exact, root fallback, first-of-several, unregistered non-root, and no-pages cases are all covered.
  • AGENTS.md conventions: changeset present, no new UI strings (no Lingui impact), no SQL, no Tailwind changes.
  • Docs and skill reference updated accurately.

Headline conclusion: The implementation is clean and I found no issues.

@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 Jun 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Bug: Plugin admin React page resolution is brittle for settings routes

2 participants