feat: updated integration page with missing API routes

This commit is contained in:
Florian Sylvain committed 2026-10-26 02:18:35 +01:00
1 parent 25d9c30b8a
commit 7c0ee21906
25 files changed
+2402 -350

No files matched your search

+181 -47
View File
@@ -22,12 +22,15 @@ import SessionOnlyGroup from '@/components/SessionOnlyGroup.vue'
import {
API_GROUPS,
ERROR_ROWS,
FIELD_TYPES_ANCHOR,
ERROR_SAMPLE,
PUBLIC_AUTH_ROWS,
SESSION_GROUPS,
authExamples,
type ApiGroup,
type ErrorRow,
} from '@/data/apiCatalogue'
import { SECTION_TITLE, endpointAnchor } from '@/lib/apiReference'
import { SECTION_TITLE, endpointAnchor, groupsAroundImages } from '@/lib/apiReference'
import { buildSearchIndex, type SearchEntry } from '@/lib/apiSearch'
const SPY_OFFSET_PX = 120
@@ -45,13 +48,17 @@ const errorSampleTabs = computed(() => [
const sessionRouteCount = SESSION_GROUPS.reduce((total, group) => total + group.rows.length, 0)
const { before: groupsBeforeImages, after: groupsAfterImages } = groupsAroundImages(API_GROUPS)
function groupSection(group: ApiGroup) {
return { id: group.key, label: translate(group.title) }
}
const sections = computed(() => [
{ id: 'auth', label: t('Authentication') },
...API_GROUPS.map((group) => ({
id: group.key,
label: translate(group.title),
})),
...groupsBeforeImages.map(groupSection),
{ id: 'images', label: t('Images and variants') },
...groupsAfterImages.map(groupSection),
{ id: 'session', label: t('Session only') },
{ id: 'errors', label: t('Errors') },
{ id: 'notes', label: t('Notes') },
@@ -67,7 +74,7 @@ const authItems = computed(() => {
const authNotes = computed(() => [
t(
'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.',
'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. Owner and admin routes answer 403 to a key whose creator is neither.',
),
t(
'Each key can make 120 requests per minute. Beyond that the API answers 429 with a `Retry-After` header.',
@@ -110,8 +117,64 @@ const errorColumns = computed(() => [
},
])
function quoteMessages(messages: string[]) {
return messages.map((message) => `"${message}"`).join(' / ')
interface MessagePart {
text: string
code: boolean
}
function messageParts(row: ErrorRow) {
const parts: MessagePart[] = []
const push = (text: string, code: boolean) => {
if (parts.length > 0) {
parts.push({ text: ' / ', code: false })
}
parts.push({ text, code })
}
row.messages.forEach((message) => push(`"${message}"`, false))
row.codes?.forEach((code) => push(code, true))
return parts
}
const fieldTypeColumns = computed(() => [
{
accessorKey: 'type',
get header() {
return t('Type')
},
meta: { class: { th: 'w-22', td: 'w-22' } },
},
{
accessorKey: 'value',
get header() {
return t('Value in an entry')
},
meta: { class: { th: 'lg:w-1/2', td: 'lg:w-1/2' } },
},
{
accessorKey: 'rules',
get header() {
return t('Rules')
},
meta: { class: { th: 'max-lg:hidden', td: 'max-lg:hidden' } },
},
])
const pageBlocks = [
...groupsBeforeImages.map((group) => ({ kind: 'group' as const, key: group.key, group })),
{ kind: 'images' as const, key: 'images', group: null },
...groupsAfterImages.map((group) => ({ kind: 'group' as const, key: group.key, group })),
]
function snippetTabs(group: ApiGroup) {
return group.snippet
? [
{
k: group.snippet.key,
label: translate(group.snippet.label),
code: group.snippet.code,
},
]
: []
}
function routeCount(count: number) {
@@ -371,7 +434,7 @@ const CARD_TITLE = 'text-md/6 font-semibold text-highlighted'
<p class="max-w-180 text-base text-pretty text-toned">
{{
t(
'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.',
'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, and manage content types and entries, without the back-office.',
)
}}
</p>
@@ -458,33 +521,88 @@ const CARD_TITLE = 'text-md/6 font-semibold text-highlighted'
</div>
</section>
<section
v-for="group in API_GROUPS"
:key="group.key"
:data-anchor="group.key"
:aria-labelledby="`h-${group.key}`"
:class="SECTION_CLASS"
>
<div class="flex flex-wrap items-baseline gap-2.5">
<h2 :id="`h-${group.key}`" :class="SECTION_TITLE">
{{ translate(group.title) }}
</h2>
<span class="text-muted">{{ routeCount(group.endpoints.length) }}</span>
</div>
<span class="text-pretty text-toned">{{ translate(group.description) }}</span>
<div class="border border-t-0 border-default bg-default">
<EndpointCard
v-for="endpoint in group.endpoints"
:key="endpoint.id"
:ref="(card) => setCard(endpoint.id, card)"
:endpoint="endpoint"
:origin="origin"
:flash="flashedCard === endpoint.id ? flashMode : null"
/>
</div>
</section>
<IntegrationImages :origin="origin" />
<template v-for="block in pageBlocks" :key="block.key">
<IntegrationImages v-if="block.kind === 'images'" :origin="origin" />
<section
v-else
:data-anchor="block.group.key"
:aria-labelledby="`h-${block.group.key}`"
:class="SECTION_CLASS"
>
<div class="flex flex-wrap items-baseline gap-2.5">
<h2 :id="`h-${block.group.key}`" :class="SECTION_TITLE">
{{ translate(block.group.title) }}
</h2>
<span class="text-muted">{{
routeCount(block.group.endpoints.length)
}}</span>
</div>
<span class="text-pretty text-toned">{{
translate(block.group.description)
}}</span>
<div v-if="block.group.note" class="flex gap-2.5 text-base text-toned">
<UIcon name="i-lucide-info" :class="NOTE_ICON" />
<span class="text-pretty"
><CodeText :text="translate(block.group.note)"
/></span>
</div>
<div
v-if="block.group.fieldTypes"
:data-anchor="FIELD_TYPES_ANCHOR"
class="flex scroll-mt-16 flex-col gap-1.5 lg:scroll-mt-8"
>
<span class="font-semibold text-highlighted">{{
t('Field types')
}}</span>
<UTable
:data="block.group.fieldTypes"
:columns="fieldTypeColumns"
:ui="{
root: 'border border-default bg-default',
base: 'table-fixed',
thead: 'bg-elevated [&>tr]:border-t-0',
separator: 'hidden',
th: 'px-2.5 py-1.5 text-sm font-medium text-muted',
td: 'px-2.5 py-2 align-top text-sm whitespace-normal text-toned',
tbody: 'divide-y-0',
tr: 'border-t border-default',
}"
>
<template #type-cell="{ row }">
<span class="font-mono text-xs font-medium text-highlighted">{{
row.original.type
}}</span>
</template>
<template #value-cell="{ row }">
<div class="flex flex-col gap-0.5">
<span class="text-pretty"
><CodeText :text="translate(row.original.value)"
/></span>
<span class="text-pretty lg:hidden"
><CodeText :text="translate(row.original.rules)"
/></span>
</div>
</template>
<template #rules-cell="{ row }">
<span class="text-pretty"
><CodeText :text="translate(row.original.rules)"
/></span>
</template>
</UTable>
</div>
<CodeSnippet v-if="block.group.snippet" :tabs="snippetTabs(block.group)" />
<div class="border border-t-0 border-default bg-default">
<EndpointCard
v-for="endpoint in block.group.endpoints"
:key="endpoint.id"
:ref="(card) => setCard(endpoint.id, card)"
:endpoint="endpoint"
:origin="origin"
:flash="flashedCard === endpoint.id ? flashMode : null"
/>
</div>
</section>
</template>
<section data-anchor="session" aria-labelledby="h-session" :class="SECTION_CLASS">
<div class="flex flex-wrap items-baseline gap-2.5">
@@ -492,7 +610,7 @@ const CARD_TITLE = 'text-md/6 font-semibold text-highlighted'
{{ t('Session only') }}
</h2>
<span class="text-muted">{{
t('{0} routes, made for a browser session', [sessionRouteCount])
t('{0} routes, not available to API keys', [sessionRouteCount])
}}</span>
</div>
<SessionOnlyGroup :groups="SESSION_GROUPS" :public-rows="PUBLIC_AUTH_ROWS" />
@@ -532,29 +650,45 @@ const CARD_TITLE = 'text-md/6 font-semibold text-highlighted'
translate(row.original.meaning)
}}</span>
<div class="flex flex-col gap-0.5 lg:hidden">
<span class="text-pretty text-toned">{{
quoteMessages(row.original.messages)
}}</span>
<span class="text-pretty text-toned"
><template
v-for="(part, index) in messageParts(row.original)"
:key="index"
><code
v-if="part.code"
class="font-mono text-xs text-highlighted"
>{{ part.text }}</code
><template v-else>{{ part.text }}</template></template
></span
>
<span
v-if="row.original.note"
class="text-pretty text-muted"
data-testid="error-note"
>{{ translate(row.original.note) }}</span
>
><CodeText :text="translate(row.original.note)"
/></span>
</div>
</div>
</template>
<template #messages-cell="{ row }">
<div class="flex flex-col gap-0.5">
<span class="text-pretty">{{
quoteMessages(row.original.messages)
}}</span>
<span class="text-pretty"
><template
v-for="(part, index) in messageParts(row.original)"
:key="index"
><code
v-if="part.code"
class="font-mono text-xs text-highlighted"
>{{ part.text }}</code
><template v-else>{{ part.text }}</template></template
></span
>
<span
v-if="row.original.note"
class="text-pretty text-muted"
data-testid="error-note"
>{{ translate(row.original.note) }}</span
>
><CodeText :text="translate(row.original.note)"
/></span>
</div>
</template>
</UTable>