# 004 — `html_fragment`/`deck`/`html_full` content-shape rules exist only in the client helper, not the server
**Status:** Fixed
## Description
`docs/api.md` and `skills/daybook-posting/SKILL.md` both describe a
content-shape contract per text `kind`:
- `html_fragment` — "Wrapped with a Daybook theme" (body-only HTML, no
`doctype`/`html`/`body`).
- `html_full` — complete document, must contain ``.
- `deck` — top-level `` blocks.
`skills/daybook-posting/scripts/daybook_post.py`'s `validate_payload()`
enforces all three of these client-side before ever sending the request.
**`artifacts/views.py`'s `ArtifactListCreateView.post` / `create_text_artifact`
enforce none of them** — the only server-side checks are "kind is one of
the 4 text kinds" and "required fields present." Any client that talks to
`POST /api/artifacts` directly (curl, a different language, a
misconfigured agent) bypasses the contract entirely, and the server
accepts and stores whatever is sent.
Confirmed: posting `kind: "html_fragment"` with a full
`......` as
`content` returns `201`, and the stored page (`GET /a/{id}`) is:
```html
...
nested html fragmentInjected
Nested doc
```
i.e. a doctype and a full `` nested inside the theme
wrapper's own `` — invalid markup that browsers will
silently "fix up" in unpredictable ways (duplicate/conflicting ``,
nested ``, etc.), rather than the clean, themed fragment the kind
is supposed to guarantee.
This is the same class of gap as #003: validation the docs promise is
implemented only in the optional client helper, not the server that's
supposed to be the actual contract enforcement point. Related to (but
distinct from) issue #003, which is about unenforced *length* limits
rather than unenforced *content shape*.
## Suggested Fix
Port `daybook_post.py`'s `validate_payload()` checks (or equivalent) into
`create_text_artifact`/`ArtifactListCreateView.post` server-side, returning
`400` with the same messages the helper already uses. This also closes the
gap for any future non-Python client.
## Resolution
Added `validate_content_shape(kind, content)` in `artifacts/views.py`,
ported directly from `daybook_post.py`'s `validate_payload()` (same rules,
same case-insensitive substring checks) so client and server agree: `deck`
requires `