This project keeps worksheet content in Markdown and centralizes repeated HTML in Hugo shortcodes and partials. The generated pages remain relative-link friendly, printable, accessible without JavaScript, and compatible with the submission script through stable data-key values.
A Hugo migration does not need to reproduce the source page byte for byte. Whitespace, wrapper elements, generated IDs, CSS classes, attribute order, and minor visual details may change when Hugo, Goldmark, or a shared renderer can express the same page more simply.
Judge a migration by the behavior people and integrations depend on:
- the same authored information and interactive fields remain available;
- submission
data-keyvalues and initial field values remain compatible; - glossary terms, senses, mission references, and page-data payloads still resolve;
- links and published assets still reach the same targets from every supported mount point;
- controls remain labelled, IDs remain unique, and keyboard/screen-reader behavior remains sound;
- content intended for print or screen retains that visibility;
- essential visible meaning is preserved, even when markup or low-value styling is normalized.
Byte-identical output is required only when an explicit contract demands it-for example, a snapshot specifically designated as canonical or an external consumer that parses an exact fragment. Do not treat a raw HTML diff by itself as a migration failure.
Before adding markup or a new component, use this order:
- Prefer ordinary Markdown for prose, lists, static tables, and standard fenced code.
- Reuse an existing shortcode when its authoring meaning fits.
- Compose an existing partial behind a thin semantic shortcode when the content deserves a distinct public name but not a distinct renderer.
- Extend a shared partial or shortcode only when the new option is broadly useful and does not weaken its contract.
- Add a new partial for genuinely reusable internal rendering behavior, or a new shortcode for a genuinely new content-authoring concept.
Search layouts/_shortcodes/, layouts/_partials/, and this catalog before implementing a new component. Prefer a small semantic alias over copied HTML. Avoid generic variant parameters when an established semantic shortcode communicates intent more clearly.
Shortcodes are the public API used by Markdown authors. Partials are internal building blocks used by layouts and shortcodes; content files should not call them directly.
- A field's
keybecomes itsdata-key. Treat it as stored data: do not rename it casually. - Form-control IDs are generated from keys by changing underscores to hyphens. Do not author IDs.
- Every text input needs either a visible label or an accessible name derived by its renderer.
- Site links and assets are emitted as relative URLs so builds work at a domain root, under a project path, and from local files.
- Hugo content links use lowercase page references (for example,
/labs,/python_labs, and/glossary) and resolve to page objects at build time. Use literal paths only for legacy static pages that are not managed by Hugo. .no-printis the shared way to hide interactive-only material on paper.
These are compatibility boundaries, not a demand for identical DOM structure. When one of them intentionally changes, document the migration and update the relevant consumer or test.
Introductory uses clean Hugo section URLs and no legacy aliases:
content/introductory/_index.md
content/introductory/coding/_index.md
content/introductory/coding/project-01.md ... project-17.md
content/introductory/ev3/_index.md
content/introductory/ev3/project-01.md ... project-17.md
content/introductory/ev3/builds/arm.md
content/introductory/ev3/builds/claw.md
content/introductory/spike/_index.md
content/introductory/spike/project-01.md ... project-17.md
content/introductory/spike/builds/arm.md
content/introductory/spike/builds/claw.md
content/introductory/systems/_index.md
content/introductory/systems/project-01.md ... project-14.md
Every project uses type: introductory implicitly from its section and is
validated at build time. The required project front matter is:
| Field | Contract |
|---|---|
title, short_title, description, weight |
Page, navigation, and hub-card text/order. Weight equals the project number. |
mission_id |
Exact persistence identifier: introductory_coding_NN (Wombat), introductory_ev3_coding_NN, introductory_spike_coding_NN, or introductory_systems_NN. |
styles |
Exactly site-base, worksheet, syntax, introductory, print, in that order. |
project_number, strand |
Numeric project identity and either coding or systems. Coding projects also set platform: wombat, ev3, or spike. |
phase, phase_order, time |
Hub grouping plus worksheet metadata. |
meta |
Labelled definition rows and exactly one What You Need row whose checklist items each contain stable key and accessible label values. |
eyebrow, heading, subheading, credit |
Shared worksheet hero and footer content. |
Optional card fields are hub_title, mission_label, no_mission, build,
and a pace mapping with kind (required, suggested, or anytime) and
label. A Coding page may place a build_gate after its phase; supply either
page/label or a links list of page/label pairs. Each target is a Hugo
page reference and therefore fails the build if it is missing.
The root and strand hubs derive cards, phase groups, counts, links, badges, and
gates from section children rather than maintaining a separate project list.
Introductory-owned extracted diagrams are colocated with the Markdown files in
their content section and referenced by bare filename. Shared images remain
below static/img/; assembly sequences live in static/img/kit/assembly/.
Figure source paths are resolved through the existing
figrow/figure-grid.html path, so project Markdown does not introduce a
Introductory-only image renderer.
Introductory phase headings use one Markdown convention:
## Try It - Observe the Wombat
## Learn It - Understand the Sensor
## Do It - Test Your Program
## Score It - Check the ResultOnly headings present in the authored source are rendered; Score It is not
added automatically. The checked-in Stage 1 project pages are explicit
stage1_fixture pages that exercise the infrastructure before Stage 2 replaces
their bodies with migrated curriculum.
Reuse these author-facing APIs from Markdown:
| Shortcode | Purpose and reuse guidance |
|---|---|
answer |
Standalone response textarea. Reuses the shared textarea renderer. |
ask |
Question plus response textarea; use when the prompt and answer belong together. |
calc |
Ordered prose, formula, equation-input, and note parts in one calculation panel. |
callout |
General red/navy/gold panel. Prefer a semantic panel alias below when one fits. |
checklist |
YAML checkbox list with stable submission keys and shared tick styling. |
code |
Chroma code with optional filename and @@...@@ teaching emphasis. Use a normal fence without those features. |
concept |
Structured concept explanation containing ordered text, code, truth, formula, or image parts. |
plsec / endplsec |
Paired PreLab section shell. Keep widgets between the pair at document top level. |
figrow |
Captioned, zoomable image row; also supports PreLab inventory checks through check_id. |
gate |
Semantic red panel for PreLab readiness/next-step gates. |
gridtable |
Explicit YAML columns and rows with static or editable cells. |
namebar |
Parameterless reflection name/date fields with fixed compatibility keys. |
mission-summary |
Canonical mission tier summary resolved by mission number or page reference; its optional YAML body adds tier status annotations. |
rec |
YAML list of visibly labelled recording fields. |
repeattable |
Generated rows with predictable <prefix><row>_<key> submission keys. |
resetbox |
Semantic gold panel for error-reset checkpoints. |
rule-definition |
Emits one canonical competition definition from data/glossary.yaml; rules definitions only. |
safety |
Semantic red safety panel; optionally screen-only with noprint=true. |
score-examples |
Required scores and does_not_score YAML lists rendered as a responsive comparison. |
short-answer |
One-line response using the shared labelled text-input renderer; requires key and label, with optional placeholder or Markdown prompt. |
signoff |
PreLab completion check plus labelled team/date-style fields. |
sketch |
Printable field-sketch area using the field SVG for the chosen start-box side. |
steps |
Generated numbered single-line response fields. |
warn |
Semantic red caution panel for drift or values that may change. |
widgetstep |
Semantic navy walkthrough panel that is always screen-only. |
zonebar |
Parameterless shared acceleration/cruise/deceleration diagram. |
Use ordinary Markdown instead of a shortcode for static tables, ordinary prose/lists, and code fences that do not need filenames or token emphasis.
Partials are internal APIs. Reuse them from layouts or shortcode implementations rather than copying their markup or logic.
| Partial | Role and reuse guidance |
|---|---|
asset.html |
Publishes an assets/ resource and returns its URL relative to the source page. Use it for processed CSS or other Hugo resources. |
botnav.html |
Previous, next, section, and glossary navigation for worksheet pages. |
checkbox.html |
Validated keyed checkbox plus associated label; derives the DOM ID from the key. |
codeblock.html |
Shared language validation, Chroma rendering, shortcode emphasis, fence options/classes, and filename tabs. Route every code path through it. |
document-shell.html |
Shared two-column document shell for a heading-derived sidebar and existing main content. |
document-sidebar.html |
Validates sidebar configuration and renders desktop/mobile navigation from .Fragments.Headings. |
figure-grid.html |
Normalized, keyboard-operable figure grid used by figrow and mission page resources. |
glossary-entry.html |
Validates and resolves a glossary base term/sense with track-specific wording. |
glossary-page-meta.html |
Produces label, class, rank, and sort metadata for glossary usage links. |
glossary-page-terms.html |
Discovers the base glossary terms referenced by a page using the shared parser. |
head.html |
Shared document metadata, fonts, and stylesheet links. |
hub-nav.html |
Hub-page navigation shell; delegates individual data-driven links to navigation-link.html. |
mission-summary.html |
Renders canonical mission tiers for the mission-summary shortcode, with optional authored status annotations. |
mission-tier-title.html / mission-judging-label.html |
Shared canonical display labels used by both mission pages and Introductory summaries. |
navigation-link.html |
Shared navigation anchor with configurable active class and consistent aria-current. |
overlays.html |
Emits glossary and figure dialogs only when the rendered page used them. |
page-data.html |
Serializes only the glossary definitions referenced by the current page. |
panel.html |
Shared red/navy/gold panel furniture used by all semantic panel shortcodes. |
parse-reference.html |
Parses glossary and mission token syntax, including display labels and qualifiers. |
relative-url.html |
Returns a relative URL from a source page to either a target page or site-relative path. Route internal page, image, script, and asset URLs through it. |
require.html |
Reads a required shortcode parameter and reports a source-positioned build error when absent. |
resolve-reference.html |
Dispatches a parsed reference to glossary or mission validation/resolution. |
sectionize.html |
Wraps rendered worksheet phases in sections without requiring raw wrapper HTML in Markdown. |
table-cell.html |
Shared static, number, and accessible input-cell renderer for both table shortcodes. |
score-comparison.html |
Shared responsive Scores / Does Not Score renderer. |
termify.html |
Rewrites rendered glossary tokens into popup controls and mission tokens into validated page links. |
text-input.html |
Validated visibly labelled text field used by recording, sign-off, and name/date components. |
textarea.html |
Validated accessible response textarea used by answer and ask. |
topbar.html |
Worksheet top bar, main navigation, PIN field, and submission controls. |
truthtable.html |
Core structured truth-table renderer used by concept content. |
validate-introductory-project.html |
Enforces Introductory filenames, numbering, persistence IDs, stylesheet order, phase/time fields, and the structured What You Need checklist. |
worksheet-metadata.html |
Renders definition rows and structured metadata checklists through the shared glossary and checkbox paths. |
When adding a semantic alias around an existing partial, keep the alias thin: collect/validate only its distinct authoring parameters, then delegate rendering.
Panel shortcodes share one renderer and consistent title markup. Their semantic names remain useful in source:
{{% callout title="Core Insight" %}}
The default callout is red.
{{% /callout %}}
{{% callout title="Think it through" variant="navy" %}}
Navy and gold are the two optional callout modifiers.
{{% /callout %}}
{{% safety title="⚠ Test in your hands first" noprint=true %}}
Hold the robot clear of the floor.
{{% /safety %}}| Shortcode | Parameters | Rendering behavior |
|---|---|---|
callout |
optional title; optional variant="navy" or variant="gold" |
red by default |
safety |
optional title; optional noprint=true |
red |
warn |
optional title |
red |
widgetstep |
optional title |
navy and always screen-only |
gate |
required title |
red |
resetbox |
required title |
gold |
Use % delimiters for panel bodies so Markdown inside the panel is rendered.
All response textareas use one standard height.
{{< ask key="p3_predict" label="Prediction" n=1 >}}
What do you predict will happen?
{{< /ask >}}
{{< answer key="ext_a" label="Extension A" placeholder="Write here" >}}ask requires key and label; n is optional. answer requires key and label; placeholder is optional.
Recording fields use YAML with key, label, and an optional placeholder:
{{< rec >}}
- key: rec_ip
label: Our Wombat's IP address
placeholder: e.g. 192.168.x.x
- key: rec_name
label: Our Wombat's name or number
{{< /rec >}}namebar is parameterless and emits the stable keys reflect_name and reflect_date.
All checklists use the tick presentation.
{{< checklist >}}
- key: p5_test_drive
label: "`Drive()` moved the measured distance"
- key: p5_test_arm
label: "The arm moved smoothly and safely"
{{< /checklist >}}Labels support inline Markdown and glossary references.
{{< signoff >}}
check:
key: done_all
label: Every box above is ticked honestly.
fields:
- key: rec_team
label: Team name or number
- key: rec_date
label: Date completed
{{< /signoff >}}The sign-off title is fixed as "Sign off."
Use an ordinary Markdown table for static reference information. Tables without an authored class receive the worksheet table styling automatically.
| Command | What it does |
| --- | --- |
| `ao()` | Turns every motor off. |
| `msleep(ms)` | Pauses for a number of milliseconds. |repeattable requires count and prefix; caption is optional. Its body is a YAML list of columns.
{{< repeattable count=4 prefix="trial" caption="Run log" >}}
- kind: number
head: Try
width: 8%
- head: What changed
key: changed
width: 35%
- head: What happened
key: result
aria: observed result
{{< /repeattable >}}A number column uses kind: number, head, and optional width. Input columns accept:
| Field | Meaning |
|---|---|
key |
required key suffix; row 2 becomes <prefix>2_<key> |
head |
column heading and default accessible name |
width |
optional CSS width |
aria |
optional accessible-name override |
align |
optional input text alignment |
example |
value in a worked example row above the answer rows |
seed |
static value replacing the first row's input |
Repeated input accessible names append the row number.
gridtable accepts only an optional caption parameter. Its YAML contains columns and rows.
{{< gridtable caption="Sensor readings" >}}
columns:
- head: Surface
width: 40%
- head: Reading
- head: Unit
rows:
- - text: White floor
- key: white_reading
placeholder: analog value
- key: white_unit
value: ticks
- class: blind
cells:
- text: 1 inch (blind spot)
- key: blind_reading
- text: ticks
{{< /gridtable >}}Static cells are {text: ...}. Input cells require key and optionally accept aria, placeholder, value, and align. A row can be a cell list or a map with class and cells, which supports cases such as the blind row treatment.
Input labels fall back in this order:
- the cell's explicit
aria; - the column heading;
- the row's first static text cell;
- the humanized key.
If every column heading is empty, the renderer omits <thead> and uses a <colgroup> for widths.
calc requires title. Add noprint=true only when the activity must remain screen-only. Its YAML is an ordered list; each item contains exactly one of prose, formula, equation, or note.
{{< calc title="Check the math yourself" >}}
- prose: Copy the two values from your table.
- formula: "ticks_per_inch = ticks ÷ inches"
- equation:
- input: {key: cal_ticks, placeholder: ticks}
- text: "÷"
- input: {key: cal_inches, placeholder: inches}
- text: "="
- input: {key: cal_result, aria: result}
- note: Does your hand calculation match the program?
{{< /calc >}}Equation tokens contain either text or input. An input requires key and may provide placeholder and aria. Its accessible name falls back from aria to placeholder to a humanized key.
Use the code shortcode for tutorial token emphasis or a filename tab:
{{< code lang="c" filename="main.c" >}}
int speed = @@750@@;
{{< /code >}}lang defaults to the page's track. filename is optional. Wrap tutorial-emphasis text in @@...@@; copied code contains only the inner text.
Use ordinary fenced blocks when emphasis and a filename are unnecessary:
```python
def drive(speed):
motor(0, speed)
```Fences support Goldmark classes and Chroma options. Console output uses a text fence with the console class:
```text {.console}
> Run started. Score: 0
> Final score: 40
```Every block must have a valid Chroma language. Emphasis markers are interpreted only in code shortcode and concept code content.
figrow infers its columns from the figure count. Each figure requires src and alt; alt is also its visible caption. check_id remains available for PreLab inventory overlays and produces the stable key part_<check_id>. Images are rendered inside native buttons, so Enter and Space work without custom keyboard handlers. Closing the shared zoom dialog returns focus to that button.
{{< figrow >}}
- src: kit/controller.jpg
alt: KIPR Wombat controller
check_id: wombat-controller
- src: kit/motor.jpg
alt: Motor and cable
{{< /figrow >}}The acceleration/cruise/deceleration bar has fixed shared content:
{{< zonebar >}}sketch requires aria, tag, and startbox="left" or startbox="right"; note is optional. The start-box side selects b3x_lsb.svg or b3x_rsb.svg from static/img/field.
concept keeps its positional title and ordered YAML part model. Supported parts remain text, code, truth, formula, and image.
{{< concept "A named number" >}}
- text: An integer stores a whole number.
- code: |
int speed = @@750@@;
- formula: "distance = speed × time"
{{< /concept >}}The plsec/endplsec pair, concept, overlays, asset publishing, section splitting, required-parameter helper, and core truth-table renderer retain their specialized structure.
The shared reference parser accepts:
| Syntax | Meaning |
|---|---|
[[ENCODER]] |
glossary entry |
[[ENCODER|encoders]] |
glossary entry with display label |
[[PROTOTYPE:design|prototype]] |
named glossary sense |
[[@2|Mission 2]] |
mission reference |
[[@2:bonus|Mission 2]] |
mission tier reference |
Unknown terms, senses, missions, and tiers fail the build. Mission references resolve from the mission page collection-not a parallel data file-and link to the stable #base, #bonus, or #advanced tier anchor. Page data contains only glossary references actually used on that page, with Python wording selected when track: python is set.
The 2027 Explorer is ordinary Hugo content with no URL overrides or aliases:
content/botball_explorer_2027/
├── _index.md
├── rules.md
└── missions/
├── mission-1/
│ ├── index.md
│ └── mission.mp4
└── mission-18/
├── index.md
└── mission.mp4
Do not add missions/_index.md. The Explorer root is the only mission index, and each mission is a leaf bundle whose explanatory video is a page resource.
Mission front matter is the source of truth for index cards, tier cards, video, and scoring summaries:
title: "Mission 1 - Waypoint Alpha"
linkTitle: "Waypoint Alpha"
layout: mission
nav: missions
hub: true
body_class: explorer
styles: ["site-base", "hub", "explorer"]
mission_number: 1
weight: 1
skill: "Basic autonomous navigation and stopping at a specified location."
video: mission.mp4
tiers:
- id: base
points: 1
difficulty: 1
judging: live
description: "A robot stops while [[IN THE ZONE]]."
- id: bonus
points: 1
difficulty: 1
judging: live
description: "The robot returns [[FULLY WITHIN]] a starting box."Mission rendering validates the mission number, bundle name, weight, tier order, point and difficulty ranges, judging mode, and every named page resource. The root build additionally requires the unique mission numbers 1 through 18.
Every mission body contains the comparison and ordinary Judge Notes:
{{< score-examples >}}
scores:
- "A robot enters and visibly stops."
does_not_score:
- "The robot drives through without stopping."
{{< /score-examples >}}
## Judge Notes
- Judges must clearly observe the stop.Competition definitions are maintained canonical data in data/glossary.yaml with source: rules. Rules Markdown supplies the heading, examples, and notes while definition prose comes from data:
### Touching
{{< rule-definition term="TOUCHING" >}}The former extraction script, data/missions.yaml, field-popup payload, and window.KIPR_MISSIONS are intentionally absent. Never regenerate curated rule definitions from legacy HTML.
Pages opt into the heading-derived sidebar in front matter:
sidebar:
title: "Rule Sections"
start_level: 2
end_level: 2
numbered: trueLevels must be integers from 1 through 6 and the end cannot precede the start. Enabling the sidebar without a heading in that range fails the build. Labels and targets always come from Hugo's heading fragments; do not author a second navigation list. Desktop uses the sticky aside, mobile uses a closed <details> menu, and both are hidden in print. The .is-active class is reserved for a future enhancement; no scrollspy runs today.
| Invalid source | Build failure |
|---|---|
{{< repeattable count=4 >}} |
missing required prefix |
{{< gridtable >}} without columns |
missing columns list |
an explicit table input without key |
input cell needs a key |
| a calc part with an unknown key | part must be prose, formula, equation, or note |
a calc equation token without text or input |
invalid equation token |
| a code fence without a language | code block has no language |
{{< code lang="not-a-language" >}} |
unsupported Chroma language |
| an unmatched shortcode emphasis marker | unmatched emphasis marker |
an unknown [[TERM]] |
term is absent from glossary data |
| an unknown mission tier | tier is absent from the resolved mission page |
| a mission video missing from its leaf bundle | mission video is not a page resource |
| an empty side navigation range | sidebar has no headings in its configured range |
Navigation entries define exactly one destination: page for Hugo content or url for a still-static legacy page. Page references must be lowercase and must resolve during the build.
- id: labs
name: C Labs
page: /labs
- id: missions
name: 2027 Missions
page: /botball_explorer_2027Section edition toggles follow the same rule through the toggle_page front matter field. Do not write .html paths for Hugo content; page-object links emit the configured canonical URL automatically.
Build drafts and validate syntax highlighting:
build_dir="$(mktemp -d)"
hugo --buildDrafts --destination "$build_dir" --printPathWarnings --logLevel error
node tools/check_syntax_highlighting.js "$build_dir"
node tests/test_lab_persistence.js
node tests/test_glossary_dialog.jsThe migration-specific validators and utilities were retired after the Hugo migration was accepted. The frozen inventory and exception data remain as historical evidence, but are not part of routine verification.
Always publish from a newly created destination and replace the deployed Hugo artifact as a unit. Reusing an earlier output directory can retain obsolete flat .html files or the old uppercase Python_Labs tree.
For a migration, capture a baseline build when one is available, but compare contracts rather than raw files. Appropriate checks include:
- each page's sorted
data-keymultiset and keyed initial values; - duplicate keys, duplicate IDs, unresolved
label[for]values, and unlabelled controls; - glossary references, serialized glossary payloads, and resolved mission/tier links;
- resolved
href/srctargets and the published static-asset inventory; - figure sources/alts, overlay availability, and screen/print visibility;
- copied code text, syntax languages, emphasis markers, and filename tabs;
- visible content and representative screen/print appearance.
Normalize or deliberately ignore incidental differences such as whitespace, attribute order, generated IDs, wrapper depth, semantic CSS-class consolidation, and visual differences explicitly accepted by the migration. If a comparison reports a difference, decide whether it crosses a compatibility boundary before changing shared code to imitate old markup.
Always spot-check at least one representative use of every component family affected by the migration. A successful build alone does not prove accessibility, print behavior, or submission compatibility; conversely, a non-identical HTML file is not evidence of a regression by itself.