Feature Specification: Item Markdown Editor
Behaviour-first spec per Constitution Principle IX.
Feature ID: item-markdown-editor
Branch: feat/ew-item-markdown-editor
Status: Draft
Created: 2026-05-15
Last updated: 2026-05-15
Owner: ever-works
1. Overview
Work owners can author and edit the long-form markdown body that appears on
each item's detail page on the generated site. Today the platform writes a
stub body (# Name\n\nDescription\n\n[source_url](source_url)) for every
new item because no UI surface or API field accepts user-authored content;
the underlying contract and generator have always supported a markdown
field but it was unreachable from outside the platform. This feature opens
that channel end-to-end from the dashboard through to the user's data
repository.
2. User Scenarios
2.1 Primary scenarios
- Given a work owner is on the "Add item" modal, when they type
markdown in the new "Content (Markdown)" field and submit, then the
item lands in the data repo with their markdown written to
data/<slug>/<slug>.mdand mirrored on the YAMLmarkdownfield — and the next site generation renders that body verbatim instead of the stub. - Given an existing item without authored content, when the owner picks "Edit content" from the item's dropdown menu, then the dialog opens with whatever body the item currently has (stub or authored), pre-filled in a textarea for editing.
- Given the owner edits the body and clicks "Update item", then the
platform writes the new content to
data/<slug>/<slug>.md, updates the YAML mirror, commits with messageUpdate <name> content, and pushes. Either direct-commit to default or open a PR according to the "Create Pull Request" toggle. - Given the owner toggles the "Preview" button on the markdown field,
then the rendered markdown appears below the textarea using the same
react-markdown+remark-gfmrendering used elsewhere on the platform.
2.2 Edge cases & failures
- Given the markdown body is empty on create, when the item is
submitted, then the existing stub-fallback behaviour applies (no
regression —
# Name\n\nDescription\n\n[url](url)is written). - Given the markdown body exceeds 100,000 characters, when the
request reaches the API, then validation rejects it with a
maxLengtherror before any git work begins. The UI surfaces the error. - Given the body is unchanged between dialog open and save, when the user clicks "Update item", then no commit is created (no-op close) — avoids churn-only commits in the data repo.
- Given another user/process edited the item between dialog open and save, then the platform's normal last-write-wins commit semantics apply (no optimistic locking introduced in this feature).
- Given the user authors MDX custom components (
<Tag>,<TagList>, etc.) that exist only on the directory-web-template render path, then the platform preview renders them as their plain-text fallback — this is acknowledged and documented, not a bug.
3. Functional Requirements
- FR-1 The "Add item" form MUST present an optional "Content (Markdown)" textarea that accepts up to 100,000 characters.
- FR-2 The form MUST offer an in-place rendered preview of the markdown using GitHub-Flavoured Markdown.
- FR-3 When the user submits an item with markdown body, the platform
MUST persist that body to
data/<slug>/<slug>.mdand to the YAMLmarkdownfield on the item record in the data repo. - FR-4 When the user submits an item with no markdown body, the platform MUST continue to write the existing stub fallback so the site always has a rendered detail page.
- FR-5 The item dropdown menu MUST expose an "Edit content" action that opens a dialog pre-populated with the current item's markdown body.
- FR-6 Saving an unchanged body MUST NOT produce a commit.
- FR-7 The Update endpoint MUST accept an optional
markdownfield. Submitting a changedmarkdownMUST write both files (<slug>.mdand YAML), commit with a content-update message, and push. - FR-8 The API MUST validate
markdownas an optional string with a 100,000-character maximum length; longer payloads MUST be rejected with HTTP 400 before any git work is performed. - FR-9 Adding
markdownto existing DTOs MUST remain backwards compatible: omitting the field preserves prior behaviour for callers that do not yet know about it (Principle X).
4. Non-Functional Requirements
- Performance: client-side render of the preview SHOULD only fetch the
react-markdownchunk when the user opens the preview pane (deferred import). The Add-item modal MUST NOT regress its initial bundle size by more than ~5 KB gzipped (only the textarea + dynamic-import shim ship eagerly). - Reliability: a write failure on either of the two channels
(
<slug>.mdor YAML) MUST surface the error to the user and abort the commit; partial writes MUST NOT be pushed. - Security & privacy: markdown content is non-secret user content. Body text MUST NOT be logged (only existence/size is acceptable in debug logs).
- Observability: the existing
Add .../Update ... contentcommit messages are sufficient audit trail; no new activity-log event needed. - Compatibility: contract change is additive — new field is optional on every DTO it lands on.
5. Key Entities & Domain Concepts
| Entity / concept | Description |
|---|---|
ItemData.markdown | Existing optional long-form body field on an item; already serialised into YAML and into <slug>.md by the generator. |
<slug>.md file | The canonical source the directory-web-template renders; takes precedence over the YAML markdown field. |
| Content dialog | New UI surface in the item's dropdown menu for editing the body of an existing item without re-creating it. |
6. Out of Scope
- Rich-text WYSIWYG editing. Plain textarea + preview only.
- MDX-component-aware preview (
<Tag>etc.). The preview deliberately shows GFM fallback only; the site keeps doing the MDX render. - Automatic AI generation of item bodies — handled separately by the generator pipeline, not this feature.
- Item-level revision history beyond what git already gives us in the data repo.
- Translations of the new UI labels beyond
en.json— added to the follow-up i18n sweep that already covers the other locales.
7. Acceptance Criteria
- Adding an item with a non-empty
markdownbody writes the literal text todata/<slug>/<slug>.mdand to the YAML. - Adding an item with an empty
markdownbody writes the stub fallback unchanged. - "Edit content" opens a dialog seeded with the current body.
- Saving an unchanged body does NOT create a commit.
- Saving a changed body writes both files, commits with a
Update <name> contentmessage, and either pushes to main or opens a PR based on the toggle. - The API rejects markdown bodies over 100,000 chars with a 400.
- Existing SubmitItem / UpdateItem callers that don't send
markdowncontinue to work unchanged. - All new validators are covered by
dto.spec.tscases.
8. Open Questions
[NEEDS CLARIFICATION: should we add a soft warning at, say, 50,000 chars for unusually long bodies, or trust the hard 100k cap?]— Punt, hard cap only for now.[NEEDS CLARIFICATION: do we want a follow-up to mirror the markdown field on the platform's Item dashboard list / ItemCard (so users can see whether an item has authored content)?]— Tracked as a follow-up.
9. Constitution Gates
- Plugin-first (Principle I) — N/A, no new external integration.
- Capability-driven (Principle II) — N/A, no plugin behaviour touched.
- Source-of-truth repos preserved (Principle III) — content lives in the user's data repo, not the platform DB.
- Long-running via Trigger.dev (Principle IV) — N/A, request-scoped git operations only (existing pattern).
- Forward-only migrations (Principle V) — no schema change.
- Tests accompany (Principle VI) — DTO validation specs added.
- Secret hygiene (Principle VII) — N/A.
- Plugin counts canonical (Principle VIII) — N/A.
- Behaviour-first spec (Principle IX) — this document.
- Backwards-compatible (Principle X) — additive optional field.
10. References
- Related feature:
data-generator— how item content gets written to the data repo on a full generate. - Related contract:
packages/contracts/src/item/item.types.ts— the pre-existingmarkdown?: stringfield onItemData. - Related site renderer: directory-web-template
apps/web/components/item-detail/server-item-content.tsx— consumer of themarkdownchannel.