feat: site languages, setup and site URL settings

This commit is contained in:
Florian Sylvain committed 2026-11-01 22:13:02 +01:00
1 parent 0806733d5c
commit d9158025d4
106 files changed
+10184 -56

No files matched your search

+121
View File
@@ -0,0 +1,121 @@
Feature: Public URL pattern of a site
An owner can give each site one pattern for the address of its articles on
the website, with {lang} and {slug}. It is validated on the server. When it
is unset the product shows "Public URL not configured" and invents no URL.
Background:
Given error responses are plain text whose body is the snake_case code
And an "owner" is a verified user holding settings.manage_sites and a "reader" is a verified user without it
And the pattern is stored on the site as public_url_pattern, a string or null, at most 512 characters
And it is read through the existing GET /v1/site/ list (every verified user) and written through the existing PATCH /v1/site/{id} (owner)
And no new route is added
And the design in docs/design/localization.md (L1b) is the visual spec of record
Scenario: Reading the pattern
Given site 1 has public_url_pattern "https://journal.example.com/{lang}/{slug}"
When a reader sends GET /v1/site/
Then the item for site 1 contains "public_url_pattern" with that value
And every key present before this change is present with an unchanged value and type
Scenario Outline: Writing the pattern
When an owner sends PATCH /v1/site/1 with {"public_url_pattern": <value>}
Then the status is <status> and <effect>
Examples:
| value | status | effect |
| "https://journal.example.com/{lang}/{slug}" | 200 | the site returns that value |
| "https://journal.example.com/{lang}/articles/{slug}" | 200 | the site returns that value |
| "http://localhost:3000/{slug}" | 200 | the site returns that value |
| "https://journal.example.com/{slug}/{slug}" | 200 | the site returns that value (repeats allowed) |
| null | 200 | the pattern is unset |
| "" | 200 | the pattern is unset |
| " https://journal.example.com/{slug} " | 200 | the stored value is trimmed |
Scenario Outline: Patterns that are refused
When an owner sends PATCH /v1/site/1 with {"public_url_pattern": "<value>"}
Then the status is 422 and the body is "<code>"
And the stored pattern is unchanged
Examples:
| value | code |
| ftp://journal.example.com/{lang}/{slug} | invalid_scheme |
| journal.example.com/{slug} | invalid_scheme |
| https:///{slug} | invalid_host |
| https://journal.example.com/{language}/{slug} | unknown_placeholder |
| https://journal.example.com/{lang | unknown_placeholder |
| https://journal.example.com/my pages/{lang}/{slug} | not_encoded |
| https://journal.example.com/é/{slug} | not_encoded |
| https://journal.example.com/{lang} | missing_slug |
| a pattern of 513 characters | invalid_pattern |
Scenario: A pattern is checked in a fixed order
When the pattern is "ftp://x.example.com/{language} here"
Then the code is "invalid_scheme"
And with scheme fixed the code is "unknown_placeholder", then "not_encoded", then "missing_slug"
Scenario Outline: Only an owner can write
When <caller> sends PATCH /v1/site/1 with a valid pattern
Then the status is <status> and the stored pattern is unchanged
Examples:
| caller | status |
| an anonymous client | 401 |
| a reader | 403 |
Scenario: Saving the pattern changes nothing else
When an owner sets the pattern
Then no article, canonical URL or public response changes
And the site's other fields are unchanged
Scenario: Existing site behaviour is unchanged
Then the existing site tests pass without edits and a PATCH without public_url_pattern leaves it unchanged
Scenario: Route and access
Given the route /settings/sites/:key/public-url
Then it is open to every verified signed-in user
And owners can edit, other users see the Read-only view
And owners reach it from the "Public URL" action on the Settings, Sites row, non-owners by URL only
Scenario Outline: Public URL states
When <trigger>
Then the screen is "<state>" (<id>) and shows <visible>
Examples:
| trigger | state | id | visible |
| an owner opens Journal with the pattern saved | Set | loc.l1b.set | heading "Public URL", subtitle "Where Journal articles appear on your website. Used for canonical and alternate links.", the monospace field "Public URL pattern" holding https://journal.example.com/{lang}/{slug}, the help "Placeholders: {lang} is the language code, such as fr or pt-BR. {slug} is the article address in that language.", the preview line and "Save" and "Cancel" |
| the owner types https://journal.example.com/{lang}/articles/{slug} | Editing | loc.l1b.editing | the preview follows the field on each change |
| the owner types a pattern with {language} | Unknown placeholder | loc.l1b.unknown | the error "Unknown placeholder {language}. Use {lang} and {slug}." and Save disabled |
| the owner types ftp://journal.example.com/{lang}/{slug} | Bad scheme | loc.l1b.scheme | the error "Start with https:// or http://." and Save disabled |
| the owner types a pattern with a space in the path | Not encoded | loc.l1b.encoded | the error "Remove spaces and characters that need encoding. Use letters, digits, - and _." and Save disabled |
| the owner types https://journal.example.com/{lang} | Missing {slug} | loc.l1b.noSlug | the error "Add {slug} so each article has its own address." and Save disabled |
| the owner saves and the PATCH returns 200 | Saved | loc.l1b.saved | a success toast; the field holds the saved value |
| an owner opens a site with no pattern from "Public URL not configured" | Unset | loc.l1b.unset | the info alert "Public URL not configured" / "Page, canonical and alternate addresses stay hidden until you add a pattern. Links by article ID still work.", the placeholder https://example.com/{lang}/{slug}, no preview and Save disabled |
| a reader opens Journal | Read-only view | loc.l1b.viewOnly | the alert "Only owners can change this" / "You can read the pattern. Ask an owner to change it.", the field read-only and no Save or Cancel |
Scenario: Client and server agree
Given the page validates as the owner types
When the server still returns 422 with unknown_placeholder, invalid_scheme, not_encoded or missing_slug
Then the field shows the same error as the matching state above
Scenario: Preview
Given the preview uses a real article of the site, shown with the site's default language
Then {lang} is replaced by the tag and {slug} by the article slug
Scenario: Preview of a site without articles
Given the site has no articles
Then the preview line is not shown
And this state has no frame, so the assumption is recorded in the QA notes
Scenario: Cancel
When the owner edits the field and presses "Cancel"
Then the field returns to the saved value and nothing is sent
And the design does not describe Cancel, so the assumption is recorded in the QA notes
Scenario: Clearing the field
When the owner empties the field and saves
Then a PATCH with null is sent and the site shows the Unset state
Scenario: Failures with no frame
When the load or the save fails with a 5xx
Then the page reuses the Languages "Couldn't load languages" / "Couldn't save languages" alert copy
And these states are skipped by the visual gate