Skip to content

Latest commit

 

History

2,762 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Sean Behan's Personal Website

Personal site and technical blog. EmDash on Astro, deployed as a Cloudflare Worker with D1 and R2.

One repo, two origins:

  • seanbehan.ca — name-first, résumé in the nav, and the commercial home: services, products, checkout and the legal pages
  • codebam.ca — handle-first portfolio: public projects lead, and everything written or sold 301s to seanbehan.ca

Both origins render the same database. Identity is chosen at build time by PUBLIC_SITE.

The email-list form on both origins posts directly to lists.seanbehan.ca. That Worker's ALLOWED_SIGNUP_ORIGINS must name a front door before the form there can return a reader to it; adding a third origin means adding its origin to that list.

Prerequisites

  • Node.js 26 (see .node-version)
  • npm
  • A Cloudflare account. Everything here works on the free Workers plan; EmDash marketplace installs and sandboxed plugin execution are disabled in this repo (they would need a paid plan), and no production path depends on them.
  • Nix, only for the résumé: nix run .#resume is what builds the PDF. Nothing else in the repo needs it, and no build step here asks for it.

Setup

git clone https://github.com/codebam/seanbehan.ca.git
cd seanbehan.ca
npm install
npm run dev

npm run dev runs emdash dev: it applies any pending migrations, seeds an empty database from seed/seed.json, and starts Astro. The site is at http://localhost:4321 and the admin panel at http://localhost:4321/_emdash/admin — on localhost the dev bypass signs you in without a passkey.

Use npm run dev:codebam for the handle-portfolio variant. The commercial bindings and Stripe secrets live on the default build.

The local database is a SQLite file under .wrangler/state, not committed. An empty one is seeded with the schema (posts, pages, tags) but no posts; see Content below for how the real ones got there.

/resume reads its document out of R2 rather than the database, so a fresh clone renders it without the résumé in it. npm run resume:seed builds the two artifacts into the local store once, and the page is normal from then on.

Content

Posts live in D1 and are written in the admin panel. They are not files in this repo — that is the point of the migration this site went through: the words are content, and the repo is the thing that renders them.

seed/seed.json is the schema, not the writing. It describes the collections, their fields and the tag taxonomy, and it is what a fresh database is built from.

Posts that predate the CMS came out of the old src/routes/posts/*.md files via scripts/import-markdown.mjs, which is kept for the record and for anyone repeating the move onto a fresh instance.

Build

npm run build              # seanbehan.ca
npm run build:codebam      # codebam.ca

Deploy

npm run deploy             # build seanbehan, wrangler deploy (writing + commerce)
npm run deploy:codebam     # build codebam, deploy as codebam-ca (portfolio + 301s)

Both Workers share the same D1 database and media bucket, so a post published from either admin panel reaches both origins. The paid-download bucket and order-email binding exist only on the default Worker.

First deploy needs the bindings to exist:

npx wrangler d1 create seanbehan-ca
npx wrangler r2 bucket create seanbehan-ca-media
npx wrangler r2 bucket create codebam-product-downloads
npx emdash secrets generate
npx wrangler secret put EMDASH_ENCRYPTION_KEY
npx wrangler secret put EMDASH_ENCRYPTION_KEY --env codebam

Use the same generated encryption key for both Worker secret prompts.

The production kit uses Stripe Checkout and a private R2 download. Product, Managed Payments, webhook, artifact, email, and Worker secret setup is documented in docs/stripe-checkout.md.

HTML is served with s-maxage so Cloudflare can hold it at the edge, but that only takes effect once a Cache Rule marks HTML cacheable — a zone setting, not a repo one. docs/edge-caching.md has the rule and the deploy-time purge it requires.

Published posts canonical to seanbehan.ca; project case studies canonical to codebam.ca. Homes stay self-canonical.

Marketplace plugin installation and sandboxed plugin execution are intentionally disabled in astro.config.mjs until EmDash enforces a plugin's declared manifest.hooks and runtime capability checks; the trusted local plugins in the plugins: array remain in use.

Backups

The posts used to be files in this repo, so every clone was a backup. They live in D1 now, and .github/workflows/backup.yml runs nightly to keep that from meaning "one copy": it exports the database and writes it to the seanbehan-ca-backups bucket under a dated key, which the bucket expires after 90 days. That bucket must stay private — no r2.dev public access and no custom domain — because it holds the site's writing. An export also prints a one-hour presigned URL to the dump; the backup script captures and discards that output, so it no longer reaches the CI log.

npm run backup                        # writes backup.sql from the deployed database

wrangler d1 export refuses a database containing FTS5 virtual tables, and EmDash builds one per searchable collection, so scripts/backup-d1.mjs asks the database for its table list and exports everything that is not virtual. The search indexes are derived data — after a restore, rebuild each with INSERT INTO <fts_table>(<fts_table>) VALUES('rebuild').

The dump is content-only. It deliberately omits options (preview secret, session salt and plugin settings); the user and auth tables (users, users_new, users_old, credentials, auth_tokens, auth_challenges, oauth_accounts, sessions); the _emdash_* API, authorization and OAuth token tables; plugin state and storage (_plugin_state, _the_plugin_state, _plugin_storage, _plugin_indexes); and site_stripe_fulfillments, whose checkout_session_id is a bearer download credential. A restore therefore brings back the exported content tables but not those rows; sign-ins, plugin configuration and storefront fulfillment records have to be re-provisioned or reissued afterwards.

Résumé

resume/resume.md is the résumé, and it is a build rather than a page: pandoc typesets it to PDF with tectonic and renders the same pass to an HTML fragment and a plain-text copy. CI does all three on a push that touches resume/.github/workflows/resume.yml builds, checks, and writes resume.pdf, resume.html and resume.txt to the private bucket, then purges the zone.

The page renders the fragment inline; /resume.pdf streams the PDF out of the same bucket and /resume.txt the ATS copy, which is why the downloads are paths on this origin and not a hostname. A résumé edit therefore goes live with an upload, not a deploy, and npm run build never needs TeX.

npm run resume        # all three artifacts into resume/out/, via nix
npm run resume:seed   # …and into the local dev bucket, for npm run dev

docs/resume.md is the pipeline — bucket, routes, CI, what a token needs to be able to do. resume/README.md is the author's guide — the Markdown conventions, the front matter, and every knob in metadata.yaml.

Checks

npm run check              # astro check (types across .astro and .ts)
npm run test:run           # unit tests over src/lib
npm run smoke              # ask a running site for one of everything

npm run smoke takes a base URL and defaults to http://localhost:4321. It is the check that matters most here: pages assemble on a Worker out of a database, so a page, a feed, a social card and a 404 have to be fetched to know they work. It asks for /resume and /resume.pdf too, which are the two routes that answer out of R2 — so seed the local bucket first.

GitHub Actions runs the formatter check, astro check, the tests and both builds on every push and pull request. .githooks is wired by prepare so commits run Prettier and the type check first.

Assets

  • Profile image is WebP (13.7 kB), with a 112px avatar.webp (1.4 kB) for the hero byline
  • Fonts are self-hosted from public/fonts, latin-only and axis-trimmed, and preloaded from the layout. Rebuild them with tools/fonts/build-fonts.sh after upgrading either Fontsource package — see tools/fonts/README.md
  • public/fonts/og-*.woff are the static cuts the social-card renderer needs: satori misreads the variable fonts' fvar table, and a card needs one weight of each face
  • Social cards are drawn per request at /og/<slug>.png from the title in the database

Layout

Path What it is
src/pages Routes. Every page is server-rendered.
src/layouts/Base.astro Head metadata, header, footer — the frame every page renders into
src/lib The site's own logic: post adapter, identity, feed, highlighter
src/components Plain Astro templates, including the decorative canvas markup
src/scripts Canvas effects and small delegated client behaviours
src/middleware.ts Security headers and the cache policy
seed/seed.json Schema a fresh database is built from
resume/ The résumé: Markdown source, pandoc templates, Lua filter, driver
scripts/ The markdown import, its backfill, and the smoke test
tools/ Font, favicon and Cloudflare cache utilities

About

seanbehan.ca source code

Topics

Resources

Contributing

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages