feat(website-content): require SEO fields to publish, and align the published control bar - #4009
Merged
Merged
Conversation
It is the same bar in the same place, but the two looked unrelated: the published view ran the full width of the window with its buttons against the left edge and the status against the right, while edit mode puts the status at the left end and the actions at the right, both lined up with the breadcrumb and the text. The published view now reuses the same `ActionRow`, so it carries the article's 890px column and cannot drift from edit mode when that column changes. Also drops the flex wrapper around the read-only toolbar. It set gap and margin on a `position: fixed` child, which it could not lay out either way, and now that the button group uses the same container inside the row its presence there was actively misleading. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
OpenAPI ChangesNo changes detected Unexpected changes? Ensure your branch is up-to-date with |
Contributor
There was a problem hiding this comment.
Copilot review overview
🟡 Changes recommended
Nested toolbar padding and the action-group margin prevent the advertised edge alignment.
Review effort: Balanced
Findings: 1
Open (1)
What changed in this PR
Aligns published website-content controls with the centered edit-mode action row.
Changes:
- Reuses
ActionRowfor published controls. - Adds layout regression tests and removes a duplicate assertion.
- Two gutter/alignment issues remain.
| File | Description |
|---|---|
WebsiteContentEditor.tsx |
Restructures the published toolbar. |
ArticleEditor.happydom.test.tsx |
Tests published toolbar structure and ordering. |
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
They are what a search result and a link preview show, and without them the page head falls back to the title and whatever the body happens to open with -- which is the thin description the SEO rules single out. So publishing now insists on both: the press opens the settings drawer instead, and resumes once they are written. Held to the same rule as topics, and implemented the same way, for the same reason -- it has to be the publish and not the save, because a draft writes itself every couple of seconds and autosave cannot stop to ask. Unlike topics this applies to news as well: news has no topics section, so the SEO fields are the only thing its publish can wait on. The drawer refuses to save while a publish is waiting on it and still has not got what it needs. Saving closes the drawer and closing forgets the press, so allowing it would drop the publish with nothing on screen to say why -- a hole the topics rule had too. `awaitingTopicsForPublish` becomes `awaitingSettingsForPublish`, since it now covers either requirement, and the resume tests what the drawer just handed over rather than state that has not landed. `!topicsRequired` in that test is what keeps news from stranding: its drawer sends no `topics` at all, so a bare length check would never resume. Messages key on whether the content is published rather than on whether the save is refused. The two used to coincide and no longer do -- a draft's save is refused too while a publish waits -- and telling a draft it is published would simply be wrong. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
daniellefrappier18
approved these changes
Sep 29, 2026
daniellefrappier18
left a comment
Contributor
There was a problem hiding this comment.
Not blocking but worth a mention - the "Required" indicator on SEO fields misrepresents actual enforcement.
- You open a brand-new, empty draft article and click Settings.
- The SEO Title and SEO Description fields show a red asterisk (*) next to their labels, and a screen reader announces them as "required."
- You leave both fields blank and click "Save Settings." It saves fine — nothing stops you.
Screen.Recording.2026-09-29.at.12.39.27.PM.mov
The drawer marked both SEO fields required, put an asterisk on each label, and said an SEO title and description were needed to publish -- then let Save Settings through with both empty. Same for topics. The refusal now follows the requirement, on a draft as much as on something public. That collapses `topicsMayNotBeEmptied` and `seoMayNotBeEmptied` into `topicsRequired` and `seoRequired`: the two were always going to agree, and a second prop that only ever restated the first was the reason the button and the label could disagree in the first place. `contentIsPublished` stays, since it still picks which sentence a section shows. A draft's *content* is untouched by this -- autosave keeps writing it, and only the drawer's own settings wait on being complete. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.

What are the relevant tickets?
Part of https://github.com/mitodl/hq/issues/13253
Description (What does it do?)
Two changes to the content editor's control bar and settings drawer.
1. The published bar is laid out like the edit bar.
It is the same bar in the same place, but the two looked unrelated: the published view ran the full width of the window, with Draft, Edit and the settings icon hard against the left edge and
Status: Publishedagainst the right, while edit mode puts the status at the left end and the actions at the right, both lined up with the breadcrumb and the article text.The published view now reuses the same
ActionRow, so it gets the article's 890px centred column and the same ordering. Reusing the row rather than restyling it means the two cannot drift apart again — that row already carries a comment saying its width has to keep following the banner and the body, and now one row does that for both modes.This also drops the flex wrapper that sat around the read-only toolbar. It set
gapandmargin-righton aposition: fixedchild, which it could not lay out either way, and now that the button group uses that same container inside the row, leaving it wrapped around the outside was actively misleading.2. Publishing requires an SEO title and description.
They are what a search result and a link preview show. Without them the page head falls back to the content's title and whatever the body happens to open with — which for an unreadable body is the bare
"Learn with MIT", the thin description the SEO rules document singles out. So publishing insists on both: the press opens the settings drawer instead, and resumes once they are written.This is the same rule as topics, implemented the same way, and it has to hang on the publish rather than the save — a draft writes itself every couple of seconds and autosave cannot stop to ask. A draft may therefore sit without them; publishing is where they are insisted on.
Unlike topics it applies to news as well as articles: news has no topics section at all, so the SEO fields are the only thing its publish can ever be waiting on. That case is also what forced the resume to test
!topicsRequiredrather than a bare topic count — a length check alone would strand a news publish forever.The drawer refuses to save while anything required is blank — on a draft as much as on something public. It marked both SEO fields required, put an asterisk on each label and said they were needed to publish, then let Save Settings through with both empty; topics behaved the same way. That collapses
topicsMayNotBeEmptiedandseoMayNotBeEmptiedintotopicsRequiredandseoRequired: the two were always going to agree, and a second prop restating the first is what let the button and the label disagree.contentIsPublishedstays, since it still picks which sentence a section shows.A draft's content is untouched by this — autosave keeps writing it, and only the drawer's own settings wait on being complete. Cancel always works.
The fields are marked
required, so each label carries an asterisk. That changed their accessible names, hence the query updates across three test files.Screenshots (if appropriate):
The bar change is the alignment and ordering: the row now starts with the status and ends with the actions, within the article's column rather than the window's edges, matching the edit bar. Worth a look with an article open in each mode.
Pressing Publish on content with no SEO title or description now opens the settings drawer, with the section reading "Add an SEO title and description to publish your article."
How can this be tested?
Status: Publishedon the left, then Draft, Edit and the settings icon grouped on the right, all within the width of the article text below — not the window. Press Edit: same shape.Additional Context
Worth knowing before merge: any content with blank SEO fields — published or draft — cannot have its settings saved, or be published, until someone writes them. That is deliberate and what was asked for, but it is a content-operations consequence rather than a purely additive change: if there is a backlog of published articles with blank SEO, the next person to touch any of their settings has to fill them in first. Editing and autosaving the body is unaffected.
Tests: two layout cases for the published bar mirroring the edit-mode ones, and seven for the SEO rule — publish held back, one field not being enough, whitespace not passing for a title, the resume, the drawer's refusal while a publish waits, a published item not being allowed to empty them, and a draft being allowed to sit without them. Plus the news-only case, which is the one that would catch the stranded-publish bug.
Also removed a duplicated assertion in
a published article offers Draft, Edit and Settings— the sameUnpublish Articlecheck appeared twice, left behind by an earlier merge.Unrelated pre-existing flake, not introduced here:
ArticleEditor.happydom.test.tsxfails intermittently in a full-file run, most often onan article that has never been saved is created once, then updated, with anact(...)warning or a timed-out autosave wait. I verified it fails on this branch's parent with these changes stashed (2 of 3 runs), and that the test passes in isolation. The cause isvendor/.../toolbar/toolbar.tsx:38—useToolbarNavigationobserves its whole subtree with a MutationObserver and callssetItems(collectItems())unconditionally from the callback, which happy-dom delivers as a microtask outside React'sact. The fix is probably to skip thatsetItemswhen the collected items have not changed, which also stops a re-render per DOM mutation in production. Worth its own PR.🤖 Generated with Claude Code