Skip to content

Add Site URL editor to admin General Settings (fixes #989)#1069

Draft
ebootheee wants to merge 3 commits into
emdash-cms:mainfrom
ebootheee:fix/site-url-admin-editor
Draft

Add Site URL editor to admin General Settings (fixes #989)#1069
ebootheee wants to merge 3 commits into
emdash-cms:mainfrom
ebootheee:fix/site-url-admin-editor

Conversation

@ebootheee
Copy link
Copy Markdown

What does this PR do?

Adds a dedicated Site URL editor to admin General Settings that updates the emdash:site_url option. This is the value getSiteBaseUrl() (in packages/core/src/api/site-url.ts) reads to build URLs in transactional emails (magic-link, invitation, password-reset). Previously this row was written once during the setup wizard via setIfAbsent() and was not editable from the admin UI, so any site whose first setup request observed a bogus origin (e.g. behind a reverse proxy without a properly forwarded Host/X-Forwarded-Host, or via a dev-bypass call from localhost before the public domain was wired up) had no in-product way to correct it. That matches the reproduction in issue #989.

Closes #989

Type of change

  • Bug fix
  • Feature (requires maintainer-approved Discussion)
  • Refactor (no behavior change)
  • Translation
  • Documentation
  • Performance improvement
  • Tests
  • Chore (dependencies, CI, tooling)

Why this shape (approach A)

This is approach A from a short menu of two:

  • Approach A (this PR): add a dedicated endpoint and a separate Email Site URL section in General Settings that writes emdash:site_url. Keeps the namespace boundary visible: emdash:* options are internal/security-sensitive (mirrors emdash:site_title, emdash:setup_complete, emdash:exclusive_hook:*), while site:* is presentation. The pre-existing site:url field in Site Identity (used for canonical links and sitemaps) is unchanged.
  • Approach B (not done here): unify the two URL fields — make the existing site:url field also write emdash:site_url, or migrate site:url to be the canonical key. Cleaner long-term, but it touches the migration system and changes the semantics of an existing user-facing field, so it felt out of scope for a fix PR. Happy to take it as a follow-up if maintainers prefer that direction.

Implementation notes

  • New route: GET/POST /_emdash/api/settings/site-url at packages/core/src/astro/routes/api/settings/site-url.ts, registered in astro/integration/routes.ts. Sits under /_emdash/api/settings, so the existing middleware scope rules (settings:read / settings:manage) cover it without an extra entry.
  • Validation: the route accepts any string via Zod, then runs new URL() and rejects (a) unparseable input, (b) non-http(s) schemes (XSS-prone — this value is interpolated into outgoing email content), and (c) any value carrying a path beyond /, a query string, or a fragment. The stored value is normalized to URL.origin so getSiteBaseUrl() can keep appending /_emdash cleanly.
  • Write path: plain OptionsRepository.set() (not setIfAbsent()) — the setup wizard's write-once guard exists to prevent Host-spoofing during the setup window, but post-setup the admin needs to be able to update the value. Permission is gated on settings:manage, which is Role.ADMIN.
  • Admin UI: separate Email Site URL card in General Settings, with its own save button and status banner, so it doesn't interleave with the existing site:url field. Help text explains the scope: Sets the base URL used for magic-link, invitation, and password-reset emails. Changes do not affect URLs in emails that have already been sent.

Tests

New: packages/core/tests/integration/astro/settings-site-url.test.ts (12 tests) covers:

  • GET returns the stored value, returns null pre-write, requires auth
  • POST stores a normalized origin and strips trailing slashes
  • POST overwrites a previously-stored value (regression guard against accidentally importing the setup wizard's setIfAbsent semantics)
  • POST rejects javascript: schemes, paths, query strings, and unparseable URLs
  • POST rejects editors (settings:read only) and unauthenticated callers

Checklist

  • I have read CONTRIBUTING.md
  • pnpm typecheck passes (whole workspace)
  • pnpm lint passes (0 errors on changed files; pre-existing warnings elsewhere)
  • Targeted tests pass: pnpm --filter emdash vitest run tests/integration/astro/settings-site-url.test.ts → 12/12 green
  • Full pnpm test clean — see notes below
  • pnpm format would be run before non-draft; happy to run on request
  • I have added tests for my changes
  • User-visible strings are wrapped with Lingui t macro
  • I have added a changeset (patch on emdash and @emdash-cms/admin)

Not verified

  • pnpm --filter emdash test had 9 unrelated failures on my Windows host (tar shell-out path issues in bundle-utils.test, file-based SQLite locking in connection.test, Vite alias tests using POSIX-only path assertions). All in files that don't touch site-url; happy to re-run on Linux if useful.
  • pnpm --filter @emdash-cms/admin test (Vitest browser/Playwright) showed a large number of pre-existing failures on my Windows host (timeout-based Matcher did not succeed in time) across unrelated components — none reference GeneralSettings, and there was no pre-existing test for that component. I didn't add one because the new section is straightforward state + react-query and matches the patterns already in the file.

AI-generated code disclosure

  • This PR includes AI-generated code — model/tool: Claude Opus 4.7 via Claude Code

Screenshots / test output

Targeted test run:

 RUN  v4.1.5 packages/core
 ✓ tests/integration/astro/settings-site-url.test.ts (12 tests) 265ms
 Test Files  1 passed (1)
      Tests  12 passed (12)

(Screenshot of the new admin section omitted — happy to add one if a maintainer wants to see it before un-drafting.)

Adds a dedicated GET/POST endpoint at /_emdash/api/settings/site-url
and a corresponding "Email Site URL" section in admin General Settings.
The endpoint updates the emdash:site_url option, which governs the base
URL used in transactional emails (magic-link, invitation, password-reset)
via getSiteBaseUrl(). Previously this value was written once during the
setup wizard via setIfAbsent() and was not editable from the admin UI,
so deployments behind reverse proxies that captured a bogus first-request
origin had no way to fix it. Fixes emdash-cms#989.

The write is gated on settings:manage. Submitted URLs are validated to
http(s) origins only (no path/query/fragment, no XSS-prone schemes), and
trailing slashes are stripped. The existing site:url field in Site
Identity (used for canonical links and sitemaps) is unchanged.
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 16, 2026

🦋 Changeset detected

Latest commit: e736a43

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

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 16, 2026

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

@github-actions github-actions Bot added size/L and removed size/XL labels May 16, 2026
@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented May 18, 2026

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

emdash

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

create-emdash

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

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

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: f5a6e92

@ebootheee
Copy link
Copy Markdown
Author

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

github-actions Bot added a commit that referenced this pull request May 22, 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.

Magic link and invitation URLs use localhost despite EMDASH_SITE_URL being configured

1 participant