feat: integration page (PART 3/3)

This commit is contained in:
fsylvain committed 2026-10-05 02:03:24 +02:00
1 parent 514a082c96
commit acd392c29d
20 files changed
+3115 -409

No files matched your search

+182 -52
View File
@@ -1,63 +1,193 @@
Feature: Integration screen
Feature: Integration screen (API reference)
As a verified RenewCMS owner
I want /integration to show my site's base API URL and ready-to-use endpoint examples
So that I, or the developer who builds my public site, can wire it up without reading the
source
As a developer building a frontend or an API-key client for a RenewCMS site
I want /integration to be a complete, searchable API reference
So that I can call every route a key may call without reading the source
Background:
Given /integration is a verified route (meta.auth "verified") rendered as IntegrationPage
inside DashboardShell, per docs/design/routes.md
And the screen makes no /v1 calls; api: none (static)
And the PageHeader shows title "Connect your website", description "A guide for the developer
who builds your public site.", breadcrumb "Integration" (mobile title "Integration"), and no
actions
And an introductory paragraph reads "RenewCMS is headless: it stores and publishes your
articles but doesn't show them to visitors. Your website fetches published articles from the
two addresses below and displays them in its own design."
And a "Base URL" row shows window.location.origin in a monospace box, with a "Copy" button
(icon copy) beside it
And two EndpointCard entries follow, in this order:
| method | path | description |
| GET | /v1/article/ | Lists every online article. |
| GET | /v1/article/{id} | Returns one online article. Drafts and unknown ids both return 404. |
And each EndpointCard's fetch example uses the current origin as the base of its URL:
| path | fetch example |
| /v1/article/ | const res = await fetch('{origin}/v1/article/')\nconst articles = await res.json() |
| /v1/article/{id} | const res = await fetch('{origin}/v1/article/12')\nif (res.status === 404) return notFound()\nconst article = await res.json() |
And each EndpointCard has fetch, curl and Response tabs, with fetch active by default, and its
CodeBlock is labelled "JavaScript" with its own ghost "Copy" button
And the curl tab's content is a plain curl command built from the current origin (e.g.
"curl {origin}/v1/article/" and "curl {origin}/v1/article/12"), and the Response tab's
content is a JSON sample built from the real domain Article fields; neither tab is active by
default, and neither is visually checked, since the fetch tab is the one shown in the board
And three info notes follow the endpoint cards, in this order:
- "Anonymous callers only ever get online articles. Drafts stay private until you publish
them."
- "Images in body use paths like /static/uploadedImages/<uuid>.png. Prefix them with the
base URL before rendering."
- "Browsers can only call the API from origins listed in the server's CORS setting. Ask
whoever runs the server to add your site's address."
And the PageHeader shows breadcrumb "Integration", title "API reference" and description
"Everything a developer needs to build on RenewCMS."
And the endpoint catalogue is a typed static data file at web/src/data/apiCatalogue.ts, and the view only renders it
And the catalogue holds 20 key-callable endpoints in 4 expandable groups, in this order:
| group | routes | endpoints |
| Articles | 7 | GET /v1/article/, GET /v1/article/{id}, POST /v1/article/, PATCH /v1/article/{id}, PATCH /v1/article/{id}/publish, PATCH /v1/article/{id}/unpublish, DELETE /v1/article/{id} |
| Media | 6 | GET /v1/media/, POST /v1/media/, PATCH /v1/media/{id}, DELETE /v1/media/{id}, POST /v1/media/merge, GET /v1/media/{id}/usage |
| Users and invites | 6 | GET /v1/user/, PATCH /v1/user/me, POST /v1/user/invite, DELETE /v1/user/invite/{id}, POST /v1/user/invite/{id}/renew, GET /v1/auth/me |
| Settings | 1 | GET /v1/settings |
And the "Session only" group has 13 non-expandable rows in 3 sub-groups:
| sub-group | rows |
| System · owner only | GET /v1/system/update, POST /v1/system/update/check, POST /v1/system/update/download, POST /v1/system/update/restart |
| Sign in and account | POST /v1/auth/login, POST /v1/auth/logout, POST /v1/auth/register, POST /v1/auth/verify, DELETE /v1/auth/register, POST /v1/auth/password-reset |
| API key management | GET /v1/api-key, POST /v1/api-key, DELETE /v1/api-key/{id} |
And a "Public auth flow" sub-list follows with 6 non-expandable rows and the chip "Public":
| method | path | description |
| GET | /v1/ | Hello world. |
| GET | /v1/auth/registration | Whether registration is open. |
| POST | /v1/auth/password-reset/confirm | Set a new password with the emailed code. |
| GET | /v1/auth/invite | Check an invite code. |
| POST | /v1/auth/invite/accept | Accept an invite. |
| GET | /healthz | Plain text "ok". Outside /v1. |
And every catalogue endpoint has a non-empty description, method, path, access chip, key chip,
query-parameter table (or "No query parameters."), request-body table (or "No request body."),
fetch, curl and Response examples, and error captions
Scenario: Default — the page loads with the app's own origin
Given the app is served from origin "{origin}"
Scenario: Default, every card collapsed
When the user navigates to /integration
Then the Base URL row shows exactly "{origin}"
And the Base URL "Copy" button reads "Copy", not "Copied"
And both EndpointCards' fetch examples use "{origin}" as the base of their URL
Then the intro reads "RenewCMS is headless: it stores your content and your own code displays it. There are two ways in. Public read-only access lets a website read online articles. API keys let a script or service manage articles, media and users without the back-office."
And a "Base URL" row shows window.location.origin in a monospace box with a "Copy" button (icon copy)
And every EndpointCard is collapsed as a 44 px row: method badge, mono path, one-line description, chips, chevron
And the sticky section index lists the sections on desktop and a "Jump to" select lists them on mobile
Scenario: Copied — the Base URL copy button
Given the user is on /integration, served from origin "{origin}"
When the user activates the Base URL row's "Copy" button
Scenario Outline: Method badge and chips per endpoint
Then <method> <path> shows the <method> badge, access chip "<access>" and key chip "API key OK"
Examples:
| method | path | access |
| GET | /v1/article/ | Public |
| GET | /v1/article/{id} | Public |
| POST | /v1/article/ | Verified user |
| DELETE | /v1/article/{id} | Verified user |
| GET | /v1/media/ | Verified user |
| PATCH | /v1/user/me | Verified user |
| GET | /v1/auth/me | Verified user |
| POST | /v1/user/invite | Owner only |
| DELETE | /v1/user/invite/{id} | Owner only |
| POST | /v1/user/invite/{id}/renew | Owner only |
| GET | /v1/settings | Verified user |
Scenario: Owner-only invite cards, user-approved divergence from the board
Then the three invite cards show "Owner only", where the board shows "Verified user"
And each lists a 403 error "Only the owner can do this."
And each says it is rate limited and that too many requests answer 429, with no number
Scenario: Group scrolled, one card expanded
When the user activates the "POST /v1/article/" row
Then the row's button reports aria-expanded "true"
And a "Query parameters" table lists "format", string, optional, "markdown, html or json. Default markdown."
And a "Request body" table lists:
| Name | Type | Required | Rule |
| title | string | Yes | 3 to 200 characters. Can't be changed later. |
| body | string | Yes | Up to 10 000 characters. |
And tabs fetch, curl and Response show, fetch active, with a CodeBlock labelled "JavaScript" and a ghost "Copy" button
And an "Errors" list shows a status chip with a one-line caption for 400, 401
Scenario: Query parameters table, new UI beyond the board, user-approved
Then the "Query parameters" table sits above the "Request body" table in each card
And it lists:
| route | name | type | required | rule |
| every /v1/article route | format | string | no | markdown, html or json. Default markdown. |
| GET /v1/media/ | q | string | no | Search text. |
| GET /v1/media/ | hash | string | no | SHA-256 of the content, lowercased. |
| GET /v1/media/ | cursor | string | no | The next_cursor of the previous page. |
| GET /v1/media/ | filter | string | no | all, unused or duplicates. Default all. |
| GET /v1/media/ | sort | string | no | newest, oldest, name or largest. Default newest. |
| GET /v1/media/ | limit | number | no | 1 to 100. Default 24. |
And a card without query parameters shows "No query parameters."
Scenario Outline: Request body fields come from the handler validate tags
Then <method> <path> lists these fields: <fields>
Examples:
| method | path | fields |
| POST | /v1/article/ | title string required 3-200; body string required max 10000 |
| PATCH | /v1/article/{id} | body string required max 10000 |
| PATCH | /v1/media/{id} | filename string required |
| POST | /v1/media/merge | keep_id number required >0; merge_ids number[] required min 1 each >0 |
| POST | /v1/media/ | multipart/form-data, 1 file field named "file" |
| PATCH | /v1/user/me | display_name string required max 60 |
| POST | /v1/user/invite | email string required valid email |
| GET | /v1/article/ | "No request body." |
Scenario: Response samples use the real domain JSON field names
Then the GET /v1/article/{id} Response sample has exactly the keys id, title, body, body_format, is_online, created_at, updated_at
And the sample has no "images" key and no "status" key
And POST /v1/media/merge has the keys articles_updated and merged
And GET /v1/media/ has the keys items, next_cursor and counts
And GET /v1/media/{id}/usage items have the keys id, title, published
And PATCH /v1/article/{id}/publish and /unpublish samples are articles with is_online true and false
Scenario: Article body format
Then a note reads "Article routes accept `?format=markdown`, `?format=html` or `?format=json` to choose how `body` is returned. With `json`, `body` is the stored JSON structure. Articles have `body_format: markdown` by default."
And every article card's Errors list includes 400 "The format must be one of: markdown, html, json."
Scenario: Authentication section
Then card "Public" reads "No key needed. Two read-only routes, and only online articles are returned." and names GET /v1/article/ and GET /v1/article/{id}
And card "API key" reads "Send `Authorization: Bearer rcms_…`. Requests act as the user who created the key, with that user's role (owner or admin). A key never has more rights than its creator."
And its CodeBlock has tabs curl and fetch for GET /v1/auth/me, using the current origin
And it says "Keys start with `rcms_` and are shown in full once, when you create them."
And a "Manage API keys" link navigates to /integration/api-keys
And Note 1 reads "A key can't create, list or revoke API keys, verify or cancel a registration, or call anything under `/v1/system`. Those routes answer 403 "This action needs a real session." An invalid, unknown, expired or revoked key answers 401, even on public routes, and so does a key whose user was deleted or unverified."
And Note 2 reads "Each key can make 120 requests per minute. Beyond that the API answers 429 with a `Retry-After` header."
Scenario: Session only group
Then the banner reads "These routes are made for a signed-in browser session. With an API key, verify, DELETE /auth/register, /api-key/* and /system/* answer 403 "This action needs a real session.""
And each row has a dashed border and muted fill, uses no method colour and cannot be expanded
And rows that answer 403 to a key show the lock icon and the chip "Session only"
And Log in, Log out, Register and Reset password show the caption "Made for the browser. A key is ignored here, with no 403." and no 403 claim
And no /v1/system route appears in any expandable group
And the "Public auth flow" rows carry the chip "Public" and no lock icon
Scenario: Errors section
Then the intro reads "Every error is a plain-text body with a single sentence. The status code is in the HTTP status line, and the Content-Type is text/plain."
And the table lists:
| status | message |
| 400 | The request cannot be processed due to a mismatch in the format of the body. / The expiry must be a date in the future. |
| 401 | You must be logged in to access this resource. |
| 403 | This action needs a real session. / Only the owner can do this. / You must verify your account to access this resource. |
| 404 | Article not found / Image not found / Invite not found / API key not found |
| 409 | There's already a pending invite for this address. / This invite has already been accepted. |
| 429 | Too many requests. Please try again later. |
| 500 | Internal server error |
And a sample shows "HTTP/1.1 403 Forbidden", "Content-Type: text/plain", a blank line, and "This action needs a real session."
And the table does not use the SPA copy of docs/design/states.md
Scenario Outline: Per-endpoint error captions use the real strings
Then <method> <path> lists <status> "<caption>"
Examples:
| method | path | status | caption |
| POST | /v1/media/ | 400 | the image could not be read |
| POST | /v1/media/ | 413 | the file exceeds the maximum upload size |
| POST | /v1/media/ | 415 | the file must be a PNG, JPEG, WEBP, or SVG image |
| GET | /v1/media/ | 400 | The media query is invalid. |
| POST | /v1/media/merge | 400 | Choose an image to keep and at least one other image to merge into it. |
| POST | /v1/media/merge | 409 | Some of the images to merge changed. Refresh and try again. |
| PATCH | /v1/media/{id} | 400 | Enter a filename. |
| POST | /v1/article/ | 400 | Article title must be between 3 and 200 characters. |
| GET | /v1/article/{id} | 400 | The server expects the ID to be in the format of an unsigned 32-bit integer (uint32). |
| PATCH | /v1/user/me | 400 | Display name must be 60 characters or fewer. |
| POST | /v1/user/invite | 409 | This address already has an account. |
| POST | /v1/user/invite | 409 | There's already a pending invite for this address. |
| POST | /v1/user/invite/{id}/renew | 409 | This invite has already been accepted. |
| POST | /v1/user/invite | 403 | Only the owner can do this. |
Scenario: Notes
Then the image-prefix note reads "Images in body use paths like /static/uploadedImages/<uuid>.png. Prefix them with the base URL before rendering."
And the CORS note reads "Browsers can only call the API from origins listed in the server's CORS setting. Ask whoever runs the server to add your site's address."
Scenario: Copied, Base URL
Given the app is served from origin "{origin}"
When the user activates the Base URL "Copy" button
Then "{origin}" is written to the clipboard
And the button's icon changes to check and its text changes to "Copied"
And the change is announced to assistive technology
When 2 seconds elapse without a further click
Then the button reverts to icon copy and text "Copy"
And the button shows icon check and "Copied", announced to assistive technology
When 2 seconds elapse
Then the button reverts to icon copy and "Copy"
Scenario: Each EndpointCard's own Copy button is independent of the Base URL's
Given the user is on /integration
When the user activates the "/v1/article/" EndpointCard's "JavaScript" code block's "Copy"
button
Then that card's own code is written to the clipboard, and only that button reads "Copied"
And the Base URL row's "Copy" button and the other EndpointCard's "Copy" button are unaffected
Scenario: Copied, CodeBlock is independent of Base URL
Given a card is expanded
When the user activates that CodeBlock's "Copy JavaScript example" button
Then that card's exact code string is written to the clipboard and only that button reads "Copied"
And the Base URL button and other CodeBlocks are unaffected
Scenario: Examples use the current origin and the API key header
Then every fetch and curl example starts from "{origin}" and carries no ?format= query
And examples for non-public routes carry "Authorization: Bearer rcms_…"
And examples for public routes carry no Authorization header
Scenario: Navigation
When the user activates a section in the index (desktop)
Then the page scrolls to that anchor and the item reports aria-current "true"
And on mobile the "Jump to" select opens and lists the same sections
Scenario: The page stays static
Then no /v1 request is made while the page renders or while any card is expanded