From 700bf0cc4323832474a041b07c452162bea00570 Mon Sep 17 00:00:00 2001 From: Aner Zakobar Date: Tue, 5 May 2026 23:23:49 +0300 Subject: [PATCH] Init commit! --- .gitignore | 5 ++ CLAUDE.md | 44 +++++++++++++ README.org | 99 ++++++++++++++++++++++++++++ build.el | 46 +++++++++++++ content/about.org | 6 ++ content/index.org | 12 ++++ flake.lock | 61 +++++++++++++++++ flake.nix | 112 +++++++++++++++++++++++++++++++ static/style.css | 164 ++++++++++++++++++++++++++++++++++++++++++++++ 9 files changed, 549 insertions(+) create mode 100644 .gitignore create mode 100644 CLAUDE.md create mode 100644 README.org create mode 100644 build.el create mode 100644 content/about.org create mode 100644 content/index.org create mode 100644 flake.lock create mode 100644 flake.nix create mode 100644 static/style.css diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..d006ef4 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +/.agent-shell/ +public/ +.emacs.d/ +result +result-* diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..a6b9f4d --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,44 @@ +# CLAUDE.md — Instructions for the Claude AI assistant + +## Project + +Static personal website. Source in org files, built with Nix + Emacs org-publish. +See README.org for full project structure and maintenance guide. + +## Key files + +| File | Purpose | +|---------------|----------------------------------------------| +| `flake.nix` | Nix flake — build derivation, run apps | +| `build.el` | org-publish config (pages + static assets) | +| `content/` | Org source files → HTML pages | +| `static/` | Static assets (CSS, images, fonts) | + +## Conventions + +- **New pages**: add `content/.org`, add nav link to `site-nav` in `build.el` +- **Sub-projects**: follow the three-step pattern in README.org (input → let binding → installPhase copy) +- **CSS**: edit `static/style.css`; use existing custom properties (`--bg`, `--fg`, etc.) before adding new ones +- **No JS** unless explicitly requested +- **No new files** unless clearly necessary — prefer editing existing ones + +## Build + +```sh +nix build # produces result/ symlink with the full static site +nix run # build + serve at http://localhost:8080 +``` + +The build runs `emacs --batch --load ./build.el` inside a Nix sandbox. +`org-publish-use-timestamps-flag` is `nil` so every build is a full rebuild. + +## Architecture notes + +- Multi-arch: `x86_64-linux` and `aarch64-linux` (CI runs on Raspberry Pi) +- `emacs-nox` is used (headless, no display required) +- Source is filtered with `pkgs.lib.cleanSource` — avoid relying on files that would be excluded (e.g. `.git`) +- `public/` is gitignored; never commit it + +## Updating README.org + +Keep README.org up to date as the project evolves. It is the canonical human-readable documentation. diff --git a/README.org b/README.org new file mode 100644 index 0000000..2edd1b2 --- /dev/null +++ b/README.org @@ -0,0 +1,99 @@ +#+TITLE: Personal Site +#+DESCRIPTION: Source for my personal website + +* Overview + +Static personal website built with [[https://nixos.org/][Nix]] and [[https://orgmode.org/manual/Publishing.html][org-publish]]. +Deployed to Cloudflare Pages via CI/CD on a Gitea runner (Raspberry Pi). + +* Quick Start + +#+begin_src sh +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 +#+end_src + +* Project Structure + +#+begin_example +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 +#+end_example + +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 + +1. Create =content/.org= with at minimum: + #+begin_src org + #+TITLE: Page Title + + Content here. + #+end_src + +2. Add a link to the nav in =build.el= (the =site-nav= variable at the top). + +** Adding a sub-project + +Sub-projects are separate Nix flakes that produce a static site as their default package. + +1. Add the flake input in =flake.nix=: + #+begin_src nix + project-a = { + url = "path:/path/to/project-a"; # or git URL + inputs.nixpkgs.follows = "nixpkgs"; + }; + #+end_src + +2. Expose the package in the =let= block: + #+begin_src nix + subProjectA = inputs.project-a.packages.${system}.default; + #+end_src + +3. Copy it into the site output in =installPhase=: + #+begin_src nix + mkdir -p $out/project-a + cp -r ${subProjectA}/. $out/project-a/ + #+end_src + +4. 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 + +#+begin_src sh +nix flake update +#+end_src + +* Deployment + +The site is deployed to Cloudflare Pages automatically on push via a Gitea Actions +workflow running on a Raspberry Pi runner (=aarch64-linux=). +The =nix build= output is uploaded directly as the Pages deployment artifact. diff --git a/build.el b/build.el new file mode 100644 index 0000000..bbb93c4 --- /dev/null +++ b/build.el @@ -0,0 +1,46 @@ +;; build.el — org-publish configuration for personal site +(require 'org) +(require 'ox-publish) +(require 'ox-html) + +(setq org-publish-use-timestamps-flag nil + org-export-with-broken-links t + org-html-validation-link nil + org-html-head-include-default-style nil + org-html-head-include-scripts nil) + +(setq site-nav + "") + +(setq org-publish-project-alist + `(("site-pages" + :base-directory "./content" + :base-extension "org" + :publishing-directory "./public" + :recursive t + :publishing-function org-html-publish-to-html + :html-head-include-default-style nil + :html-head-include-scripts nil + :html-head "" + :html-preamble ,site-nav + :html-postamble nil + :with-author nil + :with-creator nil + :with-timestamps nil + :section-numbers nil + :table-of-contents nil) + + ("site-static" + :base-directory "./static" + :base-extension "css\\|js\\|png\\|jpg\\|jpeg\\|gif\\|svg\\|ico\\|woff2\\|woff\\|ttf" + :publishing-directory "./public" + :recursive t + :publishing-function org-publish-attachment) + + ("site" + :components ("site-pages" "site-static")))) + +(org-publish "site" t) diff --git a/content/about.org b/content/about.org new file mode 100644 index 0000000..9deff18 --- /dev/null +++ b/content/about.org @@ -0,0 +1,6 @@ +#+TITLE: About +#+DESCRIPTION: About me + +* About me + +A few things about me. diff --git a/content/index.org b/content/index.org new file mode 100644 index 0000000..f4560b8 --- /dev/null +++ b/content/index.org @@ -0,0 +1,12 @@ +#+TITLE: Home +#+DESCRIPTION: Personal website + +* Hello + +Welcome to my personal site. I'm Aner. + +This is a place for writing, projects, and whatever else I find worth putting down. + +** Projects + +- [[file:about.org][About me]] diff --git a/flake.lock b/flake.lock new file mode 100644 index 0000000..7c144fd --- /dev/null +++ b/flake.lock @@ -0,0 +1,61 @@ +{ + "nodes": { + "flake-utils": { + "inputs": { + "systems": "systems" + }, + "locked": { + "lastModified": 1731533236, + "narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "11707dc2f618dd54ca8739b309ec4fc024de578b", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, + "nixpkgs": { + "locked": { + "lastModified": 1777918403, + "narHash": "sha256-7QiZv0LcW1yIOLo2LNuCQjWon1Z1r99FwK24hbtBOF4=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "afc5551119aae6eab73a95c1960891cfe63204f6", + "type": "github" + }, + "original": { + "owner": "NixOS", + "ref": "nixpkgs-unstable", + "repo": "nixpkgs", + "type": "github" + } + }, + "root": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs" + } + }, + "systems": { + "locked": { + "lastModified": 1681028828, + "narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=", + "owner": "nix-systems", + "repo": "default", + "rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e", + "type": "github" + }, + "original": { + "owner": "nix-systems", + "repo": "default", + "type": "github" + } + } + }, + "root": "root", + "version": 7 +} diff --git a/flake.nix b/flake.nix new file mode 100644 index 0000000..2c53746 --- /dev/null +++ b/flake.nix @@ -0,0 +1,112 @@ +{ + description = "Personal website"; + + inputs = { + nixpkgs.url = "github:NixOS/nixpkgs/nixpkgs-unstable"; + flake-utils.url = "github:numtide/flake-utils"; + + # Sub-projects: uncomment and replace URLs when ready + # project-a = { + # url = "path:/path/to/project-a"; + # inputs.nixpkgs.follows = "nixpkgs"; + # }; + # project-b = { + # url = "path:/path/to/project-b"; + # inputs.nixpkgs.follows = "nixpkgs"; + # }; + }; + + outputs = { self, nixpkgs, flake-utils, ... } @ inputs: + flake-utils.lib.eachSystem [ "x86_64-linux" "aarch64-linux" ] (system: + let + pkgs = import nixpkgs { inherit system; }; + + # Sub-project outputs — uncomment when inputs above are added + # subProjectA = inputs.project-a.packages.${system}.default; + # subProjectB = inputs.project-b.packages.${system}.default; + + site = pkgs.stdenv.mkDerivation { + name = "personal-site"; + src = pkgs.lib.cleanSource ./.; + + nativeBuildInputs = [ pkgs.emacs-nox ]; + + buildPhase = '' + mkdir -p public + export HOME=$(mktemp -d) + emacs --batch --load ./build.el + ''; + + installPhase = '' + cp -r public/. $out/ + + # Sub-project integration: when adding a sub-project, add lines like: + # mkdir -p $out/project-a + # cp -r /. $out/project-a/ + ''; + }; + + serve = pkgs.writeShellApplication { + name = "serve"; + runtimeInputs = [ pkgs.python3 ]; + text = '' + echo "Serving site at http://localhost:8080" + python -m http.server --bind 0.0.0.0 8080 -d ${site} + ''; + }; + + tunnel = pkgs.writeShellApplication { + name = "tunnel"; + runtimeInputs = [ pkgs.python3 pkgs.cloudflared pkgs.qrencode ]; + text = '' + echo "Starting local server and Cloudflare tunnel..." + echo "" + + echo "Starting local server on http://localhost:8080..." + python -m http.server --bind 0.0.0.0 8080 -d ${site} & + SERVER_PID=$! + + sleep 2 + + echo "" + echo "Starting HTTPS tunnel..." + echo "Use the QR code below on your device:" + echo "" + + cloudflared tunnel --url http://localhost:8080 2>&1 | \ + while IFS= read -r line; do + echo "$line" + if echo "$line" | grep -q "https://.*trycloudflare.com"; then + URL=$(echo "$line" | grep -o "https://[^ ]*trycloudflare.com") + echo "" + echo "==========================================" + echo "TUNNEL URL: $URL" + echo "==========================================" + echo "" + echo "QR Code:" + echo "" + qrencode -t ANSIUTF8 "$URL" + echo "" + fi + done + + kill $SERVER_PID 2>/dev/null + ''; + }; + + in { + packages.default = site; + + apps = { + default = { + type = "app"; + program = "${serve}/bin/serve"; + }; + tunnel = { + type = "app"; + program = "${tunnel}/bin/tunnel"; + }; + }; + } + ); +} diff --git a/static/style.css b/static/style.css new file mode 100644 index 0000000..217de6e --- /dev/null +++ b/static/style.css @@ -0,0 +1,164 @@ +/* ── Custom Properties ───────────────────────────────── */ +:root { + --bg: #ffffff; + --fg: #1a1a1a; + --muted: #666666; + --accent: #2563eb; + --border: #e5e5e5; + --code-bg: #f5f5f5; + --max-width: 740px; + --font-sans: system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; + --font-mono: ui-monospace, "Cascadia Code", "Fira Code", monospace; +} + +@media (prefers-color-scheme: dark) { + :root { + --bg: #111111; + --fg: #e8e8e8; + --muted: #888888; + --accent: #60a5fa; + --border: #2a2a2a; + --code-bg: #1c1c1c; + } +} + +/* ── Reset ───────────────────────────────────────────── */ +*, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; } +html { font-size: 16px; -webkit-text-size-adjust: 100%; } +img, video, svg { max-width: 100%; display: block; } + +/* ── Base ────────────────────────────────────────────── */ +body { + background: var(--bg); + color: var(--fg); + font-family: var(--font-sans); + line-height: 1.7; + padding: 0 1.25rem; +} + +/* ── Layout — center preamble and content ────────────── */ +#preamble, +#content, +#postamble { + max-width: var(--max-width); + margin-left: auto; + margin-right: auto; +} + +/* ── Navigation ──────────────────────────────────────── */ +#preamble nav { + display: flex; + gap: 1.5rem; + padding: 1.25rem 0; + border-bottom: 1px solid var(--border); + margin-bottom: 2.5rem; +} + +#preamble nav a { + color: var(--fg); + text-decoration: none; + font-weight: 500; + font-size: 0.95rem; + letter-spacing: 0.01em; +} + +#preamble nav a:hover { color: var(--accent); } + +/* ── Content ─────────────────────────────────────────── */ +#content { + padding-bottom: 4rem; +} + +/* ── Typography ──────────────────────────────────────── */ +h1, h2, h3, h4, h5, h6 { + line-height: 1.3; + margin-top: 2rem; + margin-bottom: 0.6rem; + font-weight: 600; +} + +h1 { font-size: 2rem; margin-top: 0; } +h2 { font-size: 1.4rem; } +h3 { font-size: 1.15rem; } +h4 { font-size: 1rem; } + +p { margin-bottom: 1rem; } + +a { + color: var(--accent); + text-decoration: underline; + text-decoration-color: transparent; + transition: text-decoration-color 0.15s; +} +a:hover { text-decoration-color: currentColor; } + +ul, ol { + padding-left: 1.5rem; + margin-bottom: 1rem; +} +li { margin-bottom: 0.3rem; } + +blockquote { + border-left: 3px solid var(--border); + padding-left: 1rem; + color: var(--muted); + margin: 1.5rem 0; +} + +hr { + border: none; + border-top: 1px solid var(--border); + margin: 2rem 0; +} + +/* ── Code ────────────────────────────────────────────── */ +code { + font-family: var(--font-mono); + font-size: 0.875em; + background: var(--code-bg); + padding: 0.15em 0.4em; + border-radius: 3px; +} + +pre { + font-family: var(--font-mono); + font-size: 0.875rem; + background: var(--code-bg); + border: 1px solid var(--border); + border-radius: 6px; + padding: 1rem 1.25rem; + overflow-x: auto; + margin-bottom: 1.25rem; + line-height: 1.5; +} + +pre code { + background: none; + padding: 0; + font-size: inherit; +} + +/* org-mode wraps src blocks in .src */ +.src { background: var(--code-bg); } + +/* ── Tables ──────────────────────────────────────────── */ +table { + width: 100%; + border-collapse: collapse; + margin-bottom: 1.25rem; + font-size: 0.95rem; +} +th, td { + padding: 0.5rem 0.75rem; + border: 1px solid var(--border); + text-align: left; +} +th { + background: var(--code-bg); + font-weight: 600; +} + +/* ── Org-specific cleanup ────────────────────────────── */ +.outline-2, .outline-3, .outline-4 { margin-top: 1.5rem; } +.tag { display: none; } +#table-of-contents { margin-bottom: 2rem; }