Search by

skriva / statamic

Skirva

Statamic publish endpoints for Skriva: token-authenticated collection discovery and entry create/update.

Package info

github.com/getskriva/statamic

Homepage

Type:statamic-addon

pkg:composer/skriva/statamic

Statistics

Installs: 18

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v0.2.3 2026-08-21 08:55 UTC

This package is auto-updated.

Last update: 2026-09-21 09:16:41 UTC


README

Packagist Version CI License: MIT

Statamic addon that lets Skriva publish generated articles into your collections. Statamic's REST and GraphQL APIs are read-only, so publishing needs a small addon on the Statamic side — the same shape as the Payload plugin, speaking the same wire contract (FieldNode schema tree + mapped publish data). Block sets, SEO fields, relations and enums are discovered from your blueprint, never hardcoded.

Requirements

  • PHP 8.2+
  • Statamic 5 or 6

Install

From the Statamic project root:

composer require skriva/statamic

Then set the shared token in the Statamic site's .env:

AI_CONTENT_TOKEN=<openssl rand -hex 32>

Configuration

Optional, all via .env:

# Links leaving this site open in a new tab (default true). See /publish below.
AI_CONTENT_EXTERNAL_LINKS_NEW_TAB=false

Publish the config file with php artisan vendor:publish --tag=ai-content-config to edit the values directly.

Limitation: the new-tab treatment only applies to bard fields. A markdown fieldtype stores the markdown source verbatim and markdown has no target attribute — add the ExternalLink extension to your site's own markdown parser config if you want the same there.

Connect to Skriva

In Skriva, open your site's Connection page, choose destination "Statamic", paste the site's base URL and the same token, and pick a collection.

Endpoints return 503 while the token is unset — nothing is exposed without it.

Endpoints

All require Authorization: Bearer <token> and are rate-limited (60/min). No session, no CSRF.

GET /ai-content/manifest

Connection test + collection discovery. Unlike Payload (plugin owns one target) this addon serves every collection; Skriva lists them and the user picks.

{
  "ok": true,
  "addon": "skriva/statamic",
  "statamic": "6.24.2",
  "meta": { "version": "0.2.0", "capabilities": ["draft_flow", "conditions", "defaults"] },
  "collections": [{ "handle": "landing", "title": "Landing pages", "dated": false, "hasRoute": true }]
}

meta.version is this addon's version; meta.capabilities are the behavior flags Skriva gates on (it never compares version numbers). The same meta rides along on /schema.

GET /ai-content/schema?collection=<handle>

The collection's default entry blueprint as a FieldNode tree — identical wire shape to the Payload plugin's /schema. Fieldtype mapping:

Statamic FieldNode
bard, markdown richText (markdown slot; bard converted on publish)
text, slug text — textarea → textarea
integer, float, range number — toggle → checkbox, date → date
select, radio, button_group, checkboxes select with options (+ hasMany)
replicator blocks (sets = block palette, grouped sets flattened)
group group — grid → array
entries, terms relationship + candidate options (cap 100, cappedOut above)
assets upload + isMedia (never AI-filled)
users, link, section, html, … omitted

Sets/groups whose required fields include media are flagged unsatisfiable so Skriva's compiler drops them instead of letting the AI attempt them. Reserved handles (slug, date, parent) are omitted — the entry's slug and date are managed by /publish itself.

Each field may also carry:

  • default — the blueprint's default value. Skriva leaves such fields to Statamic instead of having the AI invent one.
  • condition — a normalized form of if/unless/if_any/show_when/hide_when/unless_any: { negate?: true, when: "all"|"any", rules: [{ field, operator, value }] }. The operator is split off the rule string ("not published"{operator: "not", value: "published"}); a custom fn:params rule becomes {operator: "custom"}, which Skriva knows it cannot evaluate. Fields with always_save: true get no condition — Statamic stores them regardless.
  • requiredIfrequired_if:field,value[,value…] validate rules as [{field, value}] (multiple values are OR, as in Laravel).

POST /ai-content/publish

{
  "collection": "landing",
  "slug": "post-slug",
  "status": "published",
  "externalId": "optional-entry-id",
  "lenient": false,
  "data": {
    "title": "",
    "intro": "…markdown…",
    "page_builder": [{ "blockType": "hero", "heading": "" }],
    "seo": { "meta_title": "", "meta_description": "" }
  }
}

data follows the /schema semantics: richText slots carry GitHub-flavored markdown, replicator values are {blockType, …fields} rows. Conversion on write:

  • bard ← markdown → HTML → ProseMirror via the field's own editor config (identical to a CP save), then degraded to what the field's buttons enable — the CP editor rejects the whole value on any node type its buttons don't cover. Disallowed leaves (horizontal rules) drop, disallowed containers (tables, lists, code blocks) unwrap to paragraphs, headings clamp to the nearest enabled level, disallowed marks strip. markdown fields store the source verbatim
  • links in a bard value that leave this site get target="_blank" rel="noopener noreferrer" in the stored ProseMirror JSON, so a reader following a cited source keeps the article open. "External" is an absolute http(s) URL whose host (minus www.) is none of this installation's site hosts; relative paths, anchors, mailto:/tel: and Statamic references (entry::…) are never touched
  • replicator rows become {id, type, enabled, …}; grid rows get ids; groups recurse
  • entries/terms normalize to id arrays

Behavior:

  • externalId present → updates that entry (404 if it no longer exists). Absent → looks the slug up in the collection and updates in place, else creates. Republishing never duplicates.
  • Dated collections get now() as the entry date on create; status: "draft" saves unpublished. Statamic forces a required date field onto such collections, but it is a reserved handle that never reaches the request — the endpoint satisfies that rule on its own.
  • The blueprint validates the converted values. Failures answer 422 with "The following fields are invalid: a, b" (details carries the full errors). Fields the request omits are validated against their blueprint default, so a required field with a default passes; the stored entry still omits it, leaving Statamic's default behavior in charge.
  • lenient: true skips blueprint validation entirely, so an article Skriva cannot get past it is parked as a draft for you to finish in the control panel. Only allowed with "status": "draft" — combining it with published is refused (422). Request validation and the slug format still apply.

Response: { "ok": true, "id": "<entry-id>", "url": "<absolute-url>", "editUrl": "<cp-url>", "created": true }.

GET /ai-content/entry?id=<entry-id>

Whether one entry is live yet — how Skriva notices that a parked draft was published by hand.

{ "ok": true, "id": "", "published": true, "url": "https://…", "editUrl": "https://…/cp/…" }

published reflects the entry's real visibility, not just its published flag: a scheduled (future-dated) or expired entry answers false. url is null for collections without a route, and only meaningful while published — a draft's public URL 404s. A deleted entry answers 404.

Development

composer install
vendor/bin/phpunit

Releasing

The addon's version lives in ServiceProvider::VERSION (there is no version field in composer.json — releases are git tags). Bump that constant, add the CHANGELOG entry, then tag the commit with the same number; meta.version on /manifest and /schema must match the tag.

License

MIT