# The site `services/site` (`@gk/site`) is the public GregKilmer.com. It is an Astro app rendered on request by the Node adapter. It reads only the content service, with a reader token, and only published notes. ## How a page gets its data ```astro --- import PageShell from "@gk/ui/components/PageShell.astro"; import { site } from "../lib/page.ts"; const s = await site(); // the SiteIndex: every published note in memory const games = s.all("board-game"); // typed: games[0].data.bggId is a number --- ``` `src/lib/content.ts` loads every published note from the content service and refreshes in the background every 30 seconds (`CONTENT_REFRESH_SECONDS`). If the service is down, the last good copy keeps serving. `src/lib/site.ts` (`SiteIndex`) answers everything a page asks: | Method | Gives | | --- | --- | | `all(type)`, `one(type)`, `bySlug(type, slug)`, `byName(name or [[link]])` | Notes. | | `href(note)` | The address that shows a note (see the URL map below). | | `link("[[Name]]")` | `{ title, href, note }`; `href` is null when the note is not published, so render plain text. | | `backlinks(note, { type })` | Notes linking to it, newest first. Links to a make count for its craft, a deck for its game, a campaign for its system. | | `linksFrom(note)` | Notes it links to. | | `posts({ topic })`, `caseStudies()` | Blog posts and case studies, newest first. | | `statuses()`, `statusFor(note)` | The status log (newest per subject, pinned first) and a note's current status. | | `kind(label)` | A status kind's brand icon and page. | | `pageText(path)` | The Page text note for an address: lede, body, quick facts. | | `settings()` | Site settings: name, tagline, availability, socials. | | `render(markdown)` | Safe HTML with wikilinks resolved. | | `redirect(type, slug)` | Where an old slug now lives; answer it with a 301. | `mediaSrc(value)` turns an image field into a `/media/` address, which passes vault images through from the content service. `formatDate("2026-10-11")` gives "Oct 11, 2026". ## URL map | Address | Shows | | --- | --- | | `/` | Landing | | `/about` | About me | | `/resume`, `/resume.pdf` | Resume and its PDF | | `/work`, `/work/` | Case studies and each study | | `/inquiries`, `/inquiries/sent` | Inquiry form and its confirmation | | `/library`, `/library/shelf` | The Library and The Shelf; items are anchors `#` | | `/workshop`, `/workshop/` | Workshop and each craft; makes are anchors on their craft | | `/tabletop` | Systems, campaigns, card games and decks as anchors | | `/now` | Every status; each is an anchor | | `/blog`, `/blog/topic/`, `/blog/`, `/rss.xml` | Blog, topics, posts, feed | | `/store`, `/store/`, `/store/order` | Store, product lines, order form | | anything else | 404, Off the star chart | ## Rules for pages - Build from the components in `@gk/ui` (see [components.md](components.md)) and the gk-* classes. Every page uses `PageShell` with its `family` (edge seal) and `current` (nav entry); its hero goes in the `hero` slot. - All dark. Panels are Midnight with a gold frame, not Starlight. - Never invent facts or numbers. Where a note has no value, leave the element out, or show `[PLACEHOLDER]` where the design needs text Greg has not written yet. - No phone numbers. Email addresses stay as they are in the notes. - Lists of posts are fixed-height rows, not stretching cards. - Small client scripts (filters, tabs) are fine; the page must read correctly without them. ## Running it locally ```sh # the content service, against a copy of the Site folder SITE_DIR=… DATA_DIR=… CONTENT_TOKENS="reader:<24+ chars>" npm run content # the site CONTENT_URL=http://localhost:4310 CONTENT_TOKEN= npm run site # http://localhost:4330 ``` For a local preview of drafts, set `CONTENT_INCLUDE_DRAFTS=true` with an editor or admin token. Never set it where the public can reach the site. ## The resume PDF `/resume.pdf` is drawn with pdfkit from the same `resumeData()` as `/resume` (`src/lib/resume.ts`), so the two never disagree. Each page is laid over the Greg Kilmer letterhead (`src/assets/letterhead`, a copy of `branding/letterhead/Greg-Kilmer-Letterhead.pdf`; re-copy it when the letterhead changes) with pdf-lib, which keeps both as vectors. It reads top to bottom so applicant tracking systems take it in order, aims for one page (roles marked `earlier` print as a short list; skills share a row with education and projects), uses the brand TTFs in `src/assets/pdf-fonts`, and is rebuilt only when the notes behind it change. The highlights row and Selected projects come from the `/resume` Page text note's `stats` and `projects`. Greg's Resume Interview answers live in `tools/migrate/src/seed.ts`. ## Where each page lives | Page | File | Helpers | | --- | --- | --- | | Landing, About, Now | `pages/index.astro`, `about.astro`, `now.astro` | `lib/home.ts`, `components/home` | | Resume and PDF | `pages/resume.astro`, `resume.pdf.ts` | `lib/resume.ts`, `lib/resume-pdf.ts` | | Work, case studies | `pages/work/` | `lib/work.ts`, `components/work`, `components/blog/PostLayout` | | Inquiries, sent, 404 | `pages/inquiries/`, `404.astro` | `lib/work.ts` | | Library, Shelf | `pages/library/` | `lib/library.ts`, `components/library` | | Workshop, crafts, Tabletop | `pages/workshop/`, `tabletop.astro` | `lib/world.ts`, `components/world` | | Blog, topics, posts, RSS | `pages/blog/`, `rss.xml.ts` | `lib/blog.ts`, `components/blog` | | Store, lines, order | `pages/store/` | `lib/store.ts`, `components/store` | Forms post to `/api/inquiries` and `/api/orders`, which arrive in phase 6.