Files
com.gregkilmer.www/docs/site.md
T
Claude 963ad0df23 Resume from Greg's interview, one-page PDF, store slots closed
The experience notes, skills, education and a new /resume Page text now come
from Greg's Resume Interview answers (tools/migrate/src/seed.ts). Comcast and
Drexel move to a short Earlier list, and the page and PDF gain Selected
projects. Carrier Connect's numbers are Greg's corrected ones.

The PDF fits on one page and carries the email from site settings. Store
lines migrate with no open slots until Greg opens orders.

Schema: experience earlier, page projects, site-settings email.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
2026-10-11 21:58:21 +00:00

112 lines
5.3 KiB
Markdown

# 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/<file>` 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/<slug>` | Case studies and each study |
| `/inquiries`, `/inquiries/sent` | Inquiry form and its confirmation |
| `/library`, `/library/shelf` | The Library and The Shelf; items are anchors `#<slug>` |
| `/workshop`, `/workshop/<craft>` | 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/<topic>`, `/blog/<slug>`, `/rss.xml` | Blog, topics, posts, feed |
| `/store`, `/store/<line>`, `/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=<reader 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. 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.