No description
Find a file
2026-11-01 21:20:21 +00:00
src Content v0.3.0: madeToOrder and product variants 2026-11-01 21:20:21 +00:00
test Content v0.3.0: madeToOrder and product variants 2026-11-01 21:20:21 +00:00
.gitignore Content model v0.1.0: entries, add-ons, views, visibility, sources 2026-10-19 04:32:56 +00:00
.woodpecker.yml Content model v0.1.0: entries, add-ons, views, visibility, sources 2026-10-19 04:32:56 +00:00
package-lock.json Content v0.3.0: madeToOrder and product variants 2026-11-01 21:20:21 +00:00
package.json Content v0.3.0: madeToOrder and product variants 2026-11-01 21:20:21 +00:00
README.md Content v0.3.0: madeToOrder and product variants 2026-11-01 21:20:21 +00:00

@wl.cx/content

Content model shared by the wl.cx sites (template: alice/landing), their local dev admin, and ctrl.

media    originals in Git LFS + .meta.json with a stable 8-char `id`
entries  src/content/entries/<slug>/index.md — one shape, `type` picks the layout
add-ons  optional frontmatter blocks: artwork{}, product{}, wall
views    filter + sort + layout over entries; blog, wall, shop, catalogue built in

Install

Pin a tag via Forgejo's archive URL. It works without git in the CI image, unlike git+https:

"@wl.cx/content": "https://git.wl.cx/wl.cx/content/archive/v0.3.0.tar.gz"

A tag is a release. Bump the URL to upgrade.

Use

import { normalizeEntry, normalizeView, applyView, paginate } from '@wl.cx/content';
import { createFsSource } from '@wl.cx/content/fs';           // Node only
import { loadEntries, loadViews } from '@wl.cx/content';

const source = createFsSource(process.cwd());
const entries = await loadEntries(source);
const views = await loadViews(source);

The main export is browser-safe. @wl.cx/content/fs needs Node.

A source is any object with async read, write, list and remove on repo-relative paths (see src/source.js). ctrl provides one backed by the Forgejo API, so it validates content with the same code the build uses.

Entry

---
type: artwork                 # post | photo | artwork | page   (default post)
title: Haus Elfenwinkel
date: 2026-10-19
images:
  - media: k7f2q9ab           # media id; a plain string works too
  - media: a91xz3cd
    role: source
cover: k7f2q9ab               # default: first image
tags: [installation, hexenhaus]
visibility: public            # public | unlisted | draft
excerpt: …
layout: artwork               # default: type
nav: true                     # or a label
artwork: { medium: oil on linen, dimensions: 80 × 60 cm, year: 2019 }
product: { price: 450, currency: EUR, availability: in_stock, edition: unique }
wall: true                    # or { caption: … }
---
Markdown body.

Invalid content throws ContentError, so the build fails and ctrl refuses to commit it.

artwork uses short names. Outputs map them to schema.org: medium → artMedium, dimensions → height/width/depth, year → dateCreated.

Product add-on

product marks an entry as for sale and puts it in the built-in shop view. Every field that existed before keeps working; madeToOrder and variants are new in 0.3.0 and both are optional.

field type default notes
price number ≥ 0 required numeric strings accepted, as before; the base price when variants is empty
currency string EUR
sku string ''
availability in_stock | sold | preorder in_stock
edition string '' free text, e.g. unique, 1/25
shipping string '' free text
madeToOrder boolean false produced after the order (Printful) instead of shipped from stock; only real booleans, "yes" fails the build
variants list of variant mappings [] size/colour combinations; empty means one form, priced by price

A variant is a mapping with at most these keys — unknown keys are rejected, not silently dropped:

key type notes
size string, trimmed
color string, trimmed
sku string, trimmed
price finite number ≥ 0 this variant's price; consumers fall back to product.price when absent
printfulVariantId positive integer Printful's variant id for fulfilment

Rules, all enforced with ContentError:

  • blank size/color/sku strings are dropped from the output, not kept as ''
  • price and printfulVariantId inside a variant are strict: price: "34.50" is an error (the top-level product.price still accepts numeric strings for compatibility), and printfulVariantId must be an integer, not a string
  • every variant needs at least one of size, color, sku, printfulVariantId; a price alone does not identify anything
  • at most 100 variants. VARIANT_KEYS and MAX_VARIANTS are exported
---
type: artwork
title: Frei Hoodie
date: 2026-11-01
images: [{ media: m4k2p9xq }]
product:
  price: 42
  currency: EUR
  availability: preorder
  madeToOrder: true
  variants:
    - { size: S, color: white, price: 42, printfulVariantId: 4011 }
    - { size: M, color: white, price: 42, printfulVariantId: 4012 }
    - { size: L, color: black, price: 44, sku: HOOD-L-BLK, printfulVariantId: 4020 }
---

Unknown keys

Frontmatter keys the model does not know are kept verbatim in entry.extra (unchanged since 0.1.0) — the marie wall's takenAt is one. ctrl relies on this: its entry editor clones the raw frontmatter and rewrites only the fields it owns, so an unmodelled block — a Printful snapshot, an import marker — survives a save and a round trip through normalizeEntry. Unknown keys inside product are not preserved; product is a modelled shape and only the fields above come out.

Visibility

views, nav, sitemap, feeds rendered at its URL
public yes yes
unlisted no yes, noindex — must not be mentioned publicly
embedded no (may be given by a view's entries: list) yes, noindex
draft no no

Views

Built in: blog (posts), wall (wall add-on), shop (product), catalogue (artwork). Nav entries are opt-in. A site overrides or adds views in src/content/views/<name>.md:

---
title: Garten
filter: { tags: [garten] }    # also: type, has (add-on names)
sort: date-desc               # date-asc | title | year-desc | year-asc | manual
layout: grid                  # list | grid | wall | sections
pageSize: 24                  # 0 = one page
nav: true
# entries: [slug, slug]       # explicit list instead of a filter; implies sort: manual
---
Intro text.

Develop

npm install
npm test        # node:test, no extra deps