refactor(element): field types defined once in Go #165

Merged
dominik.polakovics merged 1 commit from afk/163 into main 2026-10-07 17:12:44 +02:00

Closes #163

What

Each of the eleven field types is now defined once, in its own file in internal/element (ftype_<name>.go). Every file implements the unexported fieldKind interface, and that one definition owns everything about the type:

  • declaring: which sidecar keys it takes (options, pageType, default), the scope rule (multiselect only in page-type fields), and the name binding (categories);
  • sanitizing: any spelling Hugo reads becomes the type's standard value, or "unreadable";
  • checking: the value rules and the site checks (page exists and has the right page type, categories exist);
  • writing: the front-matter form, the shortcode form, and a new element's initial value;
  • companions: the link's <name>Target setting;
  • uses: what modules and the element list may do with it (Uses, Compare, FilterMatch).

A type that lacks a job doesn't compile. TestFieldTypesMatchRegistry fails when FieldTypes() and the registry differ.

Every server path now goes through these definitions:

  • Checking: Schema.Check(values, site) replaces ValidateValues, CoerceValues, CheckPageParams and CheckCategoryParams. It runs for element saves, item rows, properties saves, page create and the rich-text body save. The caller hands in the site lookup, pageops.NewSite, which builds the page tree and reads the categories lazily.
  • Reading: FieldType.Read serves properties, the module list and the page date.
  • Writing: MergeParams / FrontMatterChanges write through Param.ShortcodeValue / Param.FrontMatterValue over Schema.Managed().
  • Modules: editormodule placement rules, the module sorter and filters, ElementSummary and LinkParamValues use Uses().

Behavior changes (as specified)

  • datetime: any spelling content.DateRFC3339 reads is accepted from any source, sidecar defaults included. A value without an offset is read as UTC, and the value is stored as a quoted RFC3339 string. default: next tuesday is a load error naming the file.
  • multiselect: a single string (tags: events) reads as ["events"] in properties and in the module list, and a properties save keeps it.
  • number: an unreadable value is no value. Properties show the default, else empty ("", the number input's empty value), and the module list leaves it out. A save never writes 0.
  • Body save: every inline element's declared params are checked against that element's sidecar (types, required, options, page exists and has the page type, categories exist). Errors name the inline element and the param. The params are stored in standard form, so start="2026-07-10" becomes "2026-07-10T00:00:00Z". Undeclared params are left untouched, as on an element save.
  • Page create: runs the page-field site check, so it rejects a seeded page field that names a missing page or a page of the wrong type.

Follow-on effects worth knowing

  • One sanitizer for every source. Submitted values in any spelling Hugo reads are now accepted. For example, "5" for a number, 5 for a text field, a single string for a multiselect, "a,b" for categories. Error messages for datetimes now say "must be a valid datetime, e.g. …".
  • Absent numbers. An absent number with no default now reads "" in properties, where it used to read 0.
  • Sub-second precision is dropped. content.DateRFC3339 formats with time.RFC3339, so a datetime with fractional seconds loses them.
  • Numeric datetimes. A bare number in a datetime is read as Unix seconds, exactly as Hugo's cast reads it.
  • Empty values in the module list. An empty stored value ("", or a categories list where every slug is stale) is now left out of module entries.
  • Error ordering. Field errors come from a single pass, at most one per key. Before, they were grouped by kind: type errors first, then page, then categories.
  • Untouched. The browser is unchanged; that is #164.

The issue's assumptions were checked:

  • DateRFC3339 uses the same cast layouts as Hugo's htime. The only difference is UTC instead of the site's time zone.
  • Hugo reads a single taxonomy string as one term (ToStringSlicePreserveString).
  • deckle-hosted calls none of the changed endpoints.

Verification

  • go build ./... and go vet ./... pass.
  • go test ./... passes.
  • Web: npm test passes all 1860 tests.
  • New tests:
    • per-type sanitizer tables, the registry check, and Check with a fake site;
    • datetime default spellings for element params and page-type fields;
    • regression tests for the multiselect and number reads (properties and the module list);
    • body saves with bad inline params (elemops and HTTP);
    • page create with a bad page field;
    • datetime spellings through settings and create, stored as a quoted string.

🤖 Generated with Claude Code

Closes #163 ## What Each of the eleven field types is now defined once, in its own file in `internal/element` (`ftype_<name>.go`). Every file implements the unexported `fieldKind` interface, and that one definition owns everything about the type: - **declaring:** which sidecar keys it takes (`options`, `pageType`, `default`), the scope rule (`multiselect` only in page-type fields), and the name binding (`categories`); - **sanitizing:** any spelling Hugo reads becomes the type's standard value, or "unreadable"; - **checking:** the value rules and the site checks (page exists and has the right page type, categories exist); - **writing:** the front-matter form, the shortcode form, and a new element's initial value; - **companions:** the link's `<name>Target` setting; - **uses:** what modules and the element list may do with it (`Uses`, `Compare`, `FilterMatch`). A type that lacks a job doesn't compile. `TestFieldTypesMatchRegistry` fails when `FieldTypes()` and the registry differ. Every server path now goes through these definitions: - **Checking:** `Schema.Check(values, site)` replaces `ValidateValues`, `CoerceValues`, `CheckPageParams` and `CheckCategoryParams`. It runs for element saves, item rows, properties saves, page create and the rich-text body save. The caller hands in the site lookup, `pageops.NewSite`, which builds the page tree and reads the categories lazily. - **Reading:** `FieldType.Read` serves properties, the module list and the page date. - **Writing:** `MergeParams` / `FrontMatterChanges` write through `Param.ShortcodeValue` / `Param.FrontMatterValue` over `Schema.Managed()`. - **Modules:** editormodule placement rules, the module sorter and filters, `ElementSummary` and `LinkParamValues` use `Uses()`. ## Behavior changes (as specified) - **`datetime`:** any spelling `content.DateRFC3339` reads is accepted from any source, sidecar defaults included. A value without an offset is read as UTC, and the value is stored as a quoted RFC3339 string. `default: next tuesday` is a load error naming the file. - **`multiselect`:** a single string (`tags: events`) reads as `["events"]` in properties and in the module list, and a properties save keeps it. - **`number`:** an unreadable value is no value. Properties show the default, else empty (`""`, the number input's empty value), and the module list leaves it out. A save never writes `0`. - **Body save:** every inline element's declared params are checked against that element's sidecar (types, required, options, page exists and has the page type, categories exist). Errors name the inline element and the param. The params are stored in standard form, so `start="2026-07-10"` becomes `"2026-07-10T00:00:00Z"`. Undeclared params are left untouched, as on an element save. - **Page create:** runs the page-field site check, so it rejects a seeded `page` field that names a missing page or a page of the wrong type. ## Follow-on effects worth knowing - **One sanitizer for every source.** Submitted values in any spelling Hugo reads are now accepted. For example, `"5"` for a number, `5` for a text field, a single string for a multiselect, `"a,b"` for categories. Error messages for datetimes now say "must be a valid datetime, e.g. …". - **Absent numbers.** An absent number with no default now reads `""` in properties, where it used to read `0`. - **Sub-second precision is dropped.** `content.DateRFC3339` formats with `time.RFC3339`, so a datetime with fractional seconds loses them. - **Numeric datetimes.** A bare number in a datetime is read as Unix seconds, exactly as Hugo's cast reads it. - **Empty values in the module list.** An empty stored value (`""`, or a categories list where every slug is stale) is now left out of module entries. - **Error ordering.** Field errors come from a single pass, at most one per key. Before, they were grouped by kind: type errors first, then page, then categories. - **Untouched.** The browser is unchanged; that is #164. The issue's assumptions were checked: - `DateRFC3339` uses the same cast layouts as Hugo's `htime`. The only difference is UTC instead of the site's time zone. - Hugo reads a single taxonomy string as one term (`ToStringSlicePreserveString`). - deckle-hosted calls none of the changed endpoints. ## Verification - `go build ./...` and `go vet ./...` pass. - `go test ./...` passes. - Web: `npm test` passes all 1860 tests. - New tests: - per-type sanitizer tables, the registry check, and `Check` with a fake site; - datetime default spellings for element params and page-type fields; - regression tests for the multiselect and number reads (properties and the module list); - body saves with bad inline params (elemops and HTTP); - page create with a bad `page` field; - datetime spellings through settings and create, stored as a quoted string. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
refactor(element): field types defined once in Go
All checks were successful
ci / build-and-test (pull_request) Successful in 9m3s
2e5816e8cc
Each of the eleven field types is now defined in one file of
internal/element (ftype_<name>.go) behind the unexported fieldKind
interface, which owns declaring, the sanitizer, the value rules, the
site checks, the stored forms, the initial and empty values, the link's
<name>Target companion, and what modules and the element list may do
with the type. A type missing a job does not compile, and a test fails
when FieldTypes() and the registry differ.

Every server path goes through it: Schema.Check (with a caller-supplied
Site, pageops.NewSite) replaces ValidateValues, CoerceValues,
CheckPageParams and CheckCategoryParams for element saves, item rows,
properties saves, page create and the rich-text body save, which now
checks and normalizes every inline element's declared params.
FieldType.Read serves properties, the module list and the page date.

Behavior changes, per the issue:
- datetime: any spelling content.DateRFC3339 reads, from any source,
  sidecar defaults included; no offset reads as UTC; stored RFC3339.
- multiselect: a single string reads as a one-item list.
- number: an unreadable value is no value, never 0.
- page create runs the page-field site check.

Docs: scaffold/site/AGENTS.md and README describe the new rules.

Closes #163

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Author
Owner

[autoland] verdict: pass

This was generated by AI while landing a PR.

PASS — validated, awaiting the human's merge go-ahead.

  • Signal relied on: Forgejo Actions ci / build-and-test (pull_request) green (run 151, 9m3s) — Go build/vet/test plus the web suite. Not re-run.
  • Conventions: Conventional Commits title; Closes #163 present; head afk/163 is current with main (no conflict).
  • CLAUDE.md rule: scaffold/site/AGENTS.md (and README) updated for datetime defaults in any spelling, single-string multiselect, and inline-element checks on body saves; drift test now reads FieldTypes().
  • Diff vs. #163: the eleven types live one-per-file behind fieldKind (internal/element/ftype_*.go), with a registry/order test. Schema.Check replaces ValidateValues/CoerceValues/CheckPageParams/CheckCategoryParams on element saves, item rows, properties saves, page create and inline body elements, through a lazy pageops.NewSite. Reads go through FieldType.Read (properties, module list, page date); writes go through ShortcodeValue/FrontMatterValue over Managed(). The Uses sets match the old editormodule/sorter/summary/LinkParamValues tables exactly. No per-type switches remain outside internal/element except built-in param declarations.
  • Cross-checks: the unchanged browser handles an absent number served as "" (renders empty, omits it on save). Chip insertion is configure-first, so the new inline check doesn't block fresh chips. The items-without-collection guard still runs before itemSchema.Check. deckle-hosted imports no deckle Go packages.

No CONCERNS. Documented, intended behaviour changes: sub-second datetime precision is dropped, a numeric datetime reads as Unix seconds, and an inline chip naming a since-deleted page now blocks its parent's body save until the chip is edited.

[autoland] verdict: pass > *This was generated by AI while landing a PR.* **PASS** — validated, awaiting the human's merge go-ahead. - **Signal relied on:** Forgejo Actions `ci / build-and-test (pull_request)` green (run 151, 9m3s) — Go build/vet/test plus the web suite. Not re-run. - **Conventions:** Conventional Commits title; `Closes #163` present; head `afk/163` is current with `main` (no conflict). - **CLAUDE.md rule:** `scaffold/site/AGENTS.md` (and README) updated for datetime defaults in any spelling, single-string multiselect, and inline-element checks on body saves; drift test now reads `FieldTypes()`. - **Diff vs. #163:** the eleven types live one-per-file behind `fieldKind` (`internal/element/ftype_*.go`), with a registry/order test. `Schema.Check` replaces ValidateValues/CoerceValues/CheckPageParams/CheckCategoryParams on element saves, item rows, properties saves, page create and inline body elements, through a lazy `pageops.NewSite`. Reads go through `FieldType.Read` (properties, module list, page date); writes go through `ShortcodeValue`/`FrontMatterValue` over `Managed()`. The `Uses` sets match the old editormodule/sorter/summary/LinkParamValues tables exactly. No per-type switches remain outside `internal/element` except built-in param declarations. - **Cross-checks:** the unchanged browser handles an absent number served as `""` (renders empty, omits it on save). Chip insertion is configure-first, so the new inline check doesn't block fresh chips. The items-without-collection guard still runs before `itemSchema.Check`. deckle-hosted imports no deckle Go packages. No CONCERNS. Documented, intended behaviour changes: sub-second datetime precision is dropped, a numeric datetime reads as Unix seconds, and an inline chip naming a since-deleted page now blocks its parent's body save until the chip is edited.
Sign in to join this conversation.
No reviewers
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
Cloonar/deckle!165
No description provided.