Each policy now has exactly one copy: docs/PRIVACY.md in the app's own repository. The pages keep their URLs and chrome and render that file through a content collection, so the published page and the app's own documentation cannot drift. scripts/sync-external.mjs shallow-clones both repos into external/ from prebuild and predev — not from CI: Coolify builds the site from the repo, so a checkout that only ran in a Gitea job would never reach the deploy. It falls back to the raw file if git is unavailable, and takes <APP>_REF or <APP>_LOCAL for work against a branch or an unpushed working copy. A missing, empty or malformed policy fails the build, verified against the real image: the deploy stops rather than publishing an empty privacy page.
108 lines
3.9 KiB
Markdown
108 lines
3.9 KiB
Markdown
# 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 `<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.
|
||
|
||
```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://<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).
|
||
|
||
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.
|