Tag blog posts and sign them with a member
Some checks failed
Tests / lint (push) Has been cancelled
Tests / test (push) Has been cancelled
Tests / test-postgres (push) Has been cancelled
Tests / mcp (push) Has been cancelled

Tags are named in every language and made from the page form; the byline is
the member who signs a post on the site. The public API sends both, filters
posts by tag and lists the tags in use.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
2026-10-01 11:01:18 +02:00
parent 6fc600e6a1
commit 9f970808ca
27 changed files with 809 additions and 12 deletions

View File

@@ -328,6 +328,32 @@ paths:
"422": { $ref: "#/components/responses/ValidationError" }
"503": { $ref: "#/components/responses/Maintenance" }
/public/v1/tags:
get:
tags: [Public]
summary: Blog tags in use
description: Tags on something published, with how many posts each has in the request's language. Most used first.
security: []
parameters:
- $ref: "#/components/parameters/Lang"
responses:
"200":
description: The tags
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
type: object
properties:
name: { type: string }
slug: { type: string }
count: { type: integer }
"503": { $ref: "#/components/responses/Maintenance" }
/public/v1/projects:
get:
tags: [Public]
@@ -871,6 +897,10 @@ paths:
- name: category
in: query
schema: { type: string }
- name: tag
in: query
description: A tag's slug in the active language.
schema: { type: string }
- name: featured
in: query
schema: { type: boolean }
@@ -1330,6 +1360,76 @@ paths:
"204": { description: Deleted }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/tags:
get:
tags: [Categories]
summary: List blog tags
description: Every tag, unpaginated, with how many pages carry it. Kept by whoever keeps the categories.
responses:
"200":
description: The tags
content:
application/json:
schema:
type: object
properties:
data:
type: array
items: { $ref: "#/components/schemas/Tag" }
"403": { $ref: "#/components/responses/Forbidden" }
post:
tags: [Categories]
summary: Create a blog tag
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/TagWrite" }
responses:
"201":
description: Tag created
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Tag" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
/v1/tags/{tag}:
parameters:
- name: tag
in: path
required: true
schema: { type: integer }
patch:
tags: [Categories]
summary: Update a blog tag
requestBody:
required: true
content:
application/json:
schema: { $ref: "#/components/schemas/TagWrite" }
responses:
"200":
description: Tag updated
content:
application/json:
schema:
type: object
properties:
data: { $ref: "#/components/schemas/Tag" }
"403": { $ref: "#/components/responses/Forbidden" }
"422": { $ref: "#/components/responses/ValidationError" }
delete:
tags: [Categories]
summary: Delete a blog tag
description: The pages keep existing; they just stop carrying it.
responses:
"204": { description: Deleted }
"403": { $ref: "#/components/responses/Forbidden" }
/v1/technologies:
get:
tags: [Technologies]
@@ -5221,6 +5321,26 @@ components:
cache downstream at the same moment.
updated_at: { type: string, format: date-time }
category: { $ref: "#/components/schemas/PublicCategory" }
tags:
type: array
description: The ones named in this language, to link to the blog filtered by each.
items:
type: object
properties:
name: { type: string }
slug: { type: string }
byline:
type: object
nullable: true
description: The member who signs it, when they have a public profile.
properties:
name: { type: string }
slug: { type: string }
headline: { type: string, nullable: true }
avatar:
allOf: [{ $ref: "#/components/schemas/Image" }]
nullable: true
path: { type: string }
alternates: { $ref: "#/components/schemas/Alternates" }
path: { type: string, nullable: true }
@@ -5297,6 +5417,8 @@ components:
deleted_at: { type: string, format: date-time, nullable: true }
category: { $ref: "#/components/schemas/Category" }
author: { $ref: "#/components/schemas/User" }
byline_member_id: { type: integer, nullable: true, description: The member who signs it on the site. }
tag_ids: { type: array, items: { type: integer } }
featured_image_alt:
type: string
nullable: true
@@ -5325,6 +5447,8 @@ components:
meta_title: { $ref: "#/components/schemas/TranslatedField" }
meta_description: { $ref: "#/components/schemas/TranslatedField" }
category_id: { type: integer, nullable: true }
byline_member_id: { type: integer, nullable: true, description: The member who signs it on the site. }
tag_ids: { type: array, items: { type: integer }, description: Replaced whole when sent. }
status: { type: string, enum: [draft, review, published, archived] }
featured_image:
type: string
@@ -6173,6 +6297,23 @@ components:
subject: { type: object, additionalProperties: { type: string }, description: "Language → text. `{name}`, `{subject}` and `{sender}` are filled in." }
body: { type: object, additionalProperties: { type: string } }
Tag:
type: object
properties:
id: { type: integer }
name: { type: object, additionalProperties: { type: string }, description: Language → name. }
slug: { type: object, additionalProperties: { type: string } }
pages_count: { type: integer }
TagWrite:
type: object
required: [name, slug]
properties:
name: { $ref: "#/components/schemas/TranslatedField" }
slug:
allOf: [{ $ref: "#/components/schemas/TranslatedField" }]
description: Unique per language.
MediaFor:
type: string
enum: [pages, technologies, projects, members, clients, testimonials]