From 470584400f470555220c73e5879c3db7ee18c892 Mon Sep 17 00:00:00 2001 From: Florian Sylvain Date: Fri, 16 Oct 2026 03:06:57 +0200 Subject: [PATCH] feat: SEO SPA editor integration --- features/editor_seo.feature | 532 ++++++++++++++ features/editor_seo.qa.md | 65 ++ web/src/components/ApiPreviewDialog.vue | 198 +++++ web/src/components/ArticleDetails.vue | 2 +- web/src/components/AuthorFormPanel.vue | 1 + web/src/components/DirectoryPanel.vue | 12 +- web/src/components/FallbackChip.vue | 13 + web/src/components/ImageUploader.vue | 1 + web/src/components/MediaField.vue | 4 +- web/src/components/MediaPicker.vue | 21 +- web/src/components/SearchPreview.vue | 102 +++ web/src/components/SearchSharingSection.vue | 136 ++++ web/src/components/SegmentedTabs.vue | 33 + web/src/components/SeoDisclosure.vue | 44 ++ web/src/components/SeoPanel.vue | 679 ++++++++++++++++++ web/src/components/SlugField.vue | 18 +- web/src/components/SocialCardPreview.vue | 101 +++ web/src/components/SoftCounter.vue | 18 + web/src/components/SuggestionList.vue | 117 +++ .../__tests__/useArticleDraftSeo.spec.ts | 230 ++++++ web/src/composables/useArticleDraft.ts | 108 ++- web/src/composables/useArticles.ts | 3 +- web/src/composables/useMediaNames.ts | 34 + web/src/composables/useResolvedSeo.ts | 42 ++ web/src/composables/useSeoCheck.ts | 150 ++++ web/src/composables/useSites.ts | 3 + web/src/i18n/messages.ts | 129 ++++ web/src/i18n/seoMessages.spec.ts | 193 +++++ web/src/lib/__tests__/seoCopy.spec.ts | 49 ++ web/src/lib/__tests__/seoPreview.spec.ts | 75 ++ web/src/lib/articleSeo.ts | 118 +++ web/src/lib/booleanMemory.ts | 20 + web/src/lib/imageChoice.ts | 1 + web/src/lib/metaSectionMemory.ts | 20 +- web/src/lib/segmentStyles.ts | 3 + web/src/lib/seoApiPreview.ts | 105 +++ web/src/lib/seoCopy.ts | 85 +++ web/src/lib/seoPreview.ts | 128 ++++ web/src/lib/seoSectionMemory.ts | 8 + web/src/lib/seoTypes.ts | 71 ++ web/src/views/EditorView.vue | 285 +++++++- .../views/__tests__/EditorImageUpload.spec.ts | 3 +- .../views/__tests__/EditorMediaPicker.spec.ts | 8 +- .../views/__tests__/EditorMetaSection.spec.ts | 4 +- .../views/__tests__/EditorSeoCheck.spec.ts | 350 +++++++++ .../views/__tests__/EditorSeoDialog.spec.ts | 415 +++++++++++ .../views/__tests__/EditorSeoFrench.spec.ts | 89 +++ .../views/__tests__/EditorSeoMemory.spec.ts | 153 ++++ .../views/__tests__/EditorSeoPanel.spec.ts | 571 +++++++++++++++ web/src/views/__tests__/EditorSeoSave.spec.ts | 446 ++++++++++++ .../views/__tests__/EditorSeoSection.spec.ts | 140 ++++ .../views/__tests__/EditorSeoSession.spec.ts | 145 ++++ web/src/views/__tests__/EditorSeoSlug.spec.ts | 124 ++++ .../__tests__/EditorSeoSuggestions.spec.ts | 332 +++++++++ .../views/__tests__/EditorVariants.spec.ts | 10 +- web/src/views/__tests__/EditorView.spec.ts | 21 +- .../__tests__/SitesAuthorsSessions.spec.ts | 3 +- web/src/views/__tests__/editorMetaKit.ts | 6 + web/src/views/__tests__/metaTestKit.ts | 17 + web/src/views/__tests__/seoTestKit.ts | 223 ++++++ web/visual/check.ts | 44 ++ web/visual/states.ts | 468 ++++++++++++ 62 files changed, 7471 insertions(+), 58 deletions(-) create mode 100644 features/editor_seo.feature create mode 100644 features/editor_seo.qa.md create mode 100644 web/src/components/ApiPreviewDialog.vue create mode 100644 web/src/components/FallbackChip.vue create mode 100644 web/src/components/SearchPreview.vue create mode 100644 web/src/components/SearchSharingSection.vue create mode 100644 web/src/components/SegmentedTabs.vue create mode 100644 web/src/components/SeoDisclosure.vue create mode 100644 web/src/components/SeoPanel.vue create mode 100644 web/src/components/SocialCardPreview.vue create mode 100644 web/src/components/SoftCounter.vue create mode 100644 web/src/components/SuggestionList.vue create mode 100644 web/src/composables/__tests__/useArticleDraftSeo.spec.ts create mode 100644 web/src/composables/useMediaNames.ts create mode 100644 web/src/composables/useResolvedSeo.ts create mode 100644 web/src/composables/useSeoCheck.ts create mode 100644 web/src/i18n/seoMessages.spec.ts create mode 100644 web/src/lib/__tests__/seoCopy.spec.ts create mode 100644 web/src/lib/__tests__/seoPreview.spec.ts create mode 100644 web/src/lib/articleSeo.ts create mode 100644 web/src/lib/booleanMemory.ts create mode 100644 web/src/lib/seoApiPreview.ts create mode 100644 web/src/lib/seoCopy.ts create mode 100644 web/src/lib/seoPreview.ts create mode 100644 web/src/lib/seoSectionMemory.ts create mode 100644 web/src/lib/seoTypes.ts create mode 100644 web/src/views/__tests__/EditorSeoCheck.spec.ts create mode 100644 web/src/views/__tests__/EditorSeoDialog.spec.ts create mode 100644 web/src/views/__tests__/EditorSeoFrench.spec.ts create mode 100644 web/src/views/__tests__/EditorSeoMemory.spec.ts create mode 100644 web/src/views/__tests__/EditorSeoPanel.spec.ts create mode 100644 web/src/views/__tests__/EditorSeoSave.spec.ts create mode 100644 web/src/views/__tests__/EditorSeoSection.spec.ts create mode 100644 web/src/views/__tests__/EditorSeoSession.spec.ts create mode 100644 web/src/views/__tests__/EditorSeoSlug.spec.ts create mode 100644 web/src/views/__tests__/EditorSeoSuggestions.spec.ts create mode 100644 web/src/views/__tests__/seoTestKit.ts diff --git a/features/editor_seo.feature b/features/editor_seo.feature new file mode 100644 index 0000000..823ae6b --- /dev/null +++ b/features/editor_seo.feature @@ -0,0 +1,532 @@ +Feature: Editor: search and sharing, suggestions and "What the frontend gets" + + As a RenewCMS user who can edit articles + I want to fill in an article's search and sharing data, see suggestions and previews, and see what the frontend receives + So that the API returns good SEO data for every article without blocking me from saving or publishing + + Background: + Given the screen is EditorView (/article/:id/edit) inside DashboardShell, per docs/design/routes.md + And the visual spec of record is docs/design/screens/editor.md (groups "Editor: search and sharing" with 24 states and "Editor: what the frontend gets" with 6 states), docs/design/seo-slices.md, docs/design/components.md (SearchSharingSection, SeoPanel, SoftCounter, SearchPreview, SocialCardPreview, SuggestionList, ApiPreviewDialog, SlugField, MediaField) and docs/design/states.md ("SEO round"). Exact copy is read from those files and is not repeated here unless a scenario needs it + And the board frames of docs/design/source/RenewCMS Screens Back-office.dc.html, AppFrame.dc.html and DirectoryFrame.dc.html are the layout tiebreaker + And this slice is SPA only and reuses the API of features/seo_data_backend.feature and features/seo_output_backend.feature without change + And the SPA calls, with relative URLs through useApi only: GET /v1/article/{id} and PATCH /v1/article/{id} (SEO fields slug, excerpt, seo_title, seo_description, cover_image_id, cover_image_alt, social_title, social_description, social_image_id, canonical_url, noindex), POST /v1/article/{id}/seo/check, GET /v1/article/{id}/seo, GET /v1/site/ and the media routes of the shared picker + And POST /v1/article/{id}/seo/check takes the optional SEO fields (and body) of PATCH as overrides and answers {status, suggestions: [{code, field, params}], slug: {available, suggestion}} with status "ready", "suggestions" or "hidden" + And the SPA never re-implements a suggestion rule. It renders the suggestions, the order and the status the check returns. The copy of each code lives in the SPA and has a French entry + And the suggestion codes are title_long, title_empty, desc_short, desc_long, cover_alt, cover_none, slug_long, no_au, no_h, body_alt and short + And a suggestion's field maps to a link label: seo_title "meta title", seo_description "meta description", cover_image_alt and cover_image_id "cover image", slug "URL slug", author_ids "authors", body "body" + And the soft ranges of the counters are meta title 30 to 60 and description 70 to 160 (docs/design). They colour a counter and never block + And suggestions never disable Save, "Save and publish" or Publish + And the sample article is 12, "How we price a bathroom renovation", online on the site Renew (renew.example, id 1), slug "how-we-price-a-bathroom-renovation", with no SEO field set, and the author "Inès Duarte" + And the sample cover media is "north-wall.jpg" (the site social image) and the sample panel values are those of the board frames "Panel: filled in" and "Panel: outside the soft ranges" + And every visible string goes through t(...) with the English text as the key and a French entry in web/src/i18n/messages.ts + And the screen uses Nuxt UI components and design tokens only, with no raw colors, and no comment is added anywhere + And a scenario tagged @not-in-design uses copy or behaviour that docs/design does not define: it takes the proposal of this feature, has a French entry, and is listed in the .qa.md to be fed back to the design + + Rule: The "Search and sharing" section sits under "Site and authors", collapsed by default + + Scenario: Position, in the sidebar and in the Details sheet + Given article 12 is loaded and nothing is remembered for this browser + When the user opens /article/12/edit on desktop + Then the details sidebar shows a collapsed "Search and sharing" section directly under "Site and authors" + And on mobile (390 px) the section is not visible until the Details sheet is opened, and inside the sheet it sits under "Site and authors" + + Scenario Outline: The one-line summary follows the check + Given the section is collapsed + And POST /v1/article/12/seo/check answers + Then the summary line reads "" in + + Examples: + | check | summary | colour | + | pending | Checking… | the muted colour | + | status "ready", no suggestions | Ready | the muted colour | + | status "suggestions", 1 suggestion | 1 suggestion | the warning colour | + | status "suggestions", 2 suggestions | 2 suggestions | the warning colour | + | status "hidden", 2 suggestions | Hidden from search | the muted colour | + + Scenario: The existing Editor states show the section + Then every editor state that has the details sidebar shows the collapsed section with "Ready" + And the Loading state shows the section header with a skeleton summary, as for "Site and authors" + And the Not found state shows no section + + Scenario: The choice is remembered per browser, with its own key + Given the section is collapsed by default + When the user opens it and later opens another article in the same browser + Then it is open there too + When the user collapses it and reloads + Then it is collapsed + And the choice is kept in localStorage under its own key, independent of the "Site and authors" key, and nothing is sent to the API + And the sidebar and the Details sheet share that key + + Scenario Outline: A storage that throws never breaks the editor + Given localStorage. throws a SecurityError + When the user opens the editor and toggles the section + Then the editor renders, the section opens and closes for this page view, and no error reaches the user + And reading falls back to "collapsed" and writing is silently skipped + + Examples: + | call | + | getItem | + | setItem | + + Scenario: The section opens by itself when asked from elsewhere + When the user activates "Edit search and sharing", "What the frontend gets" or a suggestion link + Then the panel or the dialog opens whatever the stored choice is, and the stored choice is not changed + + Rule: The open section: address, suggestions and actions + + Scenario: Open and ready + Given the section is open and the check answers status "ready" + Then it shows "Address" with "renew.example/how-we-price-a-bathroom-renovation" (the host of the site's base_url, without the scheme, then the slug) + And "All good" with "Nothing to suggest for this article." + And the actions "Edit search and sharing" and "What the frontend gets" + + Scenario: Open with suggestions, at most three + Given the check answers title_long (74 characters), cover_alt, no_h and short, in that order + Then exactly three suggestions are shown, in the order of the response, each with its message and a "Go to " link: "Go to meta title", "Go to cover image", "Go to body" + And a "+1 more in Search and sharing" action follows + When the user activates "+1 more in Search and sharing" + Then the panel opens with the suggestion list in view + And with three suggestions or fewer no "+N more" action is shown + + Scenario Outline: A suggestion link goes to its field + Given the check answers a suggestion whose field is "" + When the user activates its "Go to