Init commit!

This commit is contained in:
2026-05-05 23:23:49 +03:00
commit 700bf0cc43
9 changed files with 549 additions and 0 deletions
+5
View File
@@ -0,0 +1,5 @@
/.agent-shell/
public/
.emacs.d/
result
result-*
+44
View File
@@ -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/<name>.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.
+99
View File
@@ -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/<name>.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.
+46
View File
@@ -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
"<nav>
<a href=\"/\">Home</a>
<a href=\"/about.html\">About</a>
</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 "<link rel=\"stylesheet\" href=\"/style.css\">"
: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)
+6
View File
@@ -0,0 +1,6 @@
#+TITLE: About
#+DESCRIPTION: About me
* About me
A few things about me.
+12
View File
@@ -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]]
Generated
+61
View File
@@ -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
}
+112
View File
@@ -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 <subProjectA-store-path>/. $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";
};
};
}
);
}
+164
View File
@@ -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; }