Add to collection
add_content_itemCreate a new content_items row for a site under a given content_type slug — this is the RIGHT tool for adding any repeating detail page (/projects/<slug>, /blog/<slug>, /searches/<slug>, /publications/<slug>, /work/<slug>, /talks/<slug>, /teaching/<slug>, etc.) when the site already declares a matching collections[] entry. The row is written to the site's DRAFT version, so it shows on the index page's <collection-list> and its detail URL /<prefix>/<slug> in PREVIEW right away (via the collection's template + Mustache substitution); it goes public on the next publish. DO NOT use add_page for detail pages of an existing collection — that creates a static page that the index won't reflect and sitemap won't auto-emit.
IMAGES IN content: source them yourself with find_images or upload_image (with url) — that is the normal path and needs nothing from the person. An image-typed field holds an OBJECT: { "url": "https://…", "alt": "what is in the picture" }. A bare URL string is accepted and stored as { url }, but send alt when you can — it is the only description a screen reader gets, and the template may render it. Check the field's declared type with list_content_types before composing: a url-typed field takes the plain string instead. But if they want THEIR OWN picture on an item (their product shot, their photo of the venue), do not try to carry the file: create the item with the text, then give them get_editor_link with page_slug set to that item's path (e.g. 'blog/my-first-post') so they can drop the original straight onto it.
content is validated against the content_type's JSON schema (lenient/open): required fields must be present and non-empty, declared fields must match their type, extra fields are allowed. Include every field the collection's template references — missing fields render as empty strings in the detail page. visibility defaults to 'public' (anonymous-readable).
SLUG BEHAVIOR (idempotent): re-calling with the SAME slug + identical content is a safe no-op — it returns success with already_existed:true (no error), so a retried write after a dropped connection won't double-post. Re-using the same slug with DIFFERENT content is a real collision and throws ConflictError — pick another slug or use edit_content_item to change the existing row.
If the site doesn't yet have a matching collections[] entry, add one first via create_collection (composes the collections[] entry + optional index page + items[] in one call). Each site defines its own schemas inline at collections[i].fields — there is no global content-type catalog; what shapes exist for a site is whatever the site's draft_fixture declares.
Input Schema
| Name | Required | Description | Default |
|---|---|---|---|
| slug | Yes | Single-segment kebab-case slug (lowercase alphanumeric + dashes, no slashes, no leading/trailing dashes). The DB enforces this via a CHECK constraint; invalid slugs are rejected at insert time. | |
| content | Yes | ||
| site_id | Yes | ||
| visibility | No | public | |
| content_type | Yes | ||
| published_at | No | ISO-8601 timestamp used as the item's `publishedAt` for ordering. Defaults to now if omitted (and left untouched if `content.publishedAt` is already set). Sort a collection-list newest-first with sortBy:'publishedAt', sortDir:'desc' — more reliable than created_at, which can tie on bulk/automated inserts. |