Require an approval stamp on every blog post before building
Build & Deploy / deploy (push) Successful in 3m9s
Build & Deploy / deploy (push) Successful in 3m9s
Posts are written in a private drafts repo and only reach this repo through an approval step that stamps them with an HMAC over path and content. The deploy workflow now runs scripts/verify-approvals.mjs before the build and refuses to build if a post is unstamped, changed after approval, or still marked as a draft (this repo is public, so a draft here is already published). PW_PREVIEW=1 builds the site under /udkast for the private review page: noindex, a banner, no service worker, sitemap or comments. The nine existing posts are stamped as approved. Claude-Session: https://claude.ai/code/session_01JmhJG6JnhXfzNiKv8f2vHu
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
+6
-1
@@ -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,
|
||||
|
||||
@@ -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=<hex> 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.`);
|
||||
@@ -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';
|
||||
|
||||
@@ -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(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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å
|
||||
|
||||
@@ -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
|
||||
});
|
||||
})();
|
||||
</script>
|
||||
<Head {title} {description} {image} {type} {publishedTime} {modifiedTime} {tags} {jsonLd} {noindex} {lang} {alternates} />
|
||||
<Head {title} {description} {image} {type} {publishedTime} {modifiedTime} {tags} {jsonLd} noindex={noindex || PREVIEW} {lang} {alternates} />
|
||||
<ClientRouter />
|
||||
<!-- Registrér service worker'en (PWA/offline). Genereret af @vite-pwa i build;
|
||||
is:inline + public-sti så Astro ikke bundler den. 404 i dev = harmløst. -->
|
||||
<script is:inline src="/registerSW.js"></script>
|
||||
{!PREVIEW && <script is:inline src="/registerSW.js"></script>}
|
||||
</head>
|
||||
<body class="min-h-screen flex flex-col antialiased">
|
||||
{PREVIEW && (
|
||||
<div class="sticky top-0 z-50 bg-amber-400 px-4 py-2 text-center text-sm font-semibold text-black">
|
||||
UDKAST, ikke udgivet. <a href="/udkast/" class="underline">Tilbage til godkendelse</a>
|
||||
</div>
|
||||
)}
|
||||
<Header />
|
||||
<main class="mx-auto w-full max-w-3xl flex-1 px-5 py-12">
|
||||
<slot />
|
||||
|
||||
@@ -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([
|
||||
<div class="prose mt-8"><Content /></div>
|
||||
</article>
|
||||
|
||||
{!PREVIEW && (
|
||||
<Fragment>
|
||||
<section class="mt-16 border-t border-border pt-8">
|
||||
<h2 class="text-2xl font-bold tracking-tight">Kommentarer</h2>
|
||||
<comentario-comments class="mt-6 block" live-update="true"></comentario-comments>
|
||||
@@ -72,6 +74,8 @@ const crumbs = breadcrumb([
|
||||
}
|
||||
})();
|
||||
</script>
|
||||
</Fragment>
|
||||
)}
|
||||
|
||||
<style is:global>
|
||||
.prose pre { position: relative; }
|
||||
|
||||
Reference in New Issue
Block a user