Skip to content

Expose Astro's trailingSlash config to plugins via ctx.site#2122

Merged
ascorbic merged 7 commits into
emdash-cms:mainfrom
bimsonz:feat/expose-trailing-slash-to-plugins
Jul 21, 2026
Merged

Expose Astro's trailingSlash config to plugins via ctx.site#2122
ascorbic merged 7 commits into
emdash-cms:mainfrom
bimsonz:feat/expose-trailing-slash-to-plugins

Conversation

@bimsonz

@bimsonz bimsonz commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Exposes the host Astro project's trailingSlash config to plugins via ctx.site.trailingSlash, so plugins that build absolute URLs (sitemaps, canonical, hreflang) can match the site's routing policy. Without it, plugins hardcode a trailing slash — correct for Astro's default, but wrong for a site configured trailingSlash: 'never', notably a headless front-end that serves bare URLs, where a plugin's sitemap/canonical then advertise URLs the site doesn't serve.

It rides the same path the i18n config already uses: astro:config:setup captures astroConfig.trailingSlashvirtual:emdash/configreadSiteInfo()SiteInfo. It's optional on SiteInfo, and createSiteInfo defaults it to 'ignore' (Astro's default), so existing construction is unaffected. Sandboxed plugins receive it too: the sandbox already forwards the full normalized SiteInfo ("the same normalized site context used by trusted plugin hooks and routes"), and this PR makes that explicit by typing trailingSlash on the sandbox/wrapper site payloads (core + workerd + cloudflare).

Motivating consumer: jdevalk/emdash-plugin-seo#2 (its schema/map sitemap feed).

Type of change

Discussion: #2124

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code

Screenshots / test output

N/A

Plugins that build absolute URLs (sitemap, canonical, hreflang) need to match the
site's URL convention, but ctx.site only carried name/url/locale, so plugins had to
hardcode a trailing slash. That's wrong for a site configured with
trailingSlash: 'never' -- notably a headless front-end that serves bare URLs, where
the plugin's sitemap/canonical then advertise URLs the site doesn't serve.

Capture astroConfig.trailingSlash at astro:config:setup, carry it on
virtual:emdash/config, and surface it as ctx.site.trailingSlash -- the same path the
i18n config already rides. Optional on SiteInfo; createSiteInfo defaults it to
'ignore' (Astro's default) so existing construction is unaffected.
@changeset-bot

changeset-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 9251b2e

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

This PR includes changesets to release 16 packages
Name Type
emdash Minor
@emdash-cms/cloudflare Minor
@emdash-cms/sandbox-workerd Minor
@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 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/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 commented Jul 18, 2026

Copy link
Copy Markdown
Contributor

All contributors have signed the CLA ✍️ ✅
Posted by the CLA Assistant Lite bot.

@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 change for the stated problem: Astro-native sites need a way to expose trailingSlash to plugins that build absolute URLs, and threading it through virtual:emdash/config → runtime site info → ctx.site matches how i18n already travels. The implementation is small and additive, and the new test correctly covers createSiteInfo.

I found two issues that should be addressed before merge:

  1. Missing changeset. The PR changes the published emdash package, so AGENTS.md/CONTRIBUTING.md require a changeset.
  2. The sandbox payload is inadvertently widened. The PR description says the sandbox boundary is not widened, so sandboxed plugins should keep seeing the default 'ignore'. But createSandboxRunnerOptions passes the full SiteInfo object (now including trailingSlash) into the sandbox runner; the Cloudflare wrapper then JSON-stringifies that whole object into the generated worker code. That means out-of-process plugins do receive the real trailingSlash value, contrary to the stated scope and to the unchanged SandboxOptions / WrapperOptions types.

I did not find any SQL, authorization, i18n/Tailwind, or runtime-security issues in this diff.


Findings

  • [needs fixing] packages/core/src/astro/integration/index.ts:389

    This line changes the published emdash package, but the PR does not include a changeset. AGENTS.md and CONTRIBUTING.md require a changeset for any published package change.

    Add a changeset such as .changeset/expose-trailing-slash.md:

    ---
    "emdash": minor
    ---
    
    Exposes the host Astro project's `trailingSlash` config to plugins via `ctx.site.trailingSlash`, so plugins that build absolute URLs (sitemaps, canonical, hreflang) can match the site's routing policy.
  • [needs fixing] packages/core/src/plugins/sandbox/runner-options.ts:14

    The PR description explicitly says the sandbox boundary is not widened here, so sandboxed plugins should continue seeing the default 'ignore'. However, createSiteInfo(siteInfo ?? {}) now always emits a trailingSlash property, and that full SiteInfo object is passed into the sandbox runner. The Cloudflare wrapper then JSON.stringifys the whole object into the generated worker code, so out-of-process plugins actually receive the host's real trailingSlash value. This both contradicts the stated scope and silently changes the sandbox wire format without updating SandboxOptions / WrapperOptions.

    To honor the out-of-scope note, build the sandbox siteInfo from only name/url/locale:

    export function createSandboxRunnerOptions(
    	options: Omit<SandboxOptions, "siteInfo">,
    	siteInfo?: SiteInfoOptions,
    ): SandboxOptions {
    	const normalized = createSiteInfo(siteInfo ?? {});
    	return {
    		...options,
    		siteInfo: { name: normalized.name, url: normalized.url, locale: normalized.locale },
    	};
    }
    

@bimsonz

bimsonz commented Jul 18, 2026

Copy link
Copy Markdown
Contributor Author

I have read the CLA Document and I hereby sign the CLA

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

github-actions Bot added a commit that referenced this pull request Jul 18, 2026
@github-actions github-actions Bot added cla: signed review/awaiting-author Reviewed; waiting on the author to respond and removed cla: needed review/needs-review No maintainer or bot review yet labels Jul 18, 2026
Review follow-up (emdash-cms#2122): createSiteInfo already flows the full SiteInfo — including
trailingSlash — to sandboxed plugins via the runner options, matching the documented
'same normalized site context used by trusted plugin hooks and routes' parity. Make
that explicit by adding trailingSlash to the sandbox site-shape types (core + the
workerd/cloudflare wrappers) so the wire format is typed, not silently widened. Add
the changeset for the published emdash change.
@github-actions github-actions Bot added area/cloudflare size/M review/needs-rereview Author pushed changes since the last review and removed size/S review/awaiting-author Reviewed; waiting on the author to respond labels Jul 18, 2026
@swissky swissky added the bot:review Trigger an emdashbot code review on this PR label Jul 18, 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 change for the stated problem: Astro-native sites need a way to expose trailingSlash to plugins that build absolute URLs, and threading it through virtual:emdash/config → runtime site info → ctx.site matches how i18n already travels. It is additive, backward-compatible (defaults to Astro's 'ignore'), and the implementation is small.

Re-review status: the two issues from the previous review are addressed.

  1. Changeset added.changeset/expose-trailing-slash.md is now present.
  2. Sandbox scope clarified — the PR description and changeset now explicitly state that sandboxed plugins receive trailingSlash, and the core / Cloudflare / workerd wrapper site payloads have been typed consistently. The earlier contradiction is resolved.

What I checked: read the diff, the changed source files, the changeset, and the AGENTS.md/CONTRIBUTING.md changeset requirements. I traced the data flow from astro:config:setup → serialized config → readSiteInfo()createSiteInfo() → trusted plugin context factory and sandbox-runner options → Cloudflare/Workerd wrapper generation. I did not find SQL, authorization, i18n/Tailwind, or runtime-security issues.

Headline concern: the changeset only covers the emdash package, but this PR also changes the public behavior of two other published packages — @emdash-cms/cloudflare and @emdash-cms/sandbox-workerd — because their sandbox runners/wrappers now forward trailingSlash into generated plugin wrappers. CONTRIBUTING.md requires a changeset for every published package whose behavior changes. That should be fixed before merge.

@@ -0,0 +1,5 @@
---
"emdash": minor

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.

[needs fixing] This changeset only lists emdash, but the PR also changes two other published packages: @emdash-cms/cloudflare and @emdash-cms/sandbox-workerd. Their sandbox runners/wrappers now forward trailingSlash into generated worker code (e.g. WrapperOptions.site and the runner siteInfo fields are widened), which is a real public-package behavior change.

CONTRIBUTING.md says a changeset is required for every published package whose behavior changes; otherwise the change won't trigger a release.

Add the other affected packages to this changeset so the coordinated release is captured:

Suggested change
"emdash": minor
---
"emdash": minor
"@emdash-cms/cloudflare": minor
"@emdash-cms/sandbox-workerd": minor
---
Expose the host Astro project's `trailingSlash` config to plugins via `ctx.site.trailingSlash`, so plugins that build absolute URLs (sitemaps, canonical, hreflang) can match the site's routing policy. Available to in-process and sandboxed plugins alike.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 18, 2026
The sandbox runners/wrappers forward trailingSlash into generated worker
code, so both published packages change behavior and must be released
alongside emdash core.
@swissky swissky added the bot:review Trigger an emdashbot code review on this PR label Jul 19, 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 change for the stated problem: exposing Astro’s trailingSlash to plugins through the existing virtual:emdash/config → runtime site info → ctx.site pipeline is additive, backward-compatible (it defaults to Astro’s 'ignore'), and matches how i18n already travels. It’s now also correctly typed through the sandbox runners/wrappers in all three affected packages.

Re-review status: the two issues from the previous review are resolved.

  1. Changeset added.changeset/expose-trailing-slash.md now includes emdash, @emdash-cms/cloudflare, and @emdash-cms/sandbox-workerd.
  2. Sandbox scope clarified — the PR description and changeset explicitly state that sandboxed plugins receive trailingSlash, and the core / Cloudflare / workerd wrapper site payloads have been typed consistently.

New issue: I found one test regression caused by the new field. createSiteInfo now always populates trailingSlash (defaulting to "ignore"), but packages/core/tests/unit/plugins/sandbox-runner-options.test.ts uses strict toEqual assertions on the normalized siteInfo object without the new field, so those tests will fail once the change runs. The fix is to add trailingSlash: "ignore" to the expected objects.

What I checked: diff, all changed source files, the changeset, createSandboxRunnerOptions, PluginContextFactory, the Cloudflare/workerd wrapper generators, and existing test expectations around siteInfo. I traced the data flow from astro:config:setup → serialized config → readSiteInfo()createSiteInfo() → trusted plugin context factory and sandbox-runner options → wrapper JSON injection. I did not run the test suite, so failure claims are static (exact toEqual mismatches).


Findings

  • [needs fixing] packages/core/tests/unit/plugins/sandbox-runner-options.test.ts:17-25

    createSiteInfo now always adds trailingSlash (defaulting to "ignore"), so the normalized siteInfo returned by createSandboxRunnerOptions will include it. This strict toEqual assertion will fail.

    		expect(options).toEqual({
    			db,
    			siteInfo: {
    				name: "Example Site",
    				url: "https://example.com",
    				locale: "nl",
    				trailingSlash: "ignore",
    			},
    		});
    
  • [needs fixing] packages/core/tests/unit/plugins/sandbox-runner-options.test.ts:36

    The default siteInfo from createSiteInfo now includes trailingSlash: "ignore", so this exact match will fail.

    		expect(options.siteInfo).toEqual({ name: "", url: "", locale: "en", trailingSlash: "ignore" });
    
  • [suggestion] packages/core/tests/integration/runtime/plugin-route-site-info.test.ts:82-86

    Since trailingSlash now flows into trusted plugin routes via ctx.site, consider asserting it here to cover the end-to-end site-context propagation rather than only the unit-level createSiteInfo tests. This toMatchObject assertion is a subset, so adding the field is safe.

    				site: {
    					name: "Example Site",
    					url: "https://example.com",
    					locale: "nl",
    					trailingSlash: "ignore",
    				},
    

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 19, 2026
@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond review/needs-rereview Author pushed changes since the last review and removed review/needs-rereview Author pushed changes since the last review review/awaiting-author Reviewed; waiting on the author to respond labels Jul 19, 2026
createSiteInfo now always populates trailingSlash (default "ignore"), so the strict
siteInfo expectations must include it: sandbox-runner-options (toEqual x2),
create.test.ts (objectContaining on the sandbox-runner siteInfo), and the
plugin-route-site-info integration test (end-to-end). Verified by running the full
core suite locally (4753 passing).
@bimsonz
bimsonz force-pushed the feat/expose-trailing-slash-to-plugins branch from 547d82d to 675f531 Compare July 19, 2026 13:55
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 20, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 20, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-verification

npm i https://pkg.pr.new/emdash-cms/emdash/@emdash-cms/registry-verification@2122

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: 9251b2e

@ascorbic ascorbic added the bot:review Trigger an emdashbot code review on this PR label Jul 21, 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 change for the stated problem: exposing Astro's trailingSlash routing policy to plugins through the existing virtual:emdash/config → runtime site info → ctx.site pipeline is additive, backward-compatible (it defaults to Astro's 'ignore'), and mirrors how i18n already travels. It's now also correctly typed through the sandbox runners/wrappers in all three affected packages.

Re-review status: the finding from the previous review is resolved.

  • packages/core/tests/unit/plugins/sandbox-runner-options.test.ts now expects trailingSlash: "ignore" in both toEqual assertions, and packages/core/tests/integration/runtime/plugin-route-site-info.test.ts asserts the field in the end-to-end site-context output. No other strict site-info assertions remain in the test files I traced.

What I checked: the unified diff, all changed source files (astro/integration/index.ts, emdash-runtime.ts, plugins/context.ts, plugins/types.ts, plugins/sandbox/types.ts, virtual-modules.d.ts, plus the Cloudflare and workerd sandbox runner/wrapper modules), the changeset, and the updated tests. I traced the data flow from astro:config:setup → serialized config → readSiteInfo()createSiteInfo() → trusted plugin context factory and sandbox-runner options → Cloudflare/workerd wrapper JSON injection. The runtime, type declarations, wrapper payloads, and tests are all consistent.

I did not run the test suite, lint, or typecheck, so CI remains the source of truth for those. I also could not verify from the repo that Discussion #2124 is approved (no GitHub access from this environment).

No remaining blocking or suggestion findings.

@emdashbot emdashbot Bot removed the bot:review Trigger an emdashbot code review on this PR label Jul 21, 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 enabled auto-merge (squash) July 21, 2026 06:25
@ascorbic
ascorbic merged commit 6bbf93a into emdash-cms:main Jul 21, 2026
48 checks passed
swissky pushed a commit to swissky/emdash that referenced this pull request Jul 21, 2026
…ms#2122)

* Expose Astro's trailingSlash config to plugins via ctx.site

Plugins that build absolute URLs (sitemap, canonical, hreflang) need to match the
site's URL convention, but ctx.site only carried name/url/locale, so plugins had to
hardcode a trailing slash. That's wrong for a site configured with
trailingSlash: 'never' -- notably a headless front-end that serves bare URLs, where
the plugin's sitemap/canonical then advertise URLs the site doesn't serve.

Capture astroConfig.trailingSlash at astro:config:setup, carry it on
virtual:emdash/config, and surface it as ctx.site.trailingSlash -- the same path the
i18n config already rides. Optional on SiteInfo; createSiteInfo defaults it to
'ignore' (Astro's default) so existing construction is unaffected.

* Type trailingSlash on the sandbox site payload + add changeset

Review follow-up (emdash-cms#2122): createSiteInfo already flows the full SiteInfo — including
trailingSlash — to sandboxed plugins via the runner options, matching the documented
'same normalized site context used by trusted plugin hooks and routes' parity. Make
that explicit by adding trailingSlash to the sandbox site-shape types (core + the
workerd/cloudflare wrappers) so the wire format is typed, not silently widened. Add
the changeset for the published emdash change.

* changeset: declare @emdash-cms/cloudflare + @emdash-cms/sandbox-workerd

The sandbox runners/wrappers forward trailingSlash into generated worker
code, so both published packages change behavior and must be released
alongside emdash core.

* test: expect trailingSlash in normalized siteInfo

createSiteInfo now always populates trailingSlash (default "ignore"), so the strict
siteInfo expectations must include it: sandbox-runner-options (toEqual x2),
create.test.ts (objectContaining on the sandbox-runner siteInfo), and the
plugin-route-site-info integration test (end-to-end). Verified by running the full
core suite locally (4753 passing).

* style: format

---------

Co-authored-by: Matt Kane <mkane@cloudflare.com>
Co-authored-by: emdashbot[bot] <emdashbot[bot]@users.noreply.github.com>
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