Built with Astro and Svelte 5, migrated from the original Sapper/Rollup setup.
src/content/mirrors the site's URL structure..mdxfiles are markdown that can embed the shared.astrocomponents fromsrc/components/. Photos live insrc/assets/and are rendered through Astro's<Image />component (or optimized markdown images), which converts them to appropriately sized WebP at build time.- A page's styles live in a sibling
.cssfile imported by its.mdx, with every selector prefixed by a.page-<name>class that[...slug].astroputs on the page's wrapper element (the replacement for mdsvex's Svelte-scoped<style>blocks). src/pages/[...slug].astroturns every.mdxfile insrc/content/into a static page. A folder'sindex.mdxis served at the folder's own URL and receiveslinksto its sibling pages as a prop (replacing the oldindex.json.jsserver endpoints).src/layouts/BaseLayout.astroreplacessrc/template.html+src/routes/_layout.svelte: it renders the header, the title, and the nav, and injects<base href="/">so the content's root-relative URLs keep working.- The accordion nav tree is generated at build time by
src/helpers/navLinks.js, which scanssrc/content/the same way the oldnavLinks.json.jsendpoint scannedsrc/routes/. - Almost everything is prerendered static HTML. Svelte remains only for the two hydrated islands: the nav (
client:loadin the layout, for the accordion and mobile menu) and the search results page. The Dancing Phantoms page repositions images with a plain<script>ResizeObserverin its dedicated page. - The ʻokina (U+02BB) is wrapped in
<span class="okina">at build time by a rehype plugin inastro.config.mjs, so CSS can give it a font with a correct advance width.
Every page gets a unique <title> and <meta name="description">, rendered by src/layouts/BaseLayout.astro. To change them, edit the YAML frontmatter at the top of the page's file in src/content/:
---
title: Hike to Diamond Head Lighthouse
description: A free guided walk on the first Saturday of every month...
---titlebecomes<title>Title | Jack Shields Christensen</title>;descriptionbecomes the meta description. The search index also prefers the frontmattertitle.- If an
.mdxfile has no frontmattertitle, the page still gets one derived from its file name (viamakeReadableName); with nodescription, the meta tag is simply omitted. - The home page's description lives in
src/pages/index.astro. - The dedicated pages (
src/pages/index.astro,search.astro,404.astro) passtitle/descriptionstraight toBaseLayout.
site in astro.config.mjs is the canonical origin (https://jackshieldschristensen.com); every absolute URL below is resolved against it.
BaseLayout.astrorenders a<link rel="canonical">plus Open Graph and Twitter card tags on every page. Canonical URLs carry no trailing slash, matching the site's own links, and the home page stays a bare/.- The link-preview image is
src/assets/og-circle-square-logo.svg: Jack's circle-and-square symbol on the site's background color, drawn at 1200×630 for the wide (summary_large_image) card. The scrapers don't render SVG, so the build rasterizes it to PNG — which is whatimage.dangerouslyProcessSVGinastro.config.mjsis for. To change the preview, edit that one file. @astrojs/sitemapwritessitemap-index.xmlandsitemap-0.xmlat build time./404,/search, and the self-named redirect URLs inredirectsare left out; the redirect stubs Astro generates are alreadynoindexand point their canonical at the real page.public/robots.txtpoints crawlers at the sitemap and disallows/searchand/api/.
Site search is powered by a Vercel serverless function:
scripts/build-search-index.mjsruns at the start ofnpm run buildand indexes every page insrc/content/(title from frontmatter or the first heading, plus the page text with markup stripped) intoapi/_search-index.json(gitignored).api/search.jsis a Vercel serverless function served at/api/search?q=words. It ranks pages by term matches (title matches weighted highest, with a bonus for exact phrases) and returns the top results with a snippet around the first match.- The search box in the nav submits to
/search, wheresrc/components/SearchResults.svelte(aclient:onlyisland) calls the function and renders the results with matched terms highlighted.
astro dev serves only the static site, not /api. To exercise search locally, run npx vercel dev, or test the function directly by importing its handler after generating the index with npm run build:search-index.
npm run dev— dev server at http://localhost:4321npm run build— static build todist/npm run preview— serve the built site locallynpm test— runs the dev server and the Cypress specs (requires Cypress installed)
npm run build produces a fully static site in dist/ that can be deployed to any static host. On Vercel, the api/ directory is additionally deployed as serverless functions, which the site search depends on.