Feedback posts
A post is a sequence of steps. Each step asks a single question. The
post's definition is a JSON document persisted in the post_version
table. The same shape governs the default "did you try it?" post and any
post the operator builds.
Schema
{
"schema_version": 1,
"title": "Quick feedback",
"description": "Tell us what you think.",
"steps": [
{
"id": "tried",
"kind": "yes_no",
"label": "Did you try it?",
"required": true,
"branches": { "yes": "what_tried", "no": "why_not" }
},
{
"id": "what_tried",
"kind": "long_text",
"label": "What did you try?",
"next": "visibility"
}
]
}
Step kinds
yes_no— boolean. Branch viabranches: { yes, no }.short_text— single-line text.long_text— multi-line text.single_choice— one value fromoptions[]. Branch viabrancheskeyed by the chosenvalue.multiple_choice— list of values fromoptions[].rating— integer 1..5.
Step fields
| Field | Notes |
|-------|-------|
| id | Stable identifier within the post. Used for branching, telemetry, and as the answer key. |
| kind | One of the kinds above. |
| label | Question text shown to the visitor. |
| required | true blocks "Next" until the visitor answers. |
| help | Optional helper text rendered below the label. |
| options | Required for single_choice / multiple_choice. List of {value, label}. |
| branches | Map of answer-value → next-step-id. null means the post ends after this answer. |
| next | Fallback target when no branches entry matches. null means terminal. |
| controls | Optional special role: visibility (this answer sets is_public) or public_text (this answer becomes the public comment text). |
Link cards
A post can carry an optional link — an external URL that points at
whatever you're collecting feedback on. The admin pastes the URL into the
post editor and clicks Fetch link preview. The app fetches the URL
server-side, parses its Open Graph meta tags (og:title,
og:description, og:image, with twitter:* and <title> as
fallbacks), and renders a preview card the admin can save or remove.
When saved, the link block is persisted in the post's definition
JSON:
{
"schema_version": 1,
"title": "...",
"link": {
"url": "https://example.com/post",
"title": "Post title fetched from OG",
"description": "Post description fetched from OG",
"image_url": "https://cdn.example/post.png"
},
"steps": [...]
}
The public post page renders the card above the first step. The
GET /api/v1/posts/<slug> API response includes the same block so
external consumers can render the same card.
Server-side fetch behaviour: the fetch happens only when the admin clicks the button. It has a 5-second timeout, refuses to fetch internal addresses (loopback, RFC1918, link-local, IPv6 site-local), caps the response body at 256 KiB, and follows up to 5 redirects. Failures (timeout, non-2xx upstream, no metadata found) return a typed error code the editor displays inline. The OG image is referenced by URL — the app does not proxy or store the image itself.
Draft vs published
Each post has at most one draft version and at most one published version. Public visitors always see the published version; the builder edits the draft. The Publish action validates the draft (no cycles, no empty steps) and promotes it.
Publishing a post with a cycle returns an error naming the involved steps.