jeanlucmakiola.de
Personal website + blog. Built with Astro, deployed on self-hosted Coolify, analytics via self-hosted Umami.
See PLANNING.md for the decisions and roadmap.
Develop
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 → docs/PRIVACY.md |
/agendula/privacy |
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 <h1> 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.
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-deploycron 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
maintriggers 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:
---
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://<your-umami>/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).
- New Resource → Dockerfile (or auto-detect), point at this repo.
- Set the two
PUBLIC_UMAMI_*build variables. - Set the domain to
jeanlucmakiola.de; Coolify provisions HTTPS. - 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.txtinpublic/- Add an
og-default.png(1200×630) topublic/for link previews.