# jeanlucmakiola.de Personal website + blog. Built with [Astro](https://astro.build), deployed on self-hosted [Coolify](https://coolify.io), analytics via self-hosted [Umami](https://umami.is). See [PLANNING.md](./PLANNING.md) for the decisions and roadmap. ## Develop ```sh npm install npm run dev # http://localhost:4321 npm run build # output -> dist/ npm run preview # serve the built site locally ``` ## App privacy policies (rendered from the app repos) `/calendula/privacy` and `/agendula/privacy` hold **no copy** of their prose. Each policy lives in its own app repository, as `docs/PRIVACY.md` with `title` / `description` / `updated` frontmatter, and is the single copy of that policy anywhere: | Page | Source | |------|--------| | `/calendula/privacy` | [`jlmakiola/calendula`](https://codeberg.org/jlmakiola/calendula) → `docs/PRIVACY.md` | | `/agendula/privacy` | [`jlmakiola/agendula`](https://codeberg.org/jlmakiola/agendula) → `docs/PRIVACY.md` | `scripts/sync-external.mjs` clones them (shallow, single-branch, no credentials) into `external/` before every build and dev server — it is wired to `prebuild` and `predev`, **not** to CI, because Coolify builds the site from the repo and a checkout that only ran in a Gitea job would never reach the deployed page. `src/content.config.ts` exposes each file as a content collection; the pages render it and supply the `

` from `title`. **To change a policy, edit it in the app repo, in a PR.** It reaches the live page on the site's next build. ```sh npm run sync:external # refresh the checkouts by hand CALENDULA_REF=some/branch npm run build # build against a branch AGENDULA_LOCAL=../agendula npm run dev # render a working copy, no network ``` The build **fails** if either policy is missing, empty, or malformed. That is deliberate: a privacy page silently rendering nothing is the one failure this arrangement exists to prevent, and it is worth a red deploy. ### Getting a policy edit onto the site A build is what publishes it, so: - **Floor:** the daily `scheduled-deploy` cron rebuilds every morning, so any edit is live within a day without anyone doing anything. - **Immediate:** add a webhook in the app repo (Codeberg → Settings → Webhooks) pointing at the same Coolify deploy URL the cron uses, so a merge to the app's `main` triggers a site rebuild at once. One-time setup per app repo; the secret lives in Coolify, not here. ## Writing a post Create a Markdown file in `src/content/blog/`, e.g. `my-post.md`: ```md --- title: My post description: One-line summary (used for SEO + RSS). pubDate: 2026-06-28 tags: [notes] draft: false # set true to hide from listings/feeds --- Body in Markdown. ``` The filename (minus extension) becomes the URL slug: `/blog/my-post/`. Commit + push → Coolify rebuilds → live. ## Analytics (Umami) Analytics is **off** unless these env vars are set (so it stays off in dev). Set them as **build-time** variables in Coolify: | Variable | Value | |-------------------------|-----------------------------------------| | `PUBLIC_UMAMI_SRC` | `https:///script.js` | | `PUBLIC_UMAMI_WEBSITE` | the website id (UUID) from Umami | ## Deploy on Coolify This repo ships a multi-stage `Dockerfile` (build with Node → serve `dist/` with Caddy on port 80). 1. New Resource → **Dockerfile** (or auto-detect), point at this repo. 2. Set the two `PUBLIC_UMAMI_*` build variables. 3. Set the domain to `jeanlucmakiola.de`; Coolify provisions HTTPS. 4. Deploy Umami separately (Coolify has a one-click Umami template), then copy its script URL + website id into the build variables above. ## SEO - Sitemap at `/sitemap-index.xml` (via `@astrojs/sitemap`) - RSS at `/rss.xml` - Canonical URLs + Open Graph/Twitter tags in `src/components/BaseHead.astro` - `robots.txt` in `public/` - Add an `og-default.png` (1200×630) to `public/` for link previews.