Skip to content

Commit d3fa513

Browse files
feat: dynamic section pages, structured data, tests, and catalog improvements
- Add dynamic [section].astro for content-driven section pages - Add structured JSON-LD generation (jsonld.ts) + markdown export (markdown-export.ts) - Add comprehensive test coverage (vitest.config.ts, catalog/content/jsonld tests) - Refactor sections into data layer (src/data/sections.ts) - Update category/tag/skill/video/post pages for dynamic section support - Remove hardcoded Wix section pages (cicd-github-actions, curietech, etc.) - Improve video catalog with better organization and filtering - Add CONTEXT.md domain documentation and 0001-og-endpoints-stay-per-type.md ADR - Update deploy workflow with test automation - Upgrade dependencies (package-lock.json) Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent 6834f98 commit d3fa513

39 files changed

Lines changed: 1514 additions & 379 deletions

.github/workflows/deploy.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,26 @@ concurrency:
2929
cancel-in-progress: false
3030

3131
jobs:
32+
# Gate the whole pipeline on the unit suite (catalog scheduling gate, calendar, .md/JSON-LD
33+
# builders). build `needs: test`, so a red suite blocks the build+deploy on EVERY trigger —
34+
# push, manual, and the 15-min scheduled rebuild (a failing suite must never auto-ship).
35+
test:
36+
runs-on: ubuntu-latest
37+
steps:
38+
- name: Checkout
39+
uses: actions/checkout@v4
40+
- name: Set up Node
41+
uses: actions/setup-node@v4
42+
with:
43+
node-version: 20
44+
cache: npm
45+
- name: Install dependencies
46+
run: npm ci
47+
- name: Run tests
48+
run: npm test
49+
3250
build:
51+
needs: test
3352
runs-on: ubuntu-latest
3453
outputs:
3554
# Gate for the deploy job. 'true' on every push / manual run; on a SCHEDULED run only

CONTEXT.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
# CONTEXT — ProstDev domain glossary
2+
3+
Shared vocabulary for the video catalog and the pages built from it. Use these terms
4+
as defined here in code, issues, and docs; don't drift to synonyms.
5+
6+
## Glossary
7+
8+
**Video** — one YouTube tutorial in the catalog (`VIDEOS` in `src/data/videos.ts`). Has a
9+
stable `slug` (→ `/video/<slug>`), a `youtubeId`, and the playlist ids it belongs to. A
10+
future `publishedAt` hides it in prod (the scheduling gate, `isVideoPublished`).
11+
12+
**Playlist** — a catalog fact: a named YouTube series (`PLAYLISTS` in `src/data/videos.ts`)
13+
with a `title`, `description`, optional `playlistUrl`, and a prominence `tier`
14+
(`primary` = focal / homepage-eligible, `more` = findable back-catalog). A Playlist owns the
15+
canonical title/description; it is NOT a page.
16+
17+
**Section** — a curated landing page (e.g. `/learn-acb`) composed of one or more Playlist
18+
**blocks** (`SECTIONS` in `src/data/sections.ts`, rendered by `src/pages/[section].astro`). A
19+
Section is PRESENTATION only: it adds the page `slug`, an optional `seoTitle`, and a per-block
20+
intro `hook`. It reuses the Playlist's title/description as the heading and meta. One Section
21+
can compose several Playlists (e.g. `/mulesoft-ai` renders `ai-showdown` + `mulesoft-ai-2025`),
22+
so a Playlist may be shown on a Section whose slug isn't its own.
23+
24+
**Owning section** — the Section that renders a given Playlist. `sectionHref(playlist)`
25+
(`sections.ts`) resolves a Playlist to its owning Section's URL, so a Playlist's video
26+
breadcrumbs and its `/videos` row heading link to the page that actually shows it — even when
27+
that Playlist appears only as a secondary block.
28+
29+
**Scheduled** — a Video whose `publishedAt` is still in the FUTURE relative to a given instant.
30+
`isScheduled(v, now)` (`videos.ts`) is the single, environment-free home of that fact. It is the
31+
one seam two policies build on: the **scheduling gate** `isVideoPublished` (prod HIDES scheduled
32+
videos; dev shows everything) and the content calendar (`upcomingCalendarItems`, which wants
33+
exactly the scheduled set, in any environment). Because both read `isScheduled`, the gate and the
34+
calendar can't drift. A Video with no `publishedAt` is never scheduled (always live). The blog
35+
twin is the `pubDate` gate in `src/lib/content.ts`.
36+
37+
## Notes
38+
39+
- Section ≠ Playlist: catalog truth lives on the Playlist; a Section only arranges Playlists
40+
into a page. Don't duplicate a Playlist's title/description into a Section unless the SEO
41+
title genuinely differs (then use `seoTitle`).
42+
- Adding a section page = add a `Section` to `SECTIONS`. There is no per-page `.astro` file and
43+
no slug list to keep in sync (the retired `SECTION_PAGE_SLUGS` Set).
44+
- Scheduling ≠ prod policy: `isScheduled` is the time FACT; `isVideoPublished` layers the
45+
hide-in-prod POLICY on top. Don't re-implement the `publishedAt > now` comparison anywhere —
46+
route through `isScheduled` (this is what commit 9d29051's bug + `videos.catalog.test.ts` guard).
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
# ADR-0001 — OG-image endpoints stay per-type, not a unified dispatcher
2+
3+
- **Status:** Accepted
4+
- **Date:** 2026-07-16
5+
- **Deciders:** Alex Martinez
6+
7+
## Context
8+
9+
Social-share (OG) card PNGs are generated at build time by four route files:
10+
11+
- `src/pages/og/default.png.ts` (7 lines)
12+
- `src/pages/og/post/[slug].png.ts` (15 lines)
13+
- `src/pages/og/skill/[slug].png.ts` (15 lines)
14+
- `src/pages/og/video/[slug].png.ts` (23 lines)
15+
16+
The real rendering depth already lives in one deep module, `src/lib/og-image.ts`
17+
(`card` / `bareImageCard` / `renderCard` / `loadPostHero` / `fetchRemoteImage`). The four
18+
endpoints are thin adapters over it.
19+
20+
An architecture review (July 2026) listed, as a *Speculative* candidate, unifying the four
21+
into a single `og/[...type].png.ts` that dispatches by type — noting it as "listed for
22+
completeness" and marginal. This ADR records why we declined it, so future reviews don't
23+
re-surface it.
24+
25+
## Decision
26+
27+
Keep one OG endpoint per content type. Do **not** collapse them into a type-dispatching route.
28+
29+
## Rationale
30+
31+
The four endpoints look like duplication but share almost nothing beyond the Astro
32+
`getStaticPaths` + `GET` shell. What actually differs per type:
33+
34+
- **Data source**`getPosts()` / `getSkills()` / `publishedVideos()`, and `default` has no
35+
`getStaticPaths` at all.
36+
- **Card function** — post/skill use `card()` (title overlaid on a hero image); video uses
37+
`bareImageCard()` (full-bleed YouTube thumbnail, no overlay — the thumbnail already bakes in
38+
the title, and overlaying produced text-on-text).
39+
- **Fetch model** — video is `async` and fetches a remote thumbnail with a maxres→hq fallback;
40+
post/skill do a synchronous `loadPostHero(filePath)`.
41+
42+
A unified dispatcher would have to union three `getStaticPaths` (three data sources) and branch
43+
on type for both the card function and the sync/async fetch. Net change ≈ −5 lines, but it
44+
**fails the deletion test**: deleting the four files for one router does not concentrate
45+
complexity — it scatters each content type's OG logic behind a branch and destroys locality.
46+
Today "how is a video's OG card made?" is answered by one legible 23-line file; behind a
47+
dispatcher it becomes a case in a multi-type switch. The seam is already in the right place
48+
(`og-image.ts`); the endpoints are correctly shallow adapters, which is what a route file
49+
should be.
50+
51+
## Consequences
52+
53+
- Adding a new OG card type = add one small endpoint file (the established pattern), not a new
54+
branch in a shared router.
55+
- The genuine micro-duplication between the post and skill endpoints (both:
56+
collection → `getStaticPaths``card({ title, eyebrow, bg: loadPostHero(filePath) })`) is
57+
accepted as-is; each `GET` body is three lines and the two differ in `eyebrow`/title source,
58+
so a shared helper would take nearly as many arguments as it removes lines.
59+
- Related cleanup done alongside this decision: `og/video/[slug].png.ts` annotated its props
60+
with `(typeof VIDEOS)[number]` referencing an unimported `VIDEOS` (a dangling identifier
61+
`astro build` tolerated but `astro check` would flag). Switched to the exported `Video` type.

0 commit comments

Comments
 (0)