Personal Site
Overview
Static personal website built with Nix and org-publish. Deployed to Cloudflare Pages via CI/CD on a Gitea runner (Raspberry Pi).
Quick Start
nix run # build site and serve at http://localhost:8080
nix run .#tunnel # serve + Cloudflare HTTPS tunnel + QR code
nix build # build only → result/ symlink
Project Structure
personal-site/ ├── flake.nix Nix flake — inputs, build derivation, run apps ├── build.el Emacs Lisp org-publish configuration ├── content/ Source pages as org files │ ├── index.org Homepage → /index.html │ └── about.org About page → /about.html ├── static/ Static assets, copied verbatim │ └── style.css Site stylesheet ├── README.org This file └── CLAUDE.md Instructions for the Claude AI assistant
The build produces a public/ directory (gitignored) which becomes the nix build output.
Sub-projects are embedded as subdirectories (e.g. /project-a/).
Maintenance
Adding a page
-
Create
content/<name>.orgwith at minimum:#+TITLE: Page Title Content here. - Add a link to the nav in
build.el(thesite-navvariable at the top).
Adding a sub-project
Sub-projects are separate Nix flakes that produce a static site as their default package.
-
Add the flake input in
flake.nix:project-a = { url = "path:/path/to/project-a"; # or git URL inputs.nixpkgs.follows = "nixpkgs"; }; -
Expose the package in the
letblock:subProjectA = inputs.project-a.packages.${system}.default; -
Copy it into the site output in
installPhase:mkdir -p $out/project-a cp -r ${subProjectA}/. $out/project-a/ - Optionally add a nav link in
build.el.
Styling
static/style.css uses CSS custom properties defined in :root.
Dark mode is applied automatically via @media (prefers-color-scheme: dark).
Key variables:
| Variable | Purpose |
|---|---|
--bg |
Page background |
--fg |
Body text |
--accent |
Links and highlights |
--muted |
Secondary text |
--border |
Dividers and box borders |
--code-bg |
Code block background |
--max-width |
Content column width |
Updating dependencies
nix flake update
Deployment
The site is deployed to Cloudflare Pages automatically on push via a Gitea Actions
workflow (.gitea/workflows/deploy.yml) running on a Raspberry Pi runner (aarch64-linux).
The nix build output is uploaded directly as the Pages deployment artifact.
First-time setup
1. Create a Cloudflare Pages project
In the Cloudflare dashboard → Pages → Create a project → Direct Upload.
Name it personal-site (must match the --project-name flag in the workflow).
Skip the initial upload — CI will handle it.
Alternatively, create it with Wrangler locally:
npx wrangler pages project create personal-site
2. Get your Cloudflare credentials
- Account ID: Cloudflare dashboard → right sidebar, or any zone overview page.
-
API Token: My Profile → API Tokens → Create Token. Use the Edit Cloudflare Pages template, or create a custom token with:
- Permission:
Cloudflare Pages→Edit - Account resource: your account
- Permission:
3. Add secrets to Gitea
In the Gitea repo → Settings → Secrets → Actions, add:
| Secret name | Value |
|---|---|
CLOUDFLARE_API_TOKEN |
the API token from step 2 |
CLOUDFLARE_ACCOUNT_ID |
your numeric account ID |
4. Verify the runner label
The workflow uses runs-on: ubuntu-latest.
Check your Gitea runner's label in Site Administration → Runners and update the
workflow if your runner uses a different label (e.g. nix, self-hosted, etc.).
The runner needs Nix (with flakes enabled) and Node.js on PATH.
If Node.js is not available system-wide you can prepend the build step with:
nix-shell -p nodejs --run "npx --yes wrangler@3 pages deploy ..."
5. Push to master
The workflow triggers on every push to master.
Check the Actions tab in Gitea to monitor runs.