feat: custom content types backend
This commit is contained in:
1 parent
ed67b7e70d
commit
58d58f5a04
41 files changed
+4086
-2
No files matched your search
@@ -0,0 +1,108 @@
|
||||
# Claude Design prompt: custom content types
|
||||
|
||||
Paste everything below the line into Claude Design, with the existing RenewCMS design system attached.
|
||||
|
||||
---
|
||||
|
||||
You are extending the RenewCMS back-office design. RenewCMS is a small self-hosted CMS (Go API plus a Vue 3 SPA built with Nuxt UI v4 and Tailwind 4). The existing screens, the AppFrame back-office shell, the AuthCard auth frames, the design tokens and the component inventory are already in this design system: reuse them, never invent new colors, spacing, radius or type values, and keep the same copy tone (short, plain, sentence case). Deliver the same artifacts the system already uses: one board per screen with every state, the exact copy per state, the routes with their auth requirement, the new components with their Nuxt UI mapping, and light and dark variants at desktop (1440x900) and mobile (390x844).
|
||||
|
||||
## The feature
|
||||
|
||||
Until now RenewCMS only managed fixed content: articles, authors, sites, media and users. We are adding custom content types: an owner or admin defines a content type (for example "Recipe" or "Event") made of ordered fields, and then any verified user creates entries of that type. Think of the clear part of Directus: a type is a collection, a field is a typed column. Do not copy Strapi's split between collections, components and fields into separate screens: everything about a type happens on one screen. Do not draw a table-builder feel with database jargon; speak in terms of content.
|
||||
|
||||
The backend for the first part already exists. Design the full experience including the parts that come later, and mark each screen or state as "available now" or "later" (see the scope table).
|
||||
|
||||
## What the backend does today (design against this, do not contradict it)
|
||||
|
||||
Routes, all behind the normal signed-in, verified-user back-office session (cookie or API key):
|
||||
|
||||
- GET /v1/content-type: list of types, ordered by name.
|
||||
- POST /v1/content-type, PUT /v1/content-type/{key}, DELETE /v1/content-type/{key}: owner or admin only.
|
||||
- GET /v1/content-type/{key}.
|
||||
- GET and POST /v1/content-type/{key}/entry; GET, PUT and DELETE /v1/content-type/{key}/entry/{id}. Any verified user.
|
||||
|
||||
A content type: id, key, name, fields, created_at, updated_at.
|
||||
- key: lowercase slug, 2 to 40 characters, unique, chosen at creation, never editable afterwards. It appears in URLs and the API.
|
||||
- name: free text, up to 100 characters, editable.
|
||||
- fields: ordered list, 1 to 50 fields.
|
||||
|
||||
A field: key, label, type, required, unique, default, min, max, options.
|
||||
- key: lowercase letters, digits and underscores, starts with a letter, up to 40 characters, unique within the type. It becomes the JSON key in the API.
|
||||
- label: the text shown in the editor.
|
||||
- type: one of text, richtext (markdown), number, boolean, date (a calendar date, no time), select (a single choice from a list of 1 to 50 distinct options).
|
||||
- required, unique (text, number, date and select only), default (applied only when an entry is created and the field is omitted), min and max.
|
||||
- min and max mean: length in characters for text and richtext, value for number, earliest and latest date for date. They do not exist for boolean and select. min must not exceed max. A default must satisfy the field's own rules. A unique field cannot have a default.
|
||||
- A field's type can never change after the type is saved, and the type key never changes.
|
||||
|
||||
An entry: id, content_type (the type's key), data (an object keyed by field key), created_by, updated_by, created_at, updated_at.
|
||||
- Saving an entry replaces its whole data. Defaults apply only on create. An explicit empty value stays empty, and a required field cannot be empty.
|
||||
- Unknown keys are rejected. Values of a removed field disappear.
|
||||
|
||||
Schema changes once entries exist:
|
||||
- Removing a field is allowed; its stored values stop being returned.
|
||||
- Every existing entry is re-validated against the new schema. If any entry would become invalid (a new required field without a default, a field made unique while duplicates exist, a tighter min or max below a stored value, a removed option still in use), the whole change is refused as incompatible and nothing changes.
|
||||
- Adding a required field is fine if it has a default.
|
||||
- A type that has entries cannot be deleted.
|
||||
|
||||
Entry lists are paginated: page size 5, 25, 50 or 100 (default 25), sort by id, created_at or updated_at in either direction (default most recently updated first). There is no filtering or search on custom fields yet.
|
||||
|
||||
Errors come back as plain text. Map each to friendly copy:
|
||||
- content_type_not_found, entry_not_found
|
||||
- content_type_key_taken
|
||||
- content_type_has_entries
|
||||
- invalid_content_type_key
|
||||
- invalid_field
|
||||
- schema_incompatible
|
||||
- "invalid_entry: <field key>: <rule>", rule being one of required, type, min, max, options, unique, unknown_field. Unique violations arrive as HTTP 409, the other entry rules as 422.
|
||||
- The usual 401, 403 (unverified account or insufficient role), 400 and 500 already covered in the system's HTTP-status-to-copy table.
|
||||
|
||||
## Screens to design
|
||||
|
||||
For each screen: every state (loading, empty, populated, error, forbidden, offline or failed save where relevant), the exact copy, the routes and their auth requirement, and a mobile layout.
|
||||
|
||||
### 1. Schema screen: list of types (available now)
|
||||
Sits in the back-office navigation as "Schema", visible to owners and admins; other verified users do not see it. Shows the types as a list or table: name, key, number of fields, last updated. Shows the built-in types (Articles, Authors, Sites, Media, Users) in the same list with a "Built-in" badge and a lock icon. Built-ins are read-only entries here: they open a read-only view listing their fields, with no edit, no delete and no ability to add fields. Do not suggest that built-ins are custom collections; the badge and a one-line explanation make clear they are managed by RenewCMS. Primary action "New content type". States: loading, empty (no custom type yet: invite the user to create one, built-ins still listed), populated, error, forbidden (non-admin who types the URL).
|
||||
|
||||
### 2. Schema builder (available now, the main screen)
|
||||
One screen with three regions, not three separate areas:
|
||||
- Left: the ordered field list. Reorder (drag with a keyboard-accessible alternative), add field, remove field. Each row shows label, key, type icon and small badges for required and unique.
|
||||
- Right: the field inspector for the selected field: label, key (generated from the label, editable until the type is first saved, then locked), type (a gallery of the six types with icon and one-line description), required, unique (disabled with an explanation where not allowed), default, min and max (with the right unit wording per type), and for select an options editor (add, remove, reorder, no duplicates, no blanks). Show the rules inline as the user types, not only after save.
|
||||
- Top: type name, type key (editable only at creation; afterwards locked with a tooltip explaining why), Save and Discard.
|
||||
- Live API preview: a collapsible panel showing the exact JSON an entry of this type looks like (a sample entry using each field's type and default) and the route it lives at, updating as fields change. Include a copy button.
|
||||
States: new type, editing an existing type, unsaved changes, validation errors per field and at type level (inline, with the field highlighted in the list), saving, saved, save failed, key already taken, schema incompatible with existing entries.
|
||||
|
||||
### 3. Schema change review (available now for the review step, later for the diff)
|
||||
Before saving changes to a type that already has entries, show a review step: "Add 2 fields, change 1, remove 1", listing each change, with destructive ones (removing a field, removing an option, tightening a limit, making a field unique or required) clearly flagged. Because the server decides compatibility, the review is informational plus an explicit Apply button; if the server answers schema incompatible, show a refusal state that says nothing was changed and which kind of change likely caused it (we cannot tell which entry, so write copy that does not pretend to). Also design the "delete type" confirmation: blocked when entries exist (explain that entries must be deleted first) and a plain confirmation when empty. Mark per-change entry counts and a full diff as "later" (the API does not return them yet).
|
||||
|
||||
### 4. Field-type gallery (available now)
|
||||
A reference board for the six field types showing, for each: its icon, its inspector options, its list cell (how a value renders in the entries table, including the empty state), its form control in the entry editor (markdown editor for richtext with preview, number input with min and max hints, toggle for boolean using the system's single toggle component, date picker, select, text with a character counter when a limit exists), and its error states per rule (required, type, min, max, options, unique). Show the "filter" widget for each type as "later" since filtering on custom fields is not available yet.
|
||||
|
||||
### 5. Content navigation and entry list (available now)
|
||||
The back-office sidebar gets a "Content" group generated from the custom types, one entry per type (name, with the type's icon if any; truncated gracefully with many types; collapsed on mobile). The list screen for a type shows a paginated table with a column per field (cap the visible columns and let the user choose which, defaulting to the first few), plus created and updated dates and the author of the last change. Sort control (id, created, updated), page size selector (5, 25, 50, 100), "New entry" button. Row click opens the editor. States: loading, empty for this type (with a hint pointing to "New entry"), populated, error, forbidden, type not found.
|
||||
Use a sample "Events" type for all boards: title (text, required, unique, max 80), starts_on (date, required), ends_on (date), online (boolean, default false), capacity (number, min 1, max 5000), category (select: Talk, Workshop, Meetup), description (richtext).
|
||||
|
||||
### 6. Entry editor (available now)
|
||||
Create and edit form generated from the schema, one control per field in schema order, with labels, required markers, helper text from min, max and options, a character counter where a limit exists, and inline errors per field. Show defaults pre-filled on create. Save, Cancel, Delete (with confirmation). Provenance footer: created by, updated by, dates. States: new, editing, unsaved changes, saving, saved, failed save, field errors for each rule, duplicate unique value (409), entry not found, concurrent schema change (a field disappeared or a new required field appeared since the form loaded: explain that the form must be reloaded).
|
||||
|
||||
### 7. Relation field and sentence-style relation picker (later, design now)
|
||||
Relations are not built yet. Design the picker so that the model reads as sentences, not jargon: "Each Event has one Venue" and, beside it, "Each Venue has many Events", with a live preview of both sides. Offer the three relationship shapes in plain words. Show how a relation field renders in the inspector, the entry form (search and pick, with chips for many) and the list cell. Mark everything "later".
|
||||
|
||||
### 8. Permissions grid (later, design now)
|
||||
A collection by action grid on the roles area: rows are types (custom and built-in), columns are read, create, update and delete, per role. Built-in rows are locked where the system dictates it. Make the grid usable on mobile (collapse to a per-role list). Mark "later". Today's rule is simple: owners and admins edit schema, any verified user reads and writes entries; show that as the default state of the grid.
|
||||
|
||||
### 9. Schema export and import (later, design now)
|
||||
Export the whole schema or a single type to a file, import with a preview of what will be created or changed and the same compatibility review as screen 3, plus an explicit confirm. Aim at the dev-to-production flow. Mark "later".
|
||||
|
||||
## Scope table to include in your answer
|
||||
|
||||
For every screen and state, label it "available now" or "later" so engineering knows what is wired. Available now: screens 1 to 6 (except the diff counts), the live API preview, the type-level and field-level validation states. Later: relation picker, permissions grid, schema export and import, filters on custom fields, per-change entry counts, publish and draft state, public (anonymous) read access, activity log entries for types and entries.
|
||||
|
||||
## Constraints
|
||||
|
||||
- Nuxt UI v4 only, mapped to existing components in the inventory. List every new component you introduce with its Nuxt UI mapping and why an existing one does not fit.
|
||||
- Tokens only. No raw hex values and no arbitrary Tailwind colors.
|
||||
- Accessibility: keyboard reorder for fields, focus order, visible focus, labels on every control, error text tied to its input, sufficient contrast in light and dark.
|
||||
- Copy: short and plain, no database jargon in primary labels (say "field", "choice list", "entry", "content type"; keep "key" for the API identifier and explain it once).
|
||||
- Do not redesign existing screens. Only add what these screens need: new sidebar group, new routes, new components.
|
||||
- Provide the routes table: /schema, /schema/new, /schema/:key, /content/:key, /content/:key/new, /content/:key/:id, each with its auth requirement (schema routes owner or admin, content routes any verified user).
|
||||
- Anything the API cannot do today (see above) must not appear as if it works. If a design needs a backend capability that does not exist, flag it explicitly in a short "needs backend" list at the end.
|
||||
@@ -0,0 +1,14 @@
|
||||
# Custom content types
|
||||
|
||||
A content type is a named schema (a key, a name and 1 to 50 fields) stored as one row with the fields as a JSON text column. Entries of every type share one generic table whose `data` column holds a JSON object. No DDL runs at runtime, so creating or changing a type never alters the database schema.
|
||||
|
||||
## Layering
|
||||
|
||||
The domain packages `internal/domain/contenttype` and `internal/domain/entry` own the rules: key, name and field validation, entry validation against a schema, the sort whitelist, the page limits and the value equality used by uniqueness. The use cases orchestrate them through the two repositories. The repositories only store and load rows. Entry counting for the "type still has entries" rule lives on the entry repository.
|
||||
|
||||
## Known limitations of phase 1
|
||||
|
||||
- Uniqueness and schema re-validation are O(n). Checking a unique field loads every entry of the type and compares values in Go, and updating a type with stored entries loads all of them to re-validate. This keeps the SQL portable across SQLite, MySQL/MariaDB and PostgreSQL. The path to fix it is the filterable-fields phase: project the values of unique and filterable fields into an indexed table, so uniqueness becomes an indexed lookup and re-validation can be restricted to the changed fields.
|
||||
- Unique values are checked without a transaction, so two concurrent writes of the same unique value can both succeed. A content type key is protected by a unique index, and a violation is reported as `content_type_key_taken` (409).
|
||||
- `entry.Repository.DeleteByContentType` is intentionally unused for now. A content type that still has entries cannot be deleted; the method is kept as part of the repository contract for a later cascade.
|
||||
- Listing is limited to page 1,000,000 and to page sizes of 100 so that the SQL offset cannot overflow.
|
||||
Reference in new issue
Block a user