feat(admin): show derived title and description as SEO panel placeholders#2236
feat(admin): show derived title and description as SEO panel placeholders#2236bimsonz wants to merge 1 commit into
Conversation
…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 detectedLatest commit: ef80871 The changes in this PR will be included in the next version bump. This PR includes changesets to release 17 packages
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 |
@emdash-cms/admin
@emdash-cms/auth
@emdash-cms/auth-atproto
@emdash-cms/blocks
@emdash-cms/cloudflare
@emdash-cms/contentful-to-portable-text
emdash
create-emdash
@emdash-cms/gutenberg-to-portable-text
@emdash-cms/plugin-cli
@emdash-cms/plugin-types
@emdash-cms/registry-client
@emdash-cms/registry-lexicons
@emdash-cms/registry-verification
@emdash-cms/sandbox-workerd
@emdash-cms/x402
@emdash-cms/plugin-ai-moderation
@emdash-cms/plugin-atproto
@emdash-cms/plugin-audit-log
@emdash-cms/plugin-color
@emdash-cms/plugin-embeds
@emdash-cms/plugin-field-kit
@emdash-cms/plugin-forms
@emdash-cms/plugin-webhook-notifier
commit: |
There was a problem hiding this comment.
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.
| // `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". |
There was a problem hiding this comment.
[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.
| // `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"); |
There was a problem hiding this comment.
[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.
| 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"); |
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.
getSeoMetaalready derives those values when the panel is empty: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.
SeoPanelgains two optional props,defaultTitleanddefaultDescription, named to match the vocabularygetSeoMetaalready uses for the same concept.ContentSettingsPanelpassesdata.titleanddata.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
Checklist
pnpm typecheckpassespnpm lintpassespnpm testpasses (or targeted tests for my change)pnpm formathas been runmessages.pochanges except in translation PRs - a workflow extracts catalogs on merge tomain.AI-generated code disclosure
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.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.