diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml
index 4cfd98c..cee780e 100644
--- a/.gitea/workflows/deploy.yml
+++ b/.gitea/workflows/deploy.yml
@@ -35,6 +35,12 @@ jobs:
npm install --include=optional --os=linux --libc=glibc --cpu=x64 sharp
node -e "console.log('sharp', require('sharp').versions.sharp)"
+ # Intet indlæg bygges uden godkendelse; se scripts/verify-approvals.mjs.
+ - name: Verify blog approvals
+ env:
+ PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }}
+ run: node scripts/verify-approvals.mjs
+
- name: Build site
run: npm run build
diff --git a/astro.config.mjs b/astro.config.mjs
index 84e88fc..0ee55e1 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -133,10 +133,15 @@ const buildLastmodMap = () => {
const lastmodByRoute = buildLastmodMap();
+// Preview-build af udkast (PW_PREVIEW=1): hele sitet under /udkast, uden sitemap
+// og service worker. Det serveres kun bag login og må aldrig ende i et index.
+const PREVIEW = process.env.PW_PREVIEW === '1';
+
export default defineConfig({
site: 'https://asger.xn--weirse-fya.dk',
+ ...(PREVIEW ? { base: '/udkast' } : {}),
trailingSlash: 'ignore',
- integrations: [
+ integrations: PREVIEW ? [] : [
sitemap({
changefreq: 'weekly',
priority: 0.7,
diff --git a/scripts/verify-approvals.mjs b/scripts/verify-approvals.mjs
new file mode 100644
index 0000000..a030a8f
--- /dev/null
+++ b/scripts/verify-approvals.mjs
@@ -0,0 +1,65 @@
+// Spærre før build: intet blogindlæg går live uden at være godkendt.
+//
+// Hvert indlæg i src/content/blog skal bære et `godkendt:`-stempel i frontmatter.
+// Stemplet er en HMAC-SHA256 over filens sti + indhold (uden selve stempel-linjen),
+// lavet med en nøgle som kun godkendelsestjenesten og denne workflow kender. Et
+// indlæg der er lagt direkte i repoet, eller ændret efter godkendelse, har derfor
+// intet eller et forkert stempel, og så stopper buildet før noget bliver deployet.
+//
+// Udkast hører ikke hjemme her: repoet er offentligt, så `draft: true` ville stadig
+// udgive teksten via git. Udkast ligger i et privat repo indtil de er godkendt.
+//
+// PUBLISH_KEY= node scripts/verify-approvals.mjs
+import crypto from 'node:crypto';
+import fs from 'node:fs';
+import path from 'node:path';
+
+const DIR = 'src/content/blog';
+const key = process.env.PUBLISH_KEY;
+if (!key) {
+ console.error('PUBLISH_KEY mangler; kan ikke verificere godkendelser.');
+ process.exit(1);
+}
+
+// Skal matche godkendelsestjenesten byte for byte.
+const canonical = (relPath, text) => {
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
+ if (lines[0] === '---') {
+ const end = lines.indexOf('---', 1);
+ if (end > 0) {
+ for (let i = end - 1; i > 0; i--) if (/^godkendt:/.test(lines[i])) lines.splice(i, 1);
+ }
+ }
+ return `${relPath}\n${lines.join('\n')}`;
+};
+
+const stampOf = (text) => {
+ const lines = text.replace(/\r\n/g, '\n').split('\n');
+ const end = lines[0] === '---' ? lines.indexOf('---', 1) : -1;
+ if (end < 0) return { stamp: null, draft: false };
+ const fm = lines.slice(1, end);
+ const g = fm.find((l) => /^godkendt:/.test(l));
+ const draft = fm.some((l) => /^draft:\s*true\s*$/.test(l));
+ return { stamp: g ? g.replace(/^godkendt:\s*/, '').replace(/^['"]|['"]$/g, '') : null, draft };
+};
+
+const files = fs.readdirSync(DIR, { recursive: true })
+ .filter((f) => f.endsWith('.md') || f.endsWith('.mdx'))
+ .map((f) => path.posix.join(DIR, f.split(path.sep).join('/')));
+
+let bad = 0;
+for (const rel of files) {
+ const text = fs.readFileSync(rel, 'utf8');
+ const { stamp, draft } = stampOf(text);
+ if (draft) { console.error(`✗ ${rel}: udkast (draft: true) må ikke ligge i det offentlige repo`); bad++; continue; }
+ if (!stamp) { console.error(`✗ ${rel}: mangler godkendelse`); bad++; continue; }
+ const want = 'v1:' + crypto.createHmac('sha256', Buffer.from(key, 'hex')).update(canonical(rel, text)).digest('hex');
+ const ok = stamp.length === want.length && crypto.timingSafeEqual(Buffer.from(stamp), Buffer.from(want));
+ if (!ok) { console.error(`✗ ${rel}: godkendelsen passer ikke til indholdet (ændret efter godkendelse?)`); bad++; continue; }
+ console.log(`✓ ${rel}`);
+}
+if (bad) {
+ console.error(`\n${bad} indlæg er ikke godkendt. Intet bliver udgivet.`);
+ process.exit(1);
+}
+console.log(`\nAlle ${files.length} indlæg er godkendt.`);
diff --git a/src/consts.ts b/src/consts.ts
index c746a7c..aed3c9a 100644
--- a/src/consts.ts
+++ b/src/consts.ts
@@ -31,3 +31,6 @@ export const PROJECTS = {
hide: [] as string[],
pinned: ['ubl-tools', 'gtk-ahfail', 'pi3-smart-workspace'] as string[],
};
+
+// Sat når udkast bygges til preview (se astro.config.mjs).
+export const PREVIEW = typeof process !== 'undefined' && process.env?.PW_PREVIEW === '1';
diff --git a/src/content.config.ts b/src/content.config.ts
index c29250d..70ba381 100644
--- a/src/content.config.ts
+++ b/src/content.config.ts
@@ -11,6 +11,8 @@ const blog = defineCollection({
updated: z.coerce.date().optional(),
tags: z.array(z.string()).default([]),
draft: z.boolean().default(false),
+ // Godkendelsesstempel; tjekkes af scripts/verify-approvals.mjs før build.
+ godkendt: z.string().optional(),
}),
});
diff --git a/src/content/blog/ah-fail-skaermlaas-rust.md b/src/content/blog/ah-fail-skaermlaas-rust.md
index 520f8a8..9772b9a 100644
--- a/src/content/blog/ah-fail-skaermlaas-rust.md
+++ b/src/content/blog/ah-fail-skaermlaas-rust.md
@@ -4,6 +4,7 @@ description: "Hvorfor jeg droppede ANSI C til fordel for Rust, og hvordan du ins
date: 2026-06-11
tags: [rust, pam, gtklock, wayland, linux, macos]
draft: false
+godkendt: "v1:c8d80c56e01731b150a66ec9dcdfca911d932ea1721145fb79e3cbf46bc26046"
---
Et af mine mere fjollede sideprojekter: **ah-fail**. Taster du dit kodeord forkert
diff --git a/src/content/blog/authentik-oidc-faldgruber.md b/src/content/blog/authentik-oidc-faldgruber.md
index 24f4108..464f0bb 100644
--- a/src/content/blog/authentik-oidc-faldgruber.md
+++ b/src/content/blog/authentik-oidc-faldgruber.md
@@ -4,6 +4,7 @@ description: "redirect_uri-mismatch, X-Forwarded-Proto, sub vs. email og de andr
date: 2026-06-01
tags: [authentik, oidc, sso, selfhost]
draft: false
+godkendt: "v1:6c1d0bac31fd0547c7b0fc4e1c3dbb4ec8bf13d5960f33d8df90766c7700a1cd"
---
Jeg kører **Authentik** som central identitetsudbyder i mit homelab, så jeg kan
diff --git a/src/content/blog/authentik-reverse-proxy-forward-auth.md b/src/content/blog/authentik-reverse-proxy-forward-auth.md
index 8f03fa8..9ab57bb 100644
--- a/src/content/blog/authentik-reverse-proxy-forward-auth.md
+++ b/src/content/blog/authentik-reverse-proxy-forward-auth.md
@@ -4,6 +4,7 @@ description: "Forward auth eller proxy provider? Og hvorfor store auth-headers g
date: 2026-06-05
tags: [authentik, reverse-proxy, oidc, nginx]
draft: false
+godkendt: "v1:c0acd362adab5c144d08b0e2560513d3efa83c851632f4612dbcad2c9920a3bb"
---
Når du sætter **Authentik** bag en reverse proxy, skal du tidligt træffe et valg:
diff --git a/src/content/blog/er-mit-homelab-opdateret.md b/src/content/blog/er-mit-homelab-opdateret.md
index e3db34e..2f16eb5 100644
--- a/src/content/blog/er-mit-homelab-opdateret.md
+++ b/src/content/blog/er-mit-homelab-opdateret.md
@@ -4,6 +4,7 @@ description: "Jeg byggede et lille overblik der selv kigger ind i alle mine cont
date: 2026-06-13
tags: [proxmox, homelab, lxc, selfhost]
draft: false
+godkendt: "v1:8f1db814557e81e8015ac5f9173cbe414141c8373fcfc8149b485d006547ed47"
---
Det startede som det altid gør.. med en lille irritation. Jeg ville tjekke om min
diff --git a/src/content/blog/inject-javascript-npmplus-sub-filter.md b/src/content/blog/inject-javascript-npmplus-sub-filter.md
index c7448e0..c3c31fc 100644
--- a/src/content/blog/inject-javascript-npmplus-sub-filter.md
+++ b/src/content/blog/inject-javascript-npmplus-sub-filter.md
@@ -4,6 +4,7 @@ description: "Sådan sprøjter du et script ind i HTML for hver app bag din reve
date: 2026-06-08
tags: [npmplus, nginx, sub-filter, reverse-proxy, selfhost, matomo]
draft: false
+godkendt: "v1:ab462ce1615149e1a5b65e2b5f5507e97ce231340822408ffc8d5f15956a395a"
---
Jeg ville have **Matomo-analytics** på tværs af alle mine selvhostede apps uden at
diff --git a/src/content/blog/npm-til-npmplus.md b/src/content/blog/npm-til-npmplus.md
index 3ffae29..91852cc 100644
--- a/src/content/blog/npm-til-npmplus.md
+++ b/src/content/blog/npm-til-npmplus.md
@@ -4,6 +4,7 @@ description: "Hvad der gik godt, hvad der brød, og hvorfor backup ikke er til f
date: 2026-06-07
tags: [npmplus, nginx-proxy-manager, reverse-proxy, selfhost]
draft: false
+godkendt: "v1:5d850f0e2717f7928c31972a08f61f35eb7189adfe0e3c776cbd42318e39bfba"
---
I årevis har min edge kørt på **Nginx Proxy Manager** (NPM). Det er en fremragende
diff --git a/src/content/blog/proxmox-hjemmelab.md b/src/content/blog/proxmox-hjemmelab.md
index ba9a4ae..2fbe4fe 100644
--- a/src/content/blog/proxmox-hjemmelab.md
+++ b/src/content/blog/proxmox-hjemmelab.md
@@ -4,6 +4,7 @@ description: "Et kig på opbygningen: reverse proxy, SSO, CI/CD og en masse cont
date: 2026-06-09
tags: [proxmox, selfhost, homelab]
draft: false
+godkendt: "v1:b6deba4fbb95a8418cce1ef7b688f612ce5b4065d60bb5205cd6bf36da8e70e0"
---
Mit hjemmelab kører på **Proxmox VE** med en blanding af LXC-containere og enkelte
diff --git a/src/content/blog/proxmox-lxc-fejl.md b/src/content/blog/proxmox-lxc-fejl.md
index f96e38a..dc825cf 100644
--- a/src/content/blog/proxmox-lxc-fejl.md
+++ b/src/content/blog/proxmox-lxc-fejl.md
@@ -4,6 +4,7 @@ description: "Unprivileged kontra privileged, uid-mapping til nobody, og hvorfor
date: 2026-05-26
tags: [proxmox, lxc, homelab, selfhost]
draft: false
+godkendt: "v1:e9ae34841a773a0941e783eb3d16c4bc772cd923d13f0caf22575682400347a1"
---
Næsten alt i mit homelab kører i **LXC-containere** på **Proxmox VE** frem for
diff --git a/src/content/blog/velkommen.md b/src/content/blog/velkommen.md
index f308c6b..6087f8c 100644
--- a/src/content/blog/velkommen.md
+++ b/src/content/blog/velkommen.md
@@ -4,6 +4,7 @@ description: "Hvad siden er, og hvad du kan forvente at finde her."
date: 2026-06-10
tags: [meta, selfhost]
draft: false
+godkendt: "v1:1d29e33762bae4023bbc57d2870ab97c401aef100074da53c1434d99fff5702f"
---
Velkommen. Det her er min nye personlige side.. bygget med Astro på Bun, hostet på
diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro
index c952f80..8c1c4fa 100644
--- a/src/layouts/Base.astro
+++ b/src/layouts/Base.astro
@@ -5,7 +5,7 @@ import Header from '../components/Header.astro';
import Footer from '../components/Footer.astro';
import Palette from '../components/Palette.astro';
import CookieBanner from '../components/CookieBanner.astro';
-import { SITE } from '../consts';
+import { SITE, PREVIEW } from '../consts';
import '../styles/global.css';
interface Props {
title?: string;
@@ -56,13 +56,18 @@ const { title, description, image, type, publishedTime, modifiedTime, tags, json
});
})();
-
+
+ {PREVIEW && (
+
+ UDKAST, ikke udgivet.
Tilbage til godkendelse
+
+ )}
diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro
index a80bf06..16f0e59 100644
--- a/src/pages/blog/[...slug].astro
+++ b/src/pages/blog/[...slug].astro
@@ -1,7 +1,7 @@
---
import { getCollection, render } from 'astro:content';
import Base from '../../layouts/Base.astro';
-import { SITE } from '../../consts';
+import { SITE, PREVIEW } from '../../consts';
import { blogPosting, breadcrumb } from '../../seo';
export async function getStaticPaths() {
@@ -57,6 +57,8 @@ const crumbs = breadcrumb([
+ {!PREVIEW && (
+
Kommentarer
@@ -72,6 +74,8 @@ const crumbs = breadcrumb([
}
})();
+
+ )}