Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
44 changes: 44 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
name: CI

on:
pull_request:
types: [opened, synchronize, reopened]
branches:
- main

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
check:
name: Check
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

- name: 🛠️ Setup mise
uses: jdx/mise-action@c37c93293d6b742fc901e1406b8f764f6fb19dac # v2.4.4
with:
install: true
cache: true

- name: 📦 Install dependencies
run: pnpm install --frozen-lockfile

- name: 🧹 Lint and format
run: pnpm rr jsc

- name: 🔍 Check types
run: pnpm astro check

- name: 🏗️ Build site
run: pnpm astro build
33 changes: 33 additions & 0 deletions .github/workflows/workflow-audit.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
name: Workflow audit

on:
pull_request:
types: [opened, synchronize, reopened]
branches:
- main

permissions: {}

concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

jobs:
audit-workflows:
name: Audit workflows
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read
steps:
- name: ⬇️ Checkout repo
uses: actions/checkout@11d5960a326750d5838078e36cf38b85af677262 # v4.4.0
with:
persist-credentials: false

# Repo root, not .github/: keeps annotation paths repo-relative.
- name: 🔒 Scan workflows
env:
# Enables zizmor's online audits.
GH_TOKEN: ${{ github.token }}
run: pipx run --spec zizmor==1.30.1 zizmor --format=github .
7 changes: 7 additions & 0 deletions .github/zizmor.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
rules:
unpinned-uses:
config:
policies:
# First-party withvariablelab/gh-actions is tracked at @main on purpose.
"withvariablelab/*": ref-pin
"*": hash-pin
3 changes: 1 addition & 2 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,8 +1,7 @@
node_modules
dist
.astro
.wrangler
.dev.vars*
!.dev.vars.example

# editor / OS
.DS_Store
Expand Down
1 change: 0 additions & 1 deletion .node-version

This file was deleted.

14 changes: 11 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,16 +2,24 @@

The site at `lab.variable.land`: the Index of Variable Lab's Projects and a Project page for each. [README.md](./README.md) is for people and holds the commands; this file is for agents working in this repo.

Today `src/index.ts` is a placeholder Worker: it serves `/robots.txt` and redirects everything else. The site itself is designed and specified in `docs/`, and not built yet.
The site is static. `astro build` renders the pages in `src/pages` and the content in `src/content` into `dist/`, with `public/robots.txt` copied as is. The Worker has no script: [wrangler.jsonc](./wrangler.jsonc) serves `dist/` as assets, so how a path resolves, redirects or 404s is set there. The pages are designed and specified in `docs/`.

## Docs

- [docs/design.md](./docs/design.md): what to build. It covers the pages, their head and `og:image`, the rules they render by, every measurement, and what is left open. Read it before building or changing a page.
- [docs/design.md](./docs/design.md): what to build. It covers the pages, their head and `og:image`, the rules they render by, every measurement, and the implementation's decisions. Read it before building or changing a page.
- [docs/content-model.md](./docs/content-model.md): the entities and fields behind the pages, and how each field is written. Read it before storing, rendering or writing content.
- [docs/content.md](./docs/content.md): the approved content. Render it exactly as written. New copy needs the Keeper's approval.
- [docs/glossary.md](./docs/glossary.md): the site's terms. Use them when naming anything in code, content or copy.
- [docs/add-a-project.md](./docs/add-a-project.md): the steps to add a Project.

## Code

- Stored content is in `src/content/`, typed and checked by `src/content.config.ts`. Derived fields are computed in `src/lib/content.ts`, never stored.
- The fixed words that `docs/design.md` names are in `src/lib/labels.ts`; [design.md](./docs/design.md#decisions) lists them.
- Brand values come from `@variablelab/brand`, never typed by hand. CSS uses the `--vlab-*` custom properties of its `tokens.css`, which `src/styles/site.css` imports with Fontsource's IBM Plex. TypeScript (the Logo, the Class stamps, `theme-color` and the `og:image`s) reads them through `src/lib/brand.ts`. Code gets its colors from `src/lib/code.ts`, which the Project page and the `og:image`s share.
- The tools run by name through mise's PATH; the commands are in [README.md](./README.md#local-development). In a non-interactive shell mise's hook may not run: use `mise exec -- <command>`.
- For the visual check in [design.md](./docs/design.md#verifying), serve the build as the Worker does: `astro build`, then `wrangler dev` ([README.md](./README.md#local-development)).

## Brand

Brand rules come from the `vlab-brand` skill, which is expected to be installed globally. Before designing or writing anything public, check that it is available. If it isn't, install it with `npx skills add withvariablelab/brand --skill vlab-brand -g`.
Expand All @@ -23,5 +31,5 @@ The tokens, the Logo and the Class stamps ship in the `@variablelab/brand` packa
- Work from this directory. Everything this repo needs is in it or behind a public URL.
- A change that another repository needs, such as the routes of env's docs Worker, is flagged to the Keeper, not made from here.
- Docs and public copy are in English, written for any harness: say "agents", not one tool's name, and keep install commands free of agent flags.
- Copy lives in the content, never hardcoded in a page.
- Copy lives in `src/content/` and `src/lib/labels.ts`, never in a page, component or script.
- A page is done after the visual check in [design.md](./docs/design.md#verifying).
32 changes: 24 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

The site at [lab.variable.land](https://lab.variable.land): the Index of Variable Lab's apps and libraries, and a page for each one.

Until the Index is built, it serves `/robots.txt` and redirects everything else (302) to [github.com/withvariablelab](https://github.com/withvariablelab). The site is designed and specified in [docs/](./docs):
The site is static: [Astro](https://astro.build) builds it into `dist/`, and a Worker with no script serves `dist/` as [static assets](https://developers.cloudflare.com/workers/static-assets/). Each page is served without a trailing slash: `/<project>/` and `/<project>.html` redirect to `/<project>`, and an unknown path gets a 404. `/robots.txt` is `public/robots.txt`, copied as is. The site is designed and specified in [docs/](./docs):

| Doc | What it holds |
| --- | --- |
Expand Down Expand Up @@ -30,6 +30,8 @@ The pattern ends in `/*`, so `/<project>` itself, the Project page, still reache

`/robots.txt` lives here because it must sit at the root. It also lists the sitemaps of the Projects' docs.

This Worker serves `/<project>` with no redirect, and redirects `/<project>/` to `/<project>`. A Project's Worker may redirect `/<project>/` to `/<project>` too, as env's does. So www must never redirect the other way, from `/<project>` to `/<project>/`, or the two would loop: keep `build.format: 'file'` in `astro.config.mjs` and `html_handling: "drop-trailing-slash"` in `wrangler.jsonc`.

## Adding a Project

Follow [docs/add-a-project.md](./docs/add-a-project.md).
Expand All @@ -41,20 +43,34 @@ Deployed with [Workers Builds](https://developers.cloudflare.com/workers/ci-cd/b
| Setting | Value |
| --- | --- |
| Root directory | `/` |
| Build command | `pnpm exec astro build` |
| Build variable | `NODE_VERSION` = `24` |
| Deploy command | `pnpm exec wrangler deploy` |
| Non-production branch deploy command | `npx wrangler preview` |

`previews: {}` in `wrangler.jsonc` is required for the preview command.

Each branch copies these settings into its own preview trigger the first time it's pushed. A change to them applies only to branches pushed after it.
- The Build command is set in the dashboard: `build.command` in `wrangler.jsonc` doesn't run with `wrangler preview`.
- `NODE_VERSION` is there because Workers Builds doesn't read `mise.toml`.
- Settings belong to a trigger: one for production, and Previews Base for other branches. Each branch copies them into its own preview trigger the first time it's pushed, so a change applies only to branches pushed after it. A branch pushed before the change keeps its old settings until its own trigger is edited.
- `previews: {}` in `wrangler.jsonc` is required for the preview command.
- `preview_urls: true` in `wrangler.jsonc`, together with Preview turned on under the Worker's Domains → Worker URL, gives each branch its own Preview URL: `https://<branch>-variablelab-www.<subdomain>.workers.dev`.

## Local development

[mise](https://mise.jdx.dev) pins Node and pnpm (`mise.toml`, `mise.lock`) and puts `node_modules/.bin` on the PATH, so the tools run by name:

```sh
mise install
pnpm install
pnpm dev # wrangler dev
pnpm check # tsc
pnpm exec wrangler deploy --dry-run
astro dev # the site, rebuilt as you edit
rr jsc # format and lint with Biome; rr jsc --fix applies the fixes
astro check # types: .astro, .ts and the content schema
astro build # the site, into dist/
wrangler dev # dist/, served as the Worker serves it
wrangler deploy --dry-run
```

Run `pnpm types` after changing `wrangler.jsonc` to regenerate `worker-configuration.d.ts`.
In a shell where mise isn't activated, run them as `mise exec -- <command>`.

`astro check` is the type check: `rr check` stops at its type-check step, because rr has no plugin for Astro.

CI runs `pnpm rr jsc`, `pnpm astro check` and `pnpm astro build` on each pull request to `main`. It calls the tools through pnpm because mise's GitHub Action puts only mise's shims on the PATH, not `mise.toml`'s `[env]`.
11 changes: 11 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { defineConfig } from "astro/config";

export default defineConfig({
site: "https://lab.variable.land",
output: "static",
// Pages build to /<page>.html and are served at /<page>, with no trailing slash.
// wrangler.jsonc's html_handling ("drop-trailing-slash") redirects /<page>/ and /<page>.html there.
// The stylesheets are small: inlined, they cost no request before a page can paint.
build: { format: "file", inlineStylesheets: "always" },
trailingSlash: "never",
});
10 changes: 10 additions & 0 deletions biome.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
{
"$schema": "./node_modules/@biomejs/biome/configuration_schema.json",
"extends": ["@rrlab/biome-config"],
"html": {
"experimentalFullSupportEnabled": true,
"formatter": {
"enabled": true
}
}
}
8 changes: 4 additions & 4 deletions docs/add-a-project.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Add a Project

How a Project gets into the Index and gets its Project page. Each step ends when its check holds; the next step uses what the previous one left. Fields are those of [content-model.md](content-model.md), and the result is a new entry in [content.md](content.md).
How a Project gets into the Index and gets its Project page. Each step ends when its check holds; the next step uses what the previous one left. Fields are those of [content-model.md](content-model.md), and the result is a new entry in [content.md](content.md), with the same stored fields in `src/content/projects/<name>.yaml`, which the site is built from.

1. **It belongs.** It is Variable Lab's own, it is published, and it isn't a fork. Today portless (a fork) and gh-actions (not a Project) stay out, and nothing private gets in until it is published.
Check: it has a published Package and a public Repository.
Expand All @@ -17,11 +17,11 @@ How a Project gets into the Index and gets its Project page. Each step ends when
Check: the code is what the README documents or runs as is, and no line is longer than about 70 characters.
6. **Its shape.** Until it has a Sign, it shows its Class stamp. A Sign is drawn by the `vlab-brand` skill's `project-brand.md`, and it lives in the Project's repository as `brand/sign.svg`.
Check: the Sign passes the covered-names test, and its path is copied into `sign`.
7. **Its place.** Its position in the Index's `projects`, by the rule in [content-model.md](content-model.md#index).
7. **Its place.** Its position in the Index's `projects`, by the rule in [content-model.md](content-model.md#index), in [content.md](content.md) and in `src/content/index.yaml`.
Check: every Project's Also indexed still follows rule 3 in [design.md](design.md#rules).
8. **Its page and its `og:image`.** They come from its content: nothing is drawn by hand.
8. **Its page and its `og:image`.** They come from its content: `astro build` renders both, and stops when the content breaks a rule that `src/content.config.ts` checks. Nothing is drawn by hand.
Check: its title, description and `og:image` follow [design.md](design.md#head).
9. **Its docs, if it publishes its own.** The Project's own Worker takes everything under `lab.variable.land/<project>/`, with the route `{ "pattern": "lab.variable.land/<project>/*", "zone_name": "variable.land" }`. The pattern ends in `/*`, so the Project page at `lab.variable.land/<project>` stays with www; `lab.variable.land/<project>*` would take it too. A landing the Project had before redirects to its Project page. www's `/robots.txt` lists the docs' sitemap, and any crawl rule they need, such as disallowing their API.
9. **Its docs, if it publishes its own.** The Project's own Worker takes everything under `lab.variable.land/<project>/`, with the route `{ "pattern": "lab.variable.land/<project>/*", "zone_name": "variable.land" }`. The pattern ends in `/*`, so the Project page at `lab.variable.land/<project>` stays with www; `lab.variable.land/<project>*` would take it too. A landing the Project had before redirects to its Project page. www's `/robots.txt` (`public/robots.txt`) lists the docs' sitemap, and any crawl rule they need, such as disallowing their API.
Check: `lab.variable.land/<project>` shows the Project page from www, a path under `lab.variable.land/<project>/` shows the Project's docs, and `/robots.txt` names that sitemap.
10. **Its README.** The Project's README copies its Tagline, word for word.
Check: the Tagline in the README matches `tagline`.
Expand Down
6 changes: 3 additions & 3 deletions docs/content-model.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,8 @@ One. The page at `/`.
| projects | stored | The Projects in order. The page groups them by Class, Apps first; within a Class they keep this order. A new Project goes last in its Class unless the Keeper places it. |
| description | stored | The head description. |
| address | derived | `lab.variable.land`. |
| title | derived | `Variable Land`. |
| ogAlt | derived | `Variable Land — {intro}`. |
| title | derived | `Variable Lab`. |
| ogAlt | derived | `Variable Lab — {intro}`. |

## Keeper

Expand Down Expand Up @@ -53,7 +53,7 @@ One per Project in the Index. Every Project has a Project page at `lab.variable.
| description | stored | The head description, at most 160 characters. | The Tagline plus one sentence of the Body. |
| npm | derived | `https://www.npmjs.com/package/{package}`. | |
| address | derived | `lab.variable.land/{name}`. | |
| title | derived | `{name} · Variable Land`. | |
| title | derived | `{name} · Variable Lab`. | |
| ogAlt | derived | `{name} — {tagline}`. | |
| publishedAs | derived | A Library shows `{package}`; an App shows `{executable} · {package}`. | |
| alsoIndexed | derived | Up to three other Projects: first those of the same Class, then the other Class, each group in Index order. | |
Expand Down
4 changes: 2 additions & 2 deletions docs/content.md
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ pnpm rr pack
| Field | Value |
| --- | --- |
| class | App |
| sign | `M2 2h8v6h6v8h6v6H2V2z` |
| sign | `M2 2h8v8h4v4h8v8H2V2z` |
| tagline | `Starts a project in Variable Land.` |
| body | `A library, a backend or a monorepo. Every template targets Node, uses pnpm and ships rr as the one entry point for lint, format, type-check and build.` |
| installCommand | `pnpm add -g @variablelab/vland` |
Expand Down Expand Up @@ -87,7 +87,7 @@ vland init my-app -t backend --no-install --no-git
| Field | Value |
| --- | --- |
| class | Library |
| sign | `M2 2h20v20H2V2zm4 6h10v4H6V8z` |
| sign | `M2 2h20v20H2V2zm4 8h12v4H6v-4z` |
| tagline | `Only declared variables, checked at boot.` |
| body | `One Standard Schema contract. Files and the environment, resolved at boot. If a value is missing or malformed, the process stops before your code reads it.` |
| installCommand | `pnpm add @variablelab/env` |
Expand Down
Loading
Loading