Skip to content

feat(admin): show derived title and description as SEO panel placeholders#2236

Open
bimsonz wants to merge 1 commit into
emdash-cms:mainfrom
bimsonz:feat/seo-panel-default-hints
Open

feat(admin): show derived title and description as SEO panel placeholders#2236
bimsonz wants to merge 1 commit into
emdash-cms:mainfrom
bimsonz:feat/seo-panel-default-hints

Conversation

@bimsonz

@bimsonz bimsonz commented Jul 25, 2026

Copy link
Copy Markdown
Contributor

What does this PR do?

Shows the value the SEO panel will actually fall back to as placeholder text on SEO Title and Meta Description, so an empty field reads as inheriting the page's own title and excerpt rather than as unset.

getSeoMeta already derives those values when the panel is empty:

// Title: SEO panel title > caller default > content title
seo.title || defaultTitle || (typeof content.data.title === "string" ? content.data.title : null)
// Description: SEO panel description > caller default > excerpt
seo.description || defaultDescription || (typeof content.data.excerpt === "string" ? content.data.excerpt : null)

Nothing in the admin surfaced that. On a site I maintain, all 21 pages have a completely blank SEO panel and every one emits a correct, page-specific title, description and canonical. Four empty boxes reasonably read as "no SEO set", which leads to either retyping the page title into the SEO title (duplicating content that then goes stale when the title changes) or assuming the site has no SEO at all.

SeoPanel gains two optional props, defaultTitle and defaultDescription, named to match the vocabulary getSeoMeta already uses for the same concept. ContentSettingsPanel passes data.title and data.excerpt, so the placeholder shows what will really be emitted rather than a generic hint. No placeholder is rendered when there is nothing to derive from.

Generated meta is unchanged, and no new translatable strings are introduced.

Discussion: #2235

Type of change

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

Checklist

AI-generated code disclosure

  • This PR includes AI-generated code - model/tool:

Screenshots / test output

Two tests added to tests/components/SeoPanel.test.tsx, covering the placeholder being shown and being omitted when there is nothing to derive from.

Test Files  104 passed (104)
     Tests  1238 passed (1238)

Note for reviewers: this will change the admin visual regression baselines, since the SEO panel now renders placeholder text where the inputs were previously empty.

…ders

`getSeoMeta` already falls back to an entry's own content when the SEO panel is
empty:

    // Title: SEO panel title > caller default > content title
    seo.title || defaultTitle || content.data.title || ...
    // Description: SEO panel description > caller default > excerpt
    seo.description || defaultDescription || content.data.excerpt || ...

The panel gives no sign of this. Empty inputs read as "unset", so editors either
retype the page title into the SEO title, duplicating it for no benefit, or
conclude the page has no SEO and something is broken.

SeoPanel now accepts optional `defaultTitle` and `defaultDescription` and renders
them as placeholder text, so an empty field reads as inheriting the value. The
prop names match the vocabulary `getSeoMeta` already uses for the same concept.
The call site passes `data.title` and `data.excerpt`, the values that are
actually used, so the placeholder shows what will really be emitted rather than
a generic hint.

Generated meta is unchanged, and no new translatable strings are introduced.
@changeset-bot

changeset-bot Bot commented Jul 25, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: ef80871

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

This PR includes changesets to release 17 packages
Name Type
@emdash-cms/admin Patch
emdash Patch
@emdash-cms/cloudflare Patch
@emdash-cms/sandbox-workerd Patch
@emdash-cms/plugin-mcp-smoke 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/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/admin size/M review/needs-review No maintainer or bot review yet labels Jul 25, 2026
@pkg-pr-new

pkg-pr-new Bot commented Jul 25, 2026

Copy link
Copy Markdown

Open in StackBlitz

@emdash-cms/admin

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

@emdash-cms/auth

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

@emdash-cms/auth-atproto

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

@emdash-cms/blocks

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

@emdash-cms/cloudflare

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

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

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

emdash

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

create-emdash

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

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

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

@emdash-cms/plugin-cli

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

@emdash-cms/plugin-types

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

@emdash-cms/registry-client

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

@emdash-cms/registry-lexicons

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

@emdash-cms/registry-verification

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

@emdash-cms/sandbox-workerd

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

@emdash-cms/x402

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

@emdash-cms/plugin-ai-moderation

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

@emdash-cms/plugin-atproto

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

@emdash-cms/plugin-audit-log

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

@emdash-cms/plugin-color

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

@emdash-cms/plugin-embeds

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

@emdash-cms/plugin-field-kit

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

@emdash-cms/plugin-forms

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

@emdash-cms/plugin-webhook-notifier

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

commit: ef80871

@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 a real UX problem: the admin SEO panel was silent about the fallback logic that getSeoMeta already uses, so empty fields read as unset. Surfacing data.title / data.excerpt as input placeholders is small, focused, and avoids duplicating content into the SEO panel. It matches the vocabulary getSeoMeta already exposes, adds no new translatable strings or logged-out queries, and leaves generated meta unchanged.

I read the diff, the full SeoPanel.tsx, ContentSettingsPanel.tsx, the new tests, and traced getSeoMeta in packages/core/src/seo/index.ts to confirm the fallback order. ContentSettingsPanel is the only SeoPanel consumer, and the new props are optional, so there's no breakage. The changeset is present and the RTL/Tailwind surface is unchanged.

Two issues remain:

  • The "omits placeholders when there is nothing to derive from" test only asserts on SEO Title; it should also cover Meta Description.
  • The new test has a comment that restates the PR rationale and user-problem narrative, which runs against the comment-discipline rule in AGENTS.md.

Fix those and this is good to merge.

Comment on lines +298 to +300
// `getSeoMeta` falls back to the entry's own title/excerpt when this panel is
// empty. Surfacing those as placeholders is what stops an editor retyping the
// page title into the SEO title, or reading empty fields as "no SEO set".

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.

[suggestion] This block restates the PR rationale and the behavior already captured by the test name ("shows the derived title and description as placeholders"). Per AGENTS.md, comments should not be PR descriptions, summaries, or justifications for a decision — they belong in the PR body and commit message. The test name is sufficient; future readers can read the prop JSDoc and the implementation for the getSeoMeta connection. Remove the comment.

Suggested change
// `getSeoMeta` falls back to the entry's own title/excerpt when this panel is
// empty. Surfacing those as placeholders is what stops an editor retyping the
// page title into the SEO title, or reading empty fields as "no SEO set".
it("shows the derived title and description as placeholders", async () => {
const screen = await render(

</QueryWrapper>,
);

await expect.element(screen.getByLabelText("SEO Title")).not.toHaveAttribute("placeholder");

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] The test is named "omits placeholders when there is nothing to derive from" (plural), but it only asserts that the SEO Title placeholder is absent. A regression that incorrectly rendered a placeholder on Meta Description (or set it to an empty string) would not be caught. Add the matching assertion for Meta Description.

Suggested change
await expect.element(screen.getByLabelText("SEO Title")).not.toHaveAttribute("placeholder");
await expect.element(screen.getByLabelText("SEO Title")).not.toHaveAttribute("placeholder");
await expect.element(screen.getByLabelText("Meta Description")).not.toHaveAttribute("placeholder");

@github-actions github-actions Bot added review/awaiting-author Reviewed; waiting on the author to respond and removed review/needs-review No maintainer or bot review yet labels Jul 26, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/admin review/awaiting-author Reviewed; waiting on the author to respond size/M

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant