Skip to content

feat: extensions — custom admin pages with sidebar navigation#572

Closed
mohamedmostafa58 wants to merge 4 commits into
emdash-cms:mainfrom
mohamedmostafa58:feat/extensions
Closed

feat: extensions — custom admin pages with sidebar navigation#572
mohamedmostafa58 wants to merge 4 commits into
emdash-cms:mainfrom
mohamedmostafa58:feat/extensions

Conversation

@mohamedmostafa58
Copy link
Copy Markdown
Contributor

Extensions: Custom Admin Pages

Why

Plugins run in a constrained environment -- they interact with the CMS through hooks and a scoped API. Some use cases need direct database access, full Astro rendering, or custom server-side logic that does not fit the plugin model. Examples: static site export, analytics dashboards, bulk content tools, deployment management.

Extensions fill this gap. They are first-class Astro pages that live inside the project, have full access to Astro.locals (database, storage, auth), and appear as sidebar items in the admin UI.

Developer Experience

Configuration

// astro.config.mjs
emdash({
  extensions: [
    { name: "static-deploy", label: "Static Export", icon: "rocket", group: "manage" },
    { name: "analytics", label: "Analytics", icon: "globe", group: "content" },
    { name: "bulk-ops", label: "Bulk Operations", icon: "database", group: "admin" },
  ],
})

File Structure

src/
  extensions/
    static-deploy/
      page.astro          # Required -- the extension page
      integration.ts      # Optional -- inject additional routes or config
    analytics/
      page.astro

Extension Page

A standard Astro page with full access to the CMS runtime:

---
export const prerender = false;
const user = (Astro.locals as any).user;
if (!user) return Astro.redirect("/_emdash/admin");

const { emdash } = Astro.locals;
const posts = await emdash.db.selectFrom("ec_posts").selectAll().execute();
---
<html>
<body>
  <h1>My Extension</h1>
  <p>Found {posts.length} posts.</p>
</body>
</html>

Sidebar Integration

Each extension appears in one of three sidebar groups based on the group field:

Group Description Default
content Alongside collections and media
manage With comments, menus, redirects Yes
admin With users, plugins, settings

The admin UI renders a TanStack Router route at /ext/$name that loads the extension page in an iframe pointing to /_emdash/ext/{name}. This keeps the sidebar and header chrome visible while the extension has full control over its content area.

How It Works

  1. At build time, the integration validates each extension name against /^[a-z0-9]+(?:-[a-z0-9]+)*$/, resolves src/extensions/{name}/page.astro, and calls injectRoute to register it at /_emdash/ext/{name}.

  2. The serialized extension metadata (label, icon, group, URL) is baked into the virtual config module and included in the manifest response.

  3. The admin sidebar reads extensions from the manifest and adds nav items to the appropriate group. Clicking an extension navigates to the TanStack Router route, which renders the iframe.

  4. If integration.ts exists, it is dynamically imported at build time and can inject additional routes or modify the Astro config.

Security

  • Slug validation: Extension names must match /^[a-z0-9]+(?:-[a-z0-9]+)*$/. No dots, slashes, or special characters.
  • Path traversal protection: The resolved directory is checked with startsWith(projectRoot) to prevent escaping the project root.
  • Client-side validation: The iframe route validates the slug before constructing the src URL.
  • Duplicate detection: Duplicate extension names are rejected with a warning.
  • Auth: Extension pages handle their own auth checks (same as any Astro page). The middleware chain (auth, CSRF) runs before the extension page.

Icon Mapping

Extensions specify icons by name. Currently supported:

Icon name Phosphor icon
rocket Rocket
upload Upload
database Database
gear Gear
list List
globe Globe

Falls back to Upload if the icon name is not recognized.

Extensions vs Plugins

Extensions Plugins
Access Full Astro locals (db, storage, auth) Scoped API via hooks
UI Full Astro page (HTML, any framework) Block Kit or React components
Location Project source (src/extensions/) npm package
Sandboxing None (trusted, same as your own pages) Optional (V8 isolate)
Distribution Not distributable Publishable to marketplace
Use case Site-specific tools, dashboards, admin pages Reusable functionality

Use extensions when you need full server-side control for site-specific tooling. Use plugins when building reusable functionality that other sites can install.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 15, 2026

🦋 Changeset detected

Latest commit: 6cf11bd

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

This PR includes changesets to release 9 packages
Name Type
emdash Minor
@emdash-cms/admin Minor
@emdash-cms/cloudflare Minor
@emdash-cms/auth Minor
@emdash-cms/blocks Minor
@emdash-cms/gutenberg-to-portable-text Minor
@emdash-cms/x402 Minor
create-emdash Minor
@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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 15, 2026

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

emdash

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

create-emdash

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

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

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 6cf11bd

@github-actions github-actions Bot added size/L and removed size/M labels Apr 15, 2026
@nickgraynews
Copy link
Copy Markdown
Contributor

This would be cool! I hope we can add this.

@ascorbic
Copy link
Copy Markdown
Collaborator

A lot of this could be done already with trusted plugins (which I'd like to rename something like native plugins): https://docs.emdashcms.com/plugins/sandbox/

@github-actions
Copy link
Copy Markdown
Contributor

Overlapping PRs

This PR modifies files that are also changed by other open PRs:

This may cause merge conflicts or duplicated work. A maintainer will coordinate.

@mohamedmostafa58
Copy link
Copy Markdown
Contributor Author

@ascorbic A few things trusted plugins currently cannot do:

  • ctx does not expose raw database access — ctx.content queries ec_* collections but not system tables (menus, taxonomies, media, widgets, options, seo, bylines)
  • ctx.storage is a plugin-scoped document store (get/put/query by ID), not the site R2 bucket — no way to upload files to R2
  • Block Kit cannot render custom HTML or run client-side JavaScript — works for forms and buttons but not for charts, visualizations, or custom interactions
  • React adminEntry runs client-side only — no server-side rendering, no direct db access
  • generateSnapshot() exists internally but is not exported from the emdash package
  • Plugin admin pages always appear under the Plugins section — no way to place items in Content, Manage, or Admin sidebar groups

Exposing ctx.db and ctx.siteStorage on trusted plugins, making generateSnapshot a public export, and allowing plugins to specify which sidebar group their admin pages appear in would cover most cases. Extensions would still be useful for admin tools that need full custom UI beyond Block Kit.

Happy to go either direction.

@ascorbic
Copy link
Copy Markdown
Collaborator

I am going to close this for now because features need a discussion first, but please do open a discussion instead. I will say I am skeptical: a lot of this could be accomplished with native plugins, custom PT blocks and/or Astro integrations. Look at the embeds plugin in the monorepo for an example.

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.

3 participants