Phase 1 of the functional design plan. - Move the old site into legacy/, unchanged, for reference. It still builds from there until cutover. - Add packages/schema: the one definition of all 20 note types, with field titles and descriptions, note and vault checks, and JSON Schema export so later editing tools and the MCP server share it. - Add vault-kit: Obsidian templates generated from the schema, and the Site guide for 24.01_PersonalWebsite. - Add tools/migrate: turns the legacy JSON, collections, store data and page text into 545 Site folder notes and 84 images, and checks the result with the schema. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012aSbo2D1qP8cMFQCykf7kx
6.1 KiB
Store (store.gregkilmer.com)
A small storefront for Greg's proxy MTG card and 3D-printed deck box /
counter services. It lives at /store inside the same app, reuses the main
site's aesthetic, and is reachable directly or via the store.gregkilmer.com
subdomain. It takes no payment — the order form only emails a request — and
the form is hard-locked until you choose to accept orders.
Pages
| Route | What it is |
|---|---|
/store |
Storefront: intro, products, live slots, policies |
/store/cards |
Proxy MTG cards specs + pricing |
/store/deck-boxes |
3D-printed deck boxes & counters specs + pricing |
/store/order |
Order request form (gated) or "closed" notice |
The store uses its own nav (StoreNav.astro) and has no links back to the
main site. The two sites are intentionally not connected from the UI.
Opening and closing orders
Everything customer-facing about availability is driven by one file:
src/data/store-availability.json
{
"ordersOpen": false,
"lastUpdated": "2026-08-04",
"turnaround": "Estimated turnaround: ~2 weeks once a list is confirmed.",
"maxNote": "I take a limited number of orders at a time...",
"slots": {
"cards": { "open": 0, "total": 5 },
"deckBoxes": { "open": 0, "total": 4 }
}
}
ordersOpenis the master switch.false→ the order page shows a "closed" notice and renders no form.true→ the order form appears.
slots.cards/slots.deckBoxesdrive the "X of Y open" meters on the storefront and order page. Setopento 0 to show a category as Closed.turnaround,maxNote, andlastUpdatedare shown verbatim.
Edit the file, commit, and redeploy. That's it.
The lock is enforced in two places
- UI:
/store/orderreads the file at render and hides the form whenordersOpenis false. - Server:
/api/orderreads the file again and returns 403 ifordersOpenis false — before doing any validation or delivery. So even a direct POST can't bypass the lock.
Editing products, specs, and pricing
Those live in version-controlled TypeScript (not the availability JSON):
src/data/store-products.ts
Each product has an icon, title, summary, example image, a specs list
(size, thickness, finishes, white back, etc.), and pricing tiers. Search for
[Greg to fill] to find the placeholder prices and concrete specs to replace.
Available print colors (deck boxes)
The deck-box product has a colors array — this drives the Available print
colors swatch grid on /store/deck-boxes and the color dropdown in the order
form. Each entry is { name, hex, inStock }:
colors: [
{ name: 'Midnight Black', hex: '#1b1730', inStock: true },
{ name: 'Nebula Teal', hex: '#4f8c84', inStock: false }, // shows "Out of stock"
],
- Add a color → append a new
{ name, hex, inStock }entry. - Mark a color sold out → set
inStock: false. It shows an "Out of stock" label on the grid and drops out of the order-form dropdown automatically. - Remove a color → delete the entry.
Only in-stock colors are offered in the order form; the chosen color is included
in the order email. Colors are optional — products with no colors array (e.g.
proxy cards) simply don't show the grid or the dropdown.
The shared requirements / policies list is also in this file (requirements)
and is rendered on the storefront, the order page, and required as a checkbox on
the order form.
Order delivery (email)
Order requests are emailed using the same SMTP/webhook pipeline as the contact
form (see docs/DEPLOYMENT.md). The only addition is the inbox:
ORDER_TO=orders@gregkilmer.com
If ORDER_TO is blank, it falls back to CONTACT_TO. Configure SMTP_* vars
as usual; if neither SMTP nor CONTACT_WEBHOOK_URL is set, /api/order returns
503 (delivery not configured) so it's visibly a config gap.
Rate limiting reuses CONTACT_RATE_LIMIT / CONTACT_RATE_WINDOW_MIN unless you
set ORDER_RATE_LIMIT / ORDER_RATE_WINDOW_MIN to give orders a separate budget.
Bot protection reuses the Cloudflare Turnstile keys (TURNSTILE_SITE_KEY /
TURNSTILE_SECRET_KEY). Leave both blank to skip Turnstile (e.g. local dev).
Pointing store.gregkilmer.com at the store
The store is part of the same app — no separate server. The subdomain is purely a reverse-proxy concern (Cosmos / Traefik / Nginx Proxy Manager):
- Create a DNS record for
store.gregkilmer.com. - In your reverse proxy, point
store.gregkilmer.comat the same container on port 4321, with TLS. - (Optional) Add a redirect or rewrite so
store.gregkilmer.com/maps to/store. The store pages are reachable at/store*on the main domain too, so a redirect isn't strictly required — visitors who know the path can reach it either way.
No code change is needed to enable the subdomain.
Search visibility
Store pages are noindex by default (set in StoreLayout.astro). When
you're ready for the store to be indexed, change noindex to false there (or
override it per page via the noindex prop).
Files added / changed
src/data/store-availability.json— the file you editsrc/data/store-availability.ts— typed reader (used by pages + API)src/data/store-products.ts— products, specs, pricing, requirementssrc/components/store/—StoreNav,ProductCard,PricingTable,SlotMeter,RequirementList,OrderForm.tsxsrc/layouts/StoreLayout.astro— store wrapper (surface, store nav, noindex)src/pages/store/—index,cards,deck-boxes,ordersrc/pages/api/order.ts— order endpoint with hard gatesrc/layouts/BaseLayout.astro— added optionalstoreNavprop (rendersStoreNavinstead of the mainNavBaron store pages)src/components/nav/MobileMenu.tsx— added store icon pathssrc/lib/env.ts— addedorderTo()helper.env.example— addedORDER_TO