Work & Transmissions
Work is CH 02 in the channel index, and each project page is CH 03. The theme calls a project a transmission, and the vocabulary is consistent enough that it is worth internalizing: the archive is the log, an entry is a transmission, and its number is its position in the broadcast order.
Six transmissions ship with the theme, and one of them — NEON_FLUX — carries the copy the Figma file actually draws. The other five are demo copy written in the same voice.
The index
/work/ is a thin route shell. It reads getWork(), hands the whole array to TransmissionGrid, and adds a ContactRow beneath it:
const entries = await getWork();
// …
<TvSet>
<TvScreenBar slot="screen-top" channel={channelFor(page)} label={page} />
<FastextBar slot="screen-bottom" current={page} />
<div class="flex flex-col gap-[max(20px,calc(20*var(--tv-u)))]">
<TransmissionGrid {entries} />
<ContactRow />
</div>
</TvSet>
Two things in that snippet are the theme’s shape rather than incidental. channelFor(page) looks the channel number up from navData instead of hard-coding "02", and it throws at build time if no channel carries that label — a page tuned to nothing fails the build rather than shipping. And the gap is max(20px, calc(20 * var(--tv-u))), which is the theme’s standard responsive expression: the drawn desktop value scaled by the cabinet unit, floored at the mobile frame’s own measurement. The TV Set explains where --tv-u comes from.
The page’s meta description is derived, not typed: it interpolates entries.length and the author’s role, so it stays accurate as the archive grows.
The project page
/work/<slug>/ composes four sections from Sections/Work/ plus the shared ones from Global/:
| Section | Draws |
|---|---|
ProjectBriefing |
the BRIEFING paragraphs and HIGHLIGHTS list |
ProjectGallery |
the gallery stills |
ProjectActions |
the demo and source buttons |
TransmissionGrid |
reused on the index |
The spec strip, title block, pull quote and figure come from Global/, because a second page draws each of them — that is the promotion rule from the section contract, applied.
The button pair
demoUrl and sourceUrl are both optional, and the reason is stated in the schema: omit for work with no public build — the page then draws one button, not a dead one. This is a small thing that a lot of portfolio themes get wrong by rendering a disabled button or a # link. Here an absent field means an absent button.
The gallery
gallery is an array of images, defaulting to [], so a project with no stills simply does not draw the block. Images go through astro:assets via the schema’s image() helper, which means they are optimized at build time with real dimensions — and a missing file is a build error rather than a broken image at runtime.
Ordering and numbering
order is an explicit positive integer, and getWork() sorts on it ascending. The number a page prints — “TRANSMISSION 01” — is the entry’s index in that sorted array, computed at the point of drawing rather than stored.
That matters when you reorder. Changing an entry’s order renumbers the display everywhere at once: the grid, the project page, and the home page’s recent rows. You never edit a number.
The schema comment names why order exists at all rather than sorting on year: a project’s year is editorial. "2024–25" is a legal value, and a string sort over free-form years would silently reorder the archive the day someone writes a range.
Why the markdown body is unused
Open any entry in src/data/work/ and you will find the frontmatter carries everything and the body is empty. That is deliberate.
The BRIEFING paragraphs live in a briefing: string[] array. The HIGHLIGHTS live in highlights: string[]. The reason is the same one that keeps the blog’s furniture out of its body: every string on a project page is drawn inside the tube’s own type scale, and structured fields keep it there. A ## heading typed into a markdown body would render in the document’s type, not the screen’s, and it would break the illusion in a way that is hard to fix without overriding the prose styles per-page.
The source names the point at which to change this: a long-form case study is when you would start rendering the body. If you want that, render <Content /> inside a container that applies the tube’s type classes and accept that you now own the mapping.
Structured data
Each project page emits a CreativeWork node via getCreativeWorkSchema, alongside the site-level Organization and WebSite graph that BaseHead emits everywhere. The rule the theme follows — and it is worth adopting — is that schema must never state what the markup does not draw. The CV page, for instance, emits a Person node with no image field precisely because the CV frame draws no portrait.
Adding a transmission
One folder, one file, its images beside it:
src/data/work/my-project/
├── index.md
└── preview.webp
---
title: MY_PROJECT
description: One line — the card copy and the page standfirst.
year: "2026"
order: 7
stack: [Astro, TypeScript]
role: Solo — design & build
timeline: Jan — Mar 2026
status: Shipped · Live
preview: ./preview.webp
briefing:
- The problem, in a paragraph.
- What was built, in a paragraph.
highlights:
- A measured result
demoUrl: https://example.com
---
Nothing else. The entry gets its route, appears in the grid, and shows up in the home page’s recent rows — because all three read the same list. If the frontmatter is wrong, the build fails and names the entry.