Initial Commit

This commit is contained in:
Greg Kilmer
2026-07-04 07:42:24 -04:00
commit 71bffad507
77 changed files with 14437 additions and 0 deletions
+130
View File
@@ -0,0 +1,130 @@
# Content Guide
How to add and edit content on the site. Two sections use **Content Collections**
(typed markdown/MDX); the interest pages are hand-built `.astro` files.
---
## Projects (Professional page)
Projects render from `src/content/projects/*.md` (or `.mdx`). Each file is one
project card. The schema lives in `src/content.config.ts`.
### Add a project
Create `src/content/projects/my-project.md`:
```md
---
title: 'My Awesome Project'
role: 'Lead Developer' # optional, default: 'Developer'
tech: ['TypeScript', 'Node.js', 'SAP BTP'] # rendered as pills
summary: 'A 2-3 sentence description of what it does and your contribution.'
repo: 'https://github.com/you/repo' # optional
demo: 'https://demo.example.com' # optional
caseStudy: 'https://blog.example.com' # optional
cover: '/images/my-project.png' # optional, public path
order: 20 # lower = shown first, default 100
placeholder: false # true hides it from "real" styling
---
Optional longer writeup in markdown here. The card only shows `summary`, but you
can link to a case study for depth.
```
### Field reference
| Field | Type | Required | Notes |
|---|---|---|---|
| `title` | string | yes | Project name |
| `role` | string | no | e.g. "Lead Developer" |
| `tech` | string[] | no | Tag pills |
| `summary` | string | yes | Short card description |
| `repo` / `demo` / `caseStudy` | URL | no | Outbound links |
| `cover` | string | no | Image path in `public/` |
| `order` | number | no | Sort weight |
| `placeholder` | boolean | no | Marks it as a TODO card |
To remove the seed placeholder, delete `placeholder-project.md`. Aim for 3-6 real
entries per the spec.
---
## Experience (Professional timeline)
Experience renders from `src/content/experience/*.md`. Each file is one role on
the vertical timeline.
### Add a role
Create `src/content/experience/another-role.md`:
```md
---
company: 'Previous Co.'
title: 'Junior Developer'
period: '2016 - 2020' # free-form date label
focus: 'What you primarily did.' # optional
icon: 'briefcase' # mdi icon name for the marker
order: 20 # lower = more recent / first
---
- Bullet point of an accomplishment.
- Another bullet, ideally with a metric.
- One more, showing impact.
```
The body renders inside an expandable "Show details" panel. Use markdown lists
for bullets. Replace the bracketed placeholders in the seed `implico.md` with
real outcomes, metrics, and dates.
### Icon names
The `icon` field takes an mdi name without the `mdi:` prefix. Browse names at
[pictogrammers.com](https://pictogrammers.com/library/mdi/) or search the
installed set:
```bash
node -e "const m=require('./node_modules/@iconify-json/mdi/icons.json'); console.log(Object.keys(m.icons).filter(n=>n.includes('cloud')).join('\n'))"
```
---
## Interest pages
Each interest is a hand-built page in `src/pages/interests/`. They follow the
spec's template: **Header** (title + tagline), **Overview** (narrative),
**Highlights** (card grid), **Currently** (active now), **Related** (cross-links).
To edit content, open the relevant `.astro` file and change the copy in the
frontmatter arrays or the section bodies. To add a new interest:
1. Create `src/pages/interests/my-interest.astro` (copy an existing page as a
template).
2. Use `<BaseLayout surface="interest">`, `<InterestHero>`, `<InterestCard>`, and
`<RelatedInterests>`.
3. Add a card for it in `src/pages/interests/index.astro` (the hub).
---
## Global content (nav, socials, tagline)
Edit `src/lib/site.ts`:
- `siteIdentity` — name, tagline, domain
- `navGroups` — the two-group top nav
- `socials` — footer + contact links (replace the `[Greg to fill]` handles)
---
## Finding placeholders
Every `[Greg to fill]` gap from the spec is rendered as a styled **Placeholder**
slot (dashed border + pencil badge + "TODO · label"). To find them all:
```bash
grep -rn "Placeholder label" src/
grep -rn "Greg to fill\|\[Greg" src/
```
Replace each with real content as you gather it.
+137
View File
@@ -0,0 +1,137 @@
# Deployment Guide
The site runs as a **Node server inside a container** on your homelab Cosmos
system, fronted by your reverse proxy (Traefik / Caddy / nginx) with TLS for
`patiun.com`.
---
## 1. Configure environment
Copy the example env file and fill in what you need:
```bash
cp .env.example .env
```
At minimum, set `SITE_URL` (your canonical public URL). To enable contact-form
delivery, set **either** SMTP vars **or** `CONTACT_WEBHOOK_URL` (see
[Contact delivery](#contact-delivery) below). Everything else is optional.
| Variable | Purpose | Default |
|---|---|---|
| `PORT` | Port the Node server binds to | `4321` |
| `HOST` | Bind address (`0.0.0.0` for container) | `0.0.0.0` |
| `SITE_URL` | Canonical URL for metadata | `https://patiun.com` |
| `SMTP_HOST` / `SMTP_PORT` / `SMTP_USER` / `SMTP_PASS` | SMTP relay | unset |
| `SMTP_FROM` | From address for outgoing mail | derived from `SITE_URL` |
| `CONTACT_TO` | Inbox that receives submissions | `greg@patiun.com` |
| `CONTACT_WEBHOOK_URL` | JSON webhook fallback (Discord/Slack/n8n) | unset |
| `CONTACT_RATE_LIMIT` / `CONTACT_RATE_WINDOW_MIN` | Per-IP rate limit | `5` / `15` |
---
## 2. Build & run with Docker Compose
```bash
docker compose up -d --build
```
This builds the multi-stage image (`Dockerfile`), starts the container, and maps
host port `${HOST_PORT:-4321}` to container `4321`. Logs:
```bash
docker compose logs -f greg-site
```
Healthcheck hits `/` every 30s. Verify locally:
```bash
curl http://localhost:4321/
```
---
## 3. Reverse proxy + TLS
Point your Cosmos reverse proxy at the container on port `4321`. The two common
shapes:
### Option A — Traefik (labels)
If your Cosmos uses Traefik, add labels to the service in `docker-compose.yml`:
```yaml
labels:
- "traefik.enable=true"
- "traefik.http.routers.greg-site.rule=Host(`patiun.com`)"
- "traefik.http.routers.greg-site.entrypoints=websecure"
- "traefik.http.routers.greg-site.tls.certresolver=your-resolver"
- "traefik.http.services.greg-site.loadbalancer.server.port=4321"
```
Then drop the `ports:` mapping and join the Traefik network instead.
### Option B — Caddy / nginx
Forward `patiun.com` (HTTPS) to `127.0.0.1:4321`. With Caddy, automatic TLS is a
one-liner:
```caddyfile
patiun.com {
reverse_proxy 127.0.0.1:4321
}
```
Either way, ensure the proxy forwards `X-Forwarded-For` / `X-Real-IP` so the
contact API's rate limiter sees real client IPs (not the proxy's).
---
## 4. Contact delivery
The `/api/contact` endpoint delivers submissions through the first configured
path:
1. **SMTP** (if `SMTP_HOST` is set): sends a real email via nodemailer. Works
with any SMTP relay (Gmail app-password, Mailgun, SES, your homelab mail
server).
2. **Webhook** (if `CONTACT_WEBHOOK_URL` is set and SMTP is not): POSTs a JSON
payload `{ name, email, message, text }`. Great for a Discord/Slack/n8n
inbound.
3. **Neither configured**: returns `503` with a friendly "email me directly"
message so the form never silently swallows input.
Test it:
```bash
curl -X POST http://localhost:4321/api/contact \
-H "Content-Type: application/json" \
-d '{"name":"Test","email":"test@example.com","message":"Hello there."}'
```
---
## 5. Updating the site
```bash
git pull
docker compose up -d --build
```
The build stage reinstalls deps and rebuilds `dist/`; the runtime stage swaps in
the new artifact with zero config.
---
## 6. Without Docker (bare Node)
If you prefer to run Node directly on the host:
```bash
npm ci
npm run build
HOST=0.0.0.0 PORT=4321 SITE_URL=https://patiun.com node ./dist/server/entry.mjs
```
Run it under a process manager (systemd, pm2) so it restarts on crash/boot.
+121
View File
@@ -0,0 +1,121 @@
# Photo Guide
Every image on the site right now is a **stock stand-in** (from Unsplash, free, no
attribution required) chosen to match the *vibe, pose, and style* of the real
photo that should eventually live there. This file is the shopping list for
**photos of yourself and your own stuff** — what to take or find, how to frame
each one, and which file to overwrite.
Replace a photo by **dropping a new file at the same path in `public/images/`**.
No code changes needed. Aim for roughly the recommended aspect ratio so the
crop stays clean.
> General mood across the whole site: **moody, warm-low-light, "candlelit study
> under a starfield."** Deep backgrounds, soft warm key light (window, lamp, or
> a single softbox off to one side), shallow depth of field. The frames, gold
> accents, and star background do the rest.
---
## 1. Portraits of you
These are the highest priority — they make the site feel like *yours*.
### `public/images/hero-portrait.jpg` — Home hero (used for the OG/social card too)
- **Subject:** You, at your desk or workspace.
- **Framing:** Portrait, roughly **4:5** (taller than wide). Waist-up or
three-quarter. Head near the top third.
- **Pose:** Natural, mid-thought or slight smile, looking just off-camera or at
the screen. One hand on keyboard or holding something (a die, a mug).
- **Light:** Warm side light from a window or lamp camera-left; let the
background fall into shadow. A little rim light from behind separates you
from the dark background.
- **Background:** Dark/blurred workspace — monitors, books, dice shelf out of
focus. Avoid pure white walls.
- **Wardrobe:** What you actually wear to work; dark or jewel tones read best
against the midnight palette.
- **Resolution:** ≥ 1200×1500 px.
### `public/images/about-portrait.jpg` — About page sidebar
- **Subject:** You, but warmer and more candid than the hero.
- **Framing:** Portrait **4:5**. Can be a different environment than the desk —
a reading chair, the making bench, the game table.
- **Pose:** Relaxed, looking at camera or at a hobby object. Smile is good here.
- **Light:** Soft, warm, even. Think "cozy evening."
- **Resolution:** ≥ 1200×1500 px.
### Optional extras that would level the site up
- **A tabletop "gamemaster" shot** — you behind the screen / Foundry VTT on a
monitor, dice in hand. Could replace `ttrpg-table.jpg`.
- **A making shot** — you casting resin or pulling a print, hands in frame.
Could replace `making-resin.jpg`.
---
## 2. Workspace & professional
### `public/images/pro-workspace.jpg` — Professional summary banner
- **Subject:** Your actual desk / dev setup. Monitors with code, keyboard,
a mug, low light.
- **Framing:** Wide **21:9** landscape. Slight angle, depth.
- **Mood:** "Late-night deploy." Dark room, screen glow as the key light,
warm accent lamp.
- **Resolution:** ≥ 1600×690 px.
### `public/images/pro-team.jpg` (currently unused — available)
- **Subject:** Whiteboard session, pair-programming, or team standup (with
consent). Optional; only add if you want a team-work shot.
---
## 3. Hobbies — your real collections
These benefit most from being *yours* — the stock versions are generic.
### Dice / tabletop
- `public/images/ttrpg-dice.jpg` — **your** polyhedral sets, fanned out or
stacked on a dark surface. Top-down or 45°, single warm light. ~**16:10**.
- `public/images/ttrpg-table.jpg` — your Foundry VTT screen, a battlemap, or
the physical game table mid-session. Wide **21:9**.
### Making
- `public/images/making-resin.jpg` — resin dice mid-cast, pressure pot, pigment
bottles. Macro / close-up, ~**4:3**.
- `public/images/making-print.jpg` — the Elegoo Mars 4 or Bambu P1S mid-print,
or a finished miniature in your hand. ~**4:3** or **16:10**.
- *(The Making gallery shows three images — the third reuses `ttrpg-dice.jpg`.
Add a third unique shot, e.g. painted minis, if you want.)*
### Reading
- `public/images/reading-nook.jpg` — your reading chair / nook. Wide **21:9**.
- `public/images/fantasy-books.jpg` — your Sanderson / Cosmere shelf, spines
out, warm light. ~**4:3** or **16:10**.
### Gaming
- `public/images/gaming-setup.jpg` — your PS5 / Switch / PC corner, controller
on the seat, screen glow. Wide **16:10** or **21:9**.
### Pokémon
- `public/images/pokemon-cards.jpg` — your card collection, a favorite binder
page, or a Metagross card front-and-center. ~**1:1** to **4:3**.
---
## How to swap an image
1. Put the new file at the matching path under `public/images/`.
2. Keep the **same filename** (and ideally the same aspect ratio).
3. Rebuild / redeploy. Done.
To change which image a specific spot uses, edit the `src="/images/..."` path in
the relevant `.astro` file (search the `src/pages/` and
`src/components/layout/InterestHero.astro`).
## Tech notes
- All photos render inside a `CelestialFrame` (gold hairline + corner star
bursts) or as a banner with a star accent, so they inherit the mystical
treatment automatically.
- `loading="lazy"` and `decoding="async"` are already set — no need to optimize
delivery by hand, but keep files under ~500 KB where you can.
- If you remove an image but leave the path, the `CelestialImage` component
falls back to a themed starfield placeholder so the layout never breaks.
+126
View File
@@ -0,0 +1,126 @@
# Theming & Motion Guide
How color, light/dark mode, section tone, and animation work on this site.
---
## Palette
From the spec, exposed as CSS variables in `src/styles/themes.css` and mapped to
Tailwind utilities in `src/styles/global.css`:
| Token | Value | Meaning |
|---|---|---|
| `--color-slate` | `#1E293B` | Professional confidence (primary) |
| `--color-amber` | `#F59E0B` | Warm accent (the making/creative side) |
| `--color-teal` | `#699a94` | Soft teal (secondary, favorite) |
| `--color-ink` | `#0F172A` | Near-black text on light |
| `--color-cream` | `#FFFBEB` | Cream-white text on dark |
Brand colors are fixed. **Semantic** tokens (`--surface`, `--text`, `--accent`,
`--secondary`, `--border`, ...) resolve differently per theme + section, and are
what components actually use.
---
## Two theming axes
### Axis 1 — Global light/dark
Set on `<html data-theme="light|dark">`:
- A blocking inline script (`src/scripts/theme-init.ts`, inlined in `<head>`)
reads `localStorage['gk-theme']`, falling back to the OS
`prefers-color-scheme`. This runs **before first paint**, so there is no flash.
- The nav `ThemeToggle` island flips it and persists the choice.
- If the user has never chosen, the site follows OS changes live.
### Axis 2 — Section tone
Set on `<body data-surface="neutral|professional|interest">` via the `surface`
prop on `<BaseLayout>`:
| Surface | Used on | Tone |
|---|---|---|
| `neutral` | Home, About, Contact | Balanced — clean in light, calm in dark |
| `professional` | Professional page | Dark slate, polished, amber accents |
| `interest` | Interests pages | Warm cream/amber, inviting |
The Professional surface leans dark even in light mode (it is the "polished
wing"). The Interest surface is warm in both modes. This is what gives the two
wings their distinct feel while sharing the same typography and spacing.
Each `(data-theme, data-surface)` combination redefines the semantic tokens in
`themes.css`. Add or adjust a combination there to change any section's palette.
---
## Changing colors
1. **Tweak a brand color:** edit the fixed values in `global.css` under `@theme`
(e.g. `--color-amber`).
2. **Tweak a section's tone:** edit the relevant `[data-surface="..."]` block in
`themes.css`. Each block lists `--surface`, `--surface-raised`,
`--surface-sunken`, `--text`, `--text-muted`, `--text-subtle`, `--border`,
`--accent`, `--accent-contrast`, `--secondary`.
3. Components use utilities like `bg-surface`, `text-text`, `text-accent`
(mapped from these variables), so they update automatically.
### Contrast
Verify WCAG AA (4.5:1 for body text) whenever you change a token pair. The
current combinations were chosen for AA compliance. The `--accent` on the neutral
light surface uses a deeper amber (`#b45309`) specifically to meet AA on white.
---
## Motion
Motion is layered and **always** respects `prefers-reduced-motion`:
1. **Hero background** (`AnimatedBackground.tsx`):
- A CSS mesh-gradient layer (GPU-friendly, animated via the
`animate-gradient-shift` keyframes in `animations.css`).
- An optional `<canvas>` particle field (slow drifting dots) on the home hero
only. It renders at devicePixelRatio (capped at 2), pauses when off-screen,
and is **skipped entirely** under reduced motion.
2. **Scroll reveals** (`Reveal.astro` + the `.reveal` class):
- IntersectionObserver toggles `.is-visible`, triggering a CSS fade-up. Zero
framework cost. The reduced-motion media query in `global.css` forces these
to their final state instantly.
3. **Page transitions**: Astro `<ClientRouter />` (View Transitions API) morphs
between pages and persists the nav.
4. **Nav blur on scroll**: a tiny inline script adds `.scrolled` after a few px,
enabling backdrop-blur + a hairline border + shadow.
5. **Hover micro-interactions**: Tailwind transitions on cards/buttons (lift,
accent glow, arrow nudge).
### Disabling motion
Everything is gated by the global reduced-motion rule in `global.css`:
```css
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: 0.001ms !important;
transition-duration: 0.001ms !important;
...
}
}
```
The canvas island additionally checks `matchMedia('(prefers-reduced-motion:
reduce)')` before drawing, so even JS-driven motion is skipped.
---
## Typography
Self-hosted variable fonts via Fontsource (woff2, no external requests):
- **Headings / UI:** Plus Jakarta Sans Variable (geometric sans)
- **Body:** Inter Variable
- **Mono / kickers / quotes:** JetBrains Mono Variable
Font sizes use `clamp()` for fluid scaling (see the `h1`-`h4` rules in
`global.css`).