refac: article reads, story delete, no leaks, stored SEO status, rate limit, cleanup (%site%, article repo split, old lang repos, one default per site rule to domain)
This commit is contained in:
1 parent
9d284e65a9
commit
7c15206170
85 files changed
+1788
-593
No files matched your search
@@ -32,7 +32,7 @@ The headers are set on those responses only. Same-origin SPA code reads them dir
|
||||
| 400 | `Article title must be between 3 and 200 characters.` | a `PATCH` title outside 3 to 200 runes |
|
||||
| 400 | `invalid_slug` | a `PATCH` slug that is not a slug |
|
||||
| 401 | `You must be logged in to access this resource.` | no valid session or key |
|
||||
| 403 | `missing_permission` | the caller lacks the permission (`X-Missing-Permission`) |
|
||||
| 403 | `missing_permission` | the caller lacks the permission (`X-Missing-Permission`); on `DELETE /v1/story/{story_id}`, a `content.delete` that is not at scope `all` |
|
||||
| 404 | `Article not found` | the version, or the story, does not exist or the caller cannot read it |
|
||||
| 404 | `source_unavailable` | the chosen source does not exist or is not readable; the same answer for both |
|
||||
| 404 | `site_not_found` | `POST /v1/story/{story_id}/move`, or `POST /v1/article/` with `site`, naming an unknown site key |
|
||||
@@ -43,7 +43,7 @@ The headers are set on those responses only. Same-origin SPA code reads them dir
|
||||
| 409 | `slug_taken` | a `PATCH` slug used by another article of the same site and language (`X-Slug-Suggestion`) |
|
||||
| 409 | `slug_conflict` | a move would put an address that already exists at the destination (`X-Conflicting-Articles`) |
|
||||
| 409 | `story_move_required` | `PATCH site_id` on a version whose story holds several versions |
|
||||
| 409 | `target_site_deleted` | `POST /v1/article/`: the target site was deleted after it was looked up and before the article was stored; choose another site |
|
||||
| 409 | `target_site_deleted` | `POST /v1/article/`: the target site was deleted after it was looked up and before the article was stored; nothing is stored, choose another site and retry. The SPA has no copy for it yet |
|
||||
| 409 | `story_busy` | the database could not lock the rows in time (lock timeout, deadlock or serialization failure); retry |
|
||||
| 422 | `invalid_language` | the tag is not a language tag |
|
||||
| 422 | `unsupported_language` | a valid tag outside the supported list |
|
||||
@@ -63,11 +63,15 @@ Added to the article object of `GET /v1/article/{id}`, `POST`, `PATCH`, `publish
|
||||
"revision": 3,
|
||||
"capabilities": ["read", "edit", "publish", "delete", "create_translation"],
|
||||
"translations": [
|
||||
{"id": 10, "lang": "en", "title": "Renovation costs", "is_online": true, "language_enabled": true, "capabilities": ["read", "edit", "publish", "delete", "create_translation"]},
|
||||
{"id": 13, "lang": "ar", "title": "Takalif", "is_online": true, "language_enabled": true, "capabilities": ["read", "create_translation"]},
|
||||
{"id": 12, "lang": "fr", "title": "Couts", "is_online": false, "language_enabled": false, "capabilities": ["read", "edit", "publish", "delete", "create_translation"]}
|
||||
{"id": 10, "lang": "en", "title": "Renovation costs", "is_online": true, "language_enabled": true, "excluded_from_public": false, "source_article_id": null, "capabilities": ["read", "edit", "publish", "delete", "create_translation"]},
|
||||
{"id": 13, "lang": "ar", "title": "Takalif", "is_online": true, "language_enabled": true, "excluded_from_public": false, "source_article_id": 10, "capabilities": ["read", "create_translation"]},
|
||||
{"id": 12, "lang": "fr", "title": "Couts", "is_online": false, "language_enabled": false, "excluded_from_public": true, "source_article_id": 10, "capabilities": ["read", "edit", "publish", "delete", "create_translation"]}
|
||||
],
|
||||
"source": {"article_id": 10, "revision": 4}
|
||||
"source": {"article_id": 10, "revision": 4},
|
||||
"source_unavailable": false,
|
||||
"missing_langs": ["de"],
|
||||
"excluded_from_public": false,
|
||||
"partial_view": false
|
||||
}
|
||||
```
|
||||
|
||||
@@ -75,6 +79,28 @@ Added to the article object of `GET /v1/article/{id}`, `POST`, `PATCH`, `publish
|
||||
- `capabilities` lists what the caller may do with this version, always in this order and always starting with `read`: `edit` (`content.update` covers it), `publish` (`content.publish`), `delete` (`content.delete`), `create_translation` (the caller holds `content.create`). An anonymous caller gets `["read"]`.
|
||||
- `translations` lists the readable versions of the story, the version being read included, with `id`, `lang`, `title`, `is_online`, `language_enabled` and their own `capabilities`. A version without language (`lang` `""`) reports `language_enabled: true`. The order is the default language of the site first, then the other tags in ascending order (`en`, `ar`, `de`, `fr`); a disabled language is still listed with `language_enabled: false`. A single-version article lists itself. An anonymous caller gets `[]`.
|
||||
- `source` is `{"article_id", "revision"}` when the version was created by copying another version and that source still exists and is readable by the caller, otherwise `null` (a version created empty, a deleted source, an unreadable source, an anonymous caller). `source.revision` is the content revision of the source at the moment of the copy.
|
||||
- `translations[].excluded_from_public` and `excluded_from_public` (the version being read) are `true` when the language keeps the version off the public language surfaces even once published: the language is disabled on the site, or the version has no language yet (`lang` `""`, the site default is not confirmed). The SPA reads this flag instead of re-deriving it from `language_enabled` and the site languages. A `false` flag says nothing about publication: drafts are off the public surfaces anyway.
|
||||
- `translations[].source_article_id` is the id of the version this entry tracks when that source is itself listed in `translations` (so readable), otherwise `null`.
|
||||
- The four keys below are sent to signed-in callers only; an anonymous read has none of them (its shape is unchanged).
|
||||
- `source_unavailable`: see [Versions the caller cannot read](#versions-the-caller-cannot-read).
|
||||
- `missing_langs`: the enabled languages of the site without a readable version in the story, default first then by tag, the same rule as the editorial list (`[]` when the caller cannot create, and `[]` for a story whose only versions have no language).
|
||||
- `excluded_from_public`: the flag above, for the version being read.
|
||||
- `partial_view`: see [Versions the caller cannot read](#versions-the-caller-cannot-read).
|
||||
|
||||
## Versions the caller cannot read
|
||||
|
||||
No answer reveals the existence, id, title or language of a version the caller cannot read. Where the editor needs to explain an absence, the API gives one neutral answer that is the same whether the version was deleted or is hidden from the caller, or a flag that depends on the caller's own permissions only.
|
||||
|
||||
| Need | Answer | Same for |
|
||||
| --- | --- | --- |
|
||||
| The tracked source of a version is gone | `"source": null` and `"source_unavailable": true`. The editor shows "The source isn't available to you" and asks to choose a new source | a deleted source and a source the caller cannot read |
|
||||
| Creating from a source that is gone | `404 source_unavailable` | a deleted, unknown or unreadable source |
|
||||
| Deleting a version that others track ("Review tracking will pause") | the dialog names the entries of `translations` whose `source_article_id` is the id being deleted | trackers the caller cannot read are never listed or counted, because `translations` lists readable versions only and their `source_article_id` is never sent |
|
||||
| Telling the caller that some versions may be out of view ("Versions you can't read aren't listed or counted.") | `partial_view` | `true` whenever the caller's `content.read` is not at scope `all`, whether or not the story holds a version the caller cannot read |
|
||||
| Deleting the whole article while some versions are out of view | `DELETE /v1/story/{story_id}` needs `content.delete` at scope `all` (`403 missing_permission` otherwise, decided before anything is read), deletes every version, readable or not, and answers `204` | a story with or without hidden versions |
|
||||
| Moving or editing a story with hidden versions | `article_ids` and the `X-Missing-Languages` and `X-Conflicting-Articles` headers name readable versions only (see [Moving a story](#moving-a-story-post-v1storystory_idmove)) | |
|
||||
|
||||
`partial_view` never depends on the stored data, so the note "Versions you can't read aren't listed or counted." shows for such a caller on every story, also when nothing is hidden. Showing it only when a hidden version exists would reveal that version.
|
||||
|
||||
## What raises revision
|
||||
|
||||
@@ -129,7 +155,9 @@ The new version joins the story of `{id}`, in the same site, as an offline draft
|
||||
|
||||
Answers `201` with the created article object. Order of checks: permission, path id, body, version readable, `copy` rules, language tag, `setup_required`, source, language configuration. A story deleted in the meantime answers `404 Article not found`. Both entry points leave the stored `lang` normalized.
|
||||
|
||||
### `POST /v1/article/` with `story_id`
|
||||
### `POST /v1/article/` with `story_id` (deprecated)
|
||||
|
||||
Deprecated: use `POST /v1/article/{id}/translations`. Both entry points run the same use case and keep working; new clients should not send `story_id`.
|
||||
|
||||
```json
|
||||
{"title": "Couts", "lang": "fr", "story_id": 5, "copy": true, "source_article_id": 10}
|
||||
@@ -145,7 +173,7 @@ It creates a new story with one offline version, on the default site unless the
|
||||
{"title": "Neuf", "lang": "fr", "site": "atelier"}
|
||||
```
|
||||
|
||||
- `site` is optional: the site key. Absent or `null`, the default site is used exactly as before. A present `site` is validated like any site key (`400` with the plain-text body `invalid_site_key` for `""` or `Bad Key!`), then looked up (`404 site_not_found` for an unknown key, a plain text body, nothing stored). A site deleted after that lookup and before the article is stored answers `409 target_site_deleted` (plain text, nothing stored). Every caller who may create articles may create on every site: `content.create` is not scoped to a site.
|
||||
- `site` is optional: the site key. Absent or `null`, the default site is used exactly as before. A present `site` is validated like any site key (`400` with the plain-text body `invalid_site_key` for `""` or `Bad Key!`), then looked up (`404 site_not_found` for an unknown key, a plain text body, nothing stored). A site deleted after that lookup and before the article is stored answers `409 target_site_deleted` (plain text, nothing stored). Every caller who may create articles may create on every site: `content.create` is not scoped to a site, because a role grants each permission at a scope of `none`, `own` or `all` and has no per-site scope (see [Per-site permissions](#per-site-permissions)).
|
||||
- `lang` is optional. Absent, the version gets the starting language of the chosen site; present, it is normalized and must be an enabled language of the chosen site (`422 invalid_language`, `unsupported_language`, `language_not_enabled`; `409 language_unavailable` when disabled on that site; `409 setup_required` before that site's default language is confirmed). The same tag can therefore be accepted on one site and refused on another.
|
||||
- The slug is generated from the title and made free within the chosen site and language only.
|
||||
- `site` with `story_id`, `source_article_id` or `copy` answers `400` with the body mismatch message and stores nothing. `POST /v1/article/{id}/translations` has no `site` field: the translation keeps the site of its source and an unknown `site` key in its body is ignored.
|
||||
@@ -156,6 +184,20 @@ It creates a new story with one offline version, on the default site unless the
|
||||
|
||||
`DELETE /v1/article/{id}` removes one version (`204`, empty body) and checks `content.delete` per version (`403 missing_permission`). Other versions and their story remain. Deleting the last version of a story removes the story. Nothing in the answer mentions a sibling. A deleted language can be created again in the same story; a language with a deleted version still counts as in use for removing the language from the site.
|
||||
|
||||
### Deleting the whole article: `DELETE /v1/story/{story_id}`
|
||||
|
||||
Verified session or API key with `content.delete` at scope `all` ("delete permission on every version"). Order of checks: path id (`400` with the uint32 message), `content.delete` held (`403 missing_permission`, `X-Missing-Permission: content.delete`), `content.delete` at scope `all` (`403 missing_permission`, same header; decided from the caller's role only, before any read), at least one readable version (`404 Article not found`, the same answer for an unknown story). Then every version of the story, readable or not, is deleted in one transaction under the locks of its sites, and the story row is removed: `204`, empty body.
|
||||
|
||||
- Atomic: a failure answers `500 Failed to delete article` and removes nothing; every version stays.
|
||||
- Each version is deleted like `DELETE /v1/article/{id}` does it (slug tombstone, soft delete, credits removed), so its slug is free again and its language still counts as in use for removing the language from the site.
|
||||
- Versions that tracked a deleted version read `source_unavailable: true` afterwards.
|
||||
- Activity: one `article.delete` per deleted version, oldest first, with `params.lang` and `params.story_id`. A failure records one failed `article.delete` on the oldest readable version; `403` and `404` record nothing.
|
||||
- A caller whose `content.delete` is at scope `own` deletes versions one by one; deleting the last one removes the story.
|
||||
|
||||
### Per-site permissions
|
||||
|
||||
Roles grant each permission at scope `none`, `own` or `all` and have no site dimension, so `content.create`, `content.delete` and the other content permissions apply to every site. A per-site permission (for example "may create on Journal only") is not supported; it needs a site scope in the role model first.
|
||||
|
||||
## Moving a story: `POST /v1/story/{story_id}/move`
|
||||
|
||||
```json
|
||||
@@ -186,7 +228,7 @@ An opt-in shape of the list for a signed-in, verified caller whose role resolves
|
||||
|
||||
Only versions the caller can read count: they are titled, credited, listed in `coverage`, searched and counted. A hidden version never appears, and a story whose versions are all hidden has no row.
|
||||
|
||||
**Rows.** With no `lang` a row is a story: `total` counts stories. With `lang` a row is the version of that language (`tab=all`), and the whole story's `coverage`, `story_authors` and `missing_langs` still ride on the row. With `tab=missing` rows are stories again: `lang=fr` lists the stories on a site that enables `fr` and that have a readable version but none in `fr`; without `lang` it lists the stories that lack at least one enabled language, and a caller who cannot create (no `content.create`) gets `total` 0. Articles without language (`lang` `""`) are one story each and are never listed as missing a language. A version of the story must satisfy `status`, `author`, `q` and `seo` together to make the story match; `q` ignores case and accents.
|
||||
**Rows.** With no `lang` a row is a story: `total` counts stories. With `lang` a row is the version of that language (`tab=all`), and the whole story's `coverage`, `story_authors` and `missing_langs` still ride on the row. With `tab=missing` rows are stories again: `lang=fr` lists the stories on a site that enables `fr` and that have a readable version with a language but none in `fr`; without `lang` it lists the stories that lack at least one enabled language, and a caller who cannot create (no `content.create`) gets `total` 0. A story whose readable versions all lack a language (`lang` `""`) is never listed as missing a language. A mixed story (a version without language plus at least one version with a language) is listed under Missing like any other: the version without language fills no language. A version of the story must satisfy `status`, `author`, `q` and `seo` together to make the story match; `q` ignores case and accents.
|
||||
|
||||
**Item.** The article list item of the representative version (the one in `lang`, otherwise the site default language version, otherwise the first readable one by language tag), plus:
|
||||
|
||||
@@ -194,14 +236,17 @@ Only versions the caller can read count: they are titled, credited, listed in `c
|
||||
| --- | --- | --- |
|
||||
| `story_updated_at` | RFC 3339 | The latest `updated_at` among the readable versions of the story |
|
||||
| `story_authors` | array of bylines | The union of the bylines of the readable versions, without duplicates |
|
||||
| `coverage` | array | One entry per readable version, in the order of `translations` (default language, other enabled tags, disabled tags): `{ id, lang, title, is_online, language_enabled, capabilities }` |
|
||||
| `missing_langs` | array of tags | Enabled languages without a readable version, default first then by tag; `[]` when the caller cannot create |
|
||||
| `coverage` | array | One entry per readable version, in the order of `translations` (default language, other enabled tags, disabled tags), with the same keys as a `translations` entry: `{ id, lang, title, is_online, language_enabled, excluded_from_public, source_article_id, capabilities }` |
|
||||
| `missing_langs` | array of tags | Enabled languages without a readable version, default first then by tag; `[]` when the caller cannot create or when no readable version has a language |
|
||||
| `excluded_from_public` | boolean | The flag of the representative version (see [Fields added to an article read](#fields-added-to-an-article-read)) |
|
||||
| `capabilities` | array | The capabilities of the representative version |
|
||||
|
||||
**Sort.** The whitelist is the flat one. `updated_at` uses the latest readable version of the story, `created_at` the earliest, `title` and `status` the representative; with `lang` the version itself. Ties end with `story_id` descending.
|
||||
|
||||
**Counts.** `{ all, online, draft, suggestions, missing }`. They follow `lang`, `q`, `author`, `seo` and `site` but never `status` or `tab`, and count the unit of the mode (stories, or versions of the selected language). `online` and `draft` count the units with a readable online or draft version. `missing` is the `total` the not-in view would give under the same filters (0 without `lang` for a caller who cannot create). `suggestions` is the number of stories with a readable version whose `seo_status` is `suggestions`, ignoring every parameter.
|
||||
|
||||
`seo_status`, `seo_suggestions`, the `seo` filter and `counts.suggestions` read the stored SEO status of each version (see [SEO status and suggestions](articles.md#seo-status-and-suggestions)); no request reads the bodies of the visible versions to compute them.
|
||||
|
||||
A repository failure at any step, counts included, answers 500 `Failed to retrieve articles`.
|
||||
|
||||
## Mock-ready examples
|
||||
@@ -213,6 +258,9 @@ Statuses of the new and changed calls. Bodies are plain text unless shown as JSO
|
||||
| `GET /v1/article/10` (see above) | 200 | | article object with `revision`, `capabilities`, `translations`, `source` |
|
||||
| `POST /v1/article/10/translations` `{"lang":"fr","title":"Couts","copy":true}` | 201 | | article object, `lang` `"fr"`, `"is_online": false`, `"revision": 1`, `"source": {"article_id": 10, "revision": 4}` |
|
||||
| `DELETE /v1/article/12` | 204 | | empty |
|
||||
| `DELETE /v1/story/5` by the owner | 204 | | empty |
|
||||
| `DELETE /v1/story/5` by a user holding `content.delete` at `own` | 403 | `X-Missing-Permission: content.delete` | `missing_permission` |
|
||||
| `DELETE /v1/story/5` when the database fails | 500 | | `Failed to delete article` |
|
||||
| `POST /v1/story/5/move` `{"site":"atelier"}` | 200 | | `{"story_id":5,"site":"atelier","site_id":2,"article_ids":[10,12,11,13]}` |
|
||||
| `POST /v1/article/10/translations` `{"lang":"fr","title":"Couts"}` | 400 | | `The request cannot be processed due to a mismatch in the format of the body.` |
|
||||
| `PATCH /v1/article/10` `{"title":"ab"}` | 400 | | `Article title must be between 3 and 200 characters.` |
|
||||
@@ -243,6 +291,8 @@ No new action code. The existing events carry `params.lang` (the language of the
|
||||
|
||||
Contract for `features/localization_public_api_and_surfaces.feature`. Both routes are open to everyone: a `jwt` cookie or an API key is ignored, so a draft is never returned, whoever asks. They sit under `/v1/public` and are read only.
|
||||
|
||||
They are rate limited per client address together with `GET /v1/site/{key}/sitemap`: 120 requests per minute in total, the same budget an API key gets on the authenticated routes. The client address is the connection address, or `X-Real-IP` when the connection comes from one of `TRUSTED_PROXIES`. Over the budget the answer is `429` with `Retry-After` (seconds) and the body `Too many requests. Please try again later.`
|
||||
|
||||
A version is **published** when `is_online` is true, and **eligible** when it is published and its language is enabled on the site. A disabled language stays readable on both routes; only the alternates leave it out.
|
||||
|
||||
### Exact lookup: `GET /v1/public/site/{key}/lang/{lang}/article/{slug}`
|
||||
@@ -308,6 +358,7 @@ Plain text, the body is the code followed by a line feed. `language_disabled` is
|
||||
| 404 | `article_not_found` | exact lookup: no published version for the site, language and slug |
|
||||
| 404 | `language_not_available` | story resolution: no version can be served under the rules above |
|
||||
| 404 | `site_not_found` | unknown site key |
|
||||
| 429 | `Too many requests. Please try again later.` | more than 120 requests in a minute from one client address on the public routes and the sitemap together (`Retry-After` in seconds) |
|
||||
| 500 | `Internal server error` | a repository failure |
|
||||
|
||||
## Resolved SEO and sitemap, language aware
|
||||
@@ -327,7 +378,9 @@ Plain text, the body is the code followed by a line feed. `language_disabled` is
|
||||
|
||||
**Fallbacks in the language of the version.** The title and description chains are unchanged except for the site text: `default_description` is treated as written in the current default language of the site, so it is used only by an unassigned article or a version in that language. For any other language the description stays empty, `fallbacks.description` is `none` and `fallbacks.site_text_skipped` is `true`. Changing the default language changes which versions may use the site text. `title_template`, `site_name` and the image chain stay shared; `cover_image_alt` stays per version.
|
||||
|
||||
**Sitemap.** `GET /v1/site/{key}/sitemap` lists every eligible version once (published, language enabled; unassigned published articles keep their legacy entry). Each item is `{url, modified_at, noindex, lang}`; `url` is the localized address when the site has a pattern, otherwise `base_url/slug`. `X-Total-Count` counts the listed items.
|
||||
**Sitemap.** `GET /v1/site/{key}/sitemap` lists every eligible version once (published, language enabled; unassigned published articles keep their legacy entry). Each item is `{url, modified_at, noindex, lang}`; `url` is the localized address when the site has a pattern, otherwise `base_url/slug`. `X-Total-Count` counts the listed items. The route shares the rate limit of the public language routes.
|
||||
|
||||
**SEO check and status.** `POST /v1/article/{id}/seo/check`, the stored `seo_status` of the lists and the `seo` filter apply the same language rule as the resolved description: the site `default_description` can fill the description (and so avoid `desc_short` or raise `desc_long`) only for a version in the current default language of the site or without language.
|
||||
|
||||
## Media usage
|
||||
|
||||
|
||||
@@ -44,7 +44,7 @@ Anonymous and unverified callers only see online articles, and an offline articl
|
||||
- `items` is `[]` when empty, never `null`. Each item is the article object below.
|
||||
- Items follow `sort` (default `updated_at:desc`) across the whole list before paging. `title` ignores case, `status` orders on online state (`status:asc` puts drafts first), and every ordering ends with `id` descending so pages never overlap.
|
||||
- `total` is the number of visible articles matching `q` and `status`, before paging. `page` and `size` echo the values applied.
|
||||
- `counts` ignores `q`, `status`, `site`, `seo`, `sort`, `page` and `size`, so tab counts do not move while searching. `suggestions` is the number of visible articles whose `seo_status` is `suggestions` (hidden and ready articles and dismissed codes are not counted); it equals the `total` of `?seo=suggestions` sent with no other filter by the same caller. For anonymous and unverified callers `all` equals `online` and `draft` is `0`, and `status=draft` answers `items: []` and `total: 0`.
|
||||
- `counts` ignores `q`, `status`, `site`, `seo`, `sort`, `page` and `size` (and `suggestions` also ignores `lang` and `author`), so tab counts do not move while searching. `suggestions` is the number of visible articles whose `seo_status` is `suggestions` (hidden and ready articles and dismissed codes are not counted); it equals the `total` of `?seo=suggestions` sent with no other filter by the same caller. For anonymous and unverified callers `all` equals `online` and `draft` is `0`, and `status=draft` answers `items: []` and `total: 0`.
|
||||
- Each item also carries `seo_status` and `seo_suggestions` (see [SEO status and suggestions](#seo-status-and-suggestions)). `seo` filters on the status before paging, so `total` follows it while `counts` do not.
|
||||
- A page beyond the last answers `200` with `items: []` and the real `total`.
|
||||
- An invalid `page`, `size`, `status`, `sort` or an over-long `q` answers `400` with the plain-text message `The request cannot be processed due to a mismatch in the format of the body.` An invalid `format` is reported first.
|
||||
@@ -167,6 +167,10 @@ The fallback chain, first non-empty wins:
|
||||
|
||||
Suggestions come in that order. Codes in `seo_dismissed` are never reported or counted. The status is `hidden` when `noindex` is true, else `suggestions` when at least one suggestion remains, else `ready`. The list items carry `seo_status` and `seo_suggestions` (the count, also for a hidden article).
|
||||
|
||||
The resolved description uses the site `default_description` only for a version in the current default language of its site or without language, so `desc_short` and `desc_long` follow the language of the version, in the check and in the lists alike.
|
||||
|
||||
**Stored status.** Each version stores its `seo_status` and `seo_suggestions`. Every write that can change them clears the stored value in the same statement: an edit of the version (title, body, slug, SEO fields, authors, site), a cover or social image deleted, restored or merged, a credited author deleted, a move to another site (story move or site deletion with reassignment), a language assigned by the setup, a change of the site `default_description`, and a change of the site default language. The lists recompute the cleared versions (and only those) before answering, and the server recomputes them at startup, so `seo_status`, `?seo=` and `counts.suggestions` always reflect the current data without reading every visible body per request. A value computed from data that changed in the meantime is never stored.
|
||||
|
||||
## Markdown output
|
||||
|
||||
Headings, bold, italic, links, bullet and ordered lists, blockquotes, fenced code blocks, rules, images and inline code are converted. Anything else is reduced to its text. `script` and `style` content is removed, and the source HTML is never passed through; the only HTML in the output is the alignment wrapper described below. List indentation attributes are dropped. Links with a scheme other than http, https, mailto or tel are reduced to their text. Characters that markdown would read as formatting (`*`, `_`, `` ` ``, `[`, `]`, `<`, `\`) and line starts that would read as a heading, list, quote or rule (`#`, `-`, `+`, `>`, `1.`) are backslash-escaped, so plain text stays plain text. A line break becomes a backslash followed by a newline.
|
||||
|
||||
@@ -73,7 +73,7 @@ Checks run in this order:
|
||||
| 7 | 409 | `language_in_use` | a removed language still has an article, draft, offline, online or soft-deleted (the display count `version_count` stays non-deleted only) |
|
||||
| any | 409 | `languages_changed` | the database could not lock the site in time because another change is running (lock contention); nothing is applied, reload and retry |
|
||||
|
||||
Entries are checked in list order for step 4. A rejected request changes nothing. Disabling a language never touches its articles, and no article changes `lang`, status or publication. A `social_locale` is stored as `language_REGION` (`fr_fr` becomes `fr_FR`). The language part rule only compares with the tag; matching `Site.Locale` is deferred to slice 6.
|
||||
Entries are checked in list order for step 4. A rejected request changes nothing. Disabling a language never touches its articles, and no article changes `lang`, status or publication. A site always has exactly one default language: the rows are planned with one default and checked again before they are written. Changing the default language re-evaluates the stored SEO status of every article of the site, because the site `default_description` only counts for the default language (see [SEO status and suggestions](articles.md#seo-status-and-suggestions)). A `social_locale` is stored as `language_REGION` (`fr_fr` becomes `fr_FR`). The language part rule only compares with the tag; matching `Site.Locale` is deferred to slice 6.
|
||||
|
||||
## First-time language setup
|
||||
|
||||
|
||||
Reference in new issue
Block a user