mirror of
https://github.com/Patiun/com.gregkilmer.www.git
synced 2026-11-07 19:58:24 +00:00
Initial Commit
This commit is contained in:
+130
@@ -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.
|
||||
@@ -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
@@ -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
@@ -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`).
|
||||
Reference in New Issue
Block a user