The www.iloveconference.org front end: a SvelteKit site built
with @sveltejs/adapter-static and served as static files from S3 through CloudFront. Searches go
to the search API (the server repo), which runs on AWS Lambda behind the same CloudFront
distribution at /api/*.
- Node.js 22 or newer.
.npmrcsetsengine-strict=trueandpackage.jsonsetsengines.nodeto>=22, so pnpm refuses anything older. CI and the deploy build on 22; newer is fine locally. - pnpm 8, the version in
packageManagerinpackage.json. Runcorepack enableonce to use it automatically, or runnpx pnpm@8.15.9 ….
pnpm installThe dev server proxies /api/* to http://localhost:8000 (see vite.config.ts). For working
searches, start the search API from the server repo first:
# in the server repo
uvicorn server.main:app --port 8000Then start the site at http://localhost:5173:
pnpm devWithout the API, every page except search works. Search shows "Search is temporarily unavailable".
pnpm check # svelte-check and TypeScript
pnpm lint # prettier --check and eslint
pnpm format # fix formatting with prettier
pnpm test:unit --run # vitest unit tests
pnpm test # Playwright browser tests (needs `npx playwright install` once)pnpm buildThis writes the static site to build/:
index.html,search.html,static/about.html,static/terms.htmlandstatic/privacy.html, prerendered at build time.search.htmlis an empty shell: the search page calls/api/search?q=…from the browser and shows the answer and results.404.html, which CloudFront returns for unknown paths._app/immutable/, the JS and CSS, with content hashes in their file names.
pnpm preview serves the build locally, also with the /api proxy.
In production, a CloudFront Function maps URLs to these files: / to /index.html, and a path
whose last segment has no . (such as /static/about) to the same path plus .html. A trailing
slash redirects to the path without it.
CI deploys every push to main (see below). To deploy by hand, with AWS credentials for the
account:
./deploy.sh # pnpm build, then upload
SKIP_BUILD=1 ./deploy.sh # upload the existing build/deploy.sh reads the bucket and the distribution ID from the outputs of the ilc CloudFormation
stack (us-west-2, defined in the server repo), then:
- uploads
build/_app/immutable/withCache-Control: public,max-age=31536000,immutable, never deleting old files, since pages loaded before the deploy may still request them; - uploads everything else with
Cache-Control: no-cache, copying every file rather than syncing, so even an older build (SKIP_BUILD=1for a rollback by hand) replaces the pages; - deletes files that are no longer in the build, outside
_app/immutable/; - invalidates
/*on CloudFront.
To roll back a deploy, revert the commit on main and push; CI redeploys the previous version.
.github/workflows/ci.yml runs on every push and pull request:
- build:
pnpm install --frozen-lockfile,pnpm check,pnpm lint, the unit tests andpnpm build, on Node 22 with the pnpm version frompackage.json. - deploy: only for pushes to
main, after build passes. It builds again, assumes theilc-client-deployIAM role through GitHub's OIDC provider (no stored AWS keys), runsSKIP_BUILD=1 ./deploy.sh, then checks that/and/static/aboutload on the CloudFront domain.
The role ARN comes from the repository variable AWS_CLIENT_DEPLOY_ROLE_ARN (Settings → Secrets
and variables → Actions → Variables).