diff --git a/.gitea/workflows/deploy.yml b/.gitea/workflows/deploy.yml index f9bbf79..2c5564d 100644 --- a/.gitea/workflows/deploy.yml +++ b/.gitea/workflows/deploy.yml @@ -15,31 +15,23 @@ jobs: # Fuld historik til sitemap-lastmod og IndexNow fetch-depth: 0 - - name: Setup Node - uses: actions/setup-node@v4 + - uses: actions/setup-python@v5 with: - node-version: 22 - - # Runneren udelader optional deps, så sharp-binæren installeres eksplicit - - name: Install dependencies - env: - npm_config_omit: "" - npm_config_optional: "true" - run: | - npm install --include=optional - npm install --include=optional --os=linux --libc=glibc --cpu=x64 sharp - node -e "console.log('sharp', require('sharp').versions.sharp)" + python-version: "3.12" - name: Verify blog approvals env: PUBLISH_KEY: ${{ secrets.PUBLISH_KEY }} - run: node scripts/verify-approvals.mjs + run: python3 scripts/verify-approvals.py + + - name: Fetch zola and tailwind + run: sh scripts/hent-vaerktoej.sh - name: Build site - run: npm run build + run: sh scripts/build.sh # tar over SSH med atomisk swap (runner-imaget har ikke rsync) - - name: Deploy dist/ over SSH + - name: Deploy public/ over SSH env: DEPLOY_HOST: ${{ secrets.DEPLOY_HOST }} DEPLOY_USER: ${{ vars.DEPLOY_USER }} @@ -55,7 +47,7 @@ jobs: SSH="ssh -i $HOME/.ssh/deploy_key -o StrictHostKeyChecking=accept-new" ssh-keyscan -t ed25519 "$DEPLOY_HOST" >> ~/.ssh/known_hosts 2>/dev/null || true $SSH "$DEPLOY_USER@$DEPLOY_HOST" "rm -rf $DEPLOY_PATH.new && mkdir -p $DEPLOY_PATH.new" - tar -C dist -czf - . | $SSH "$DEPLOY_USER@$DEPLOY_HOST" \ + tar -C public -czf - . | $SSH "$DEPLOY_USER@$DEPLOY_HOST" \ "tar -C $DEPLOY_PATH.new -xzf - \ && rm -rf $DEPLOY_PATH.old \ && { mv $DEPLOY_PATH $DEPLOY_PATH.old 2>/dev/null || true; } \ @@ -66,4 +58,4 @@ jobs: continue-on-error: true env: GITHUB_EVENT_BEFORE: ${{ github.event.before }} - run: node scripts/indexnow.mjs + run: python3 scripts/indexnow.py diff --git a/.gitignore b/.gitignore index 02da9ba..79d635d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,7 @@ -node_modules/ -dist/ -.astro/ .DS_Store -bun.lock* +public/ +static/css/ +data/gitea.json +static/processed_images/ +.tools/ +__pycache__/ diff --git a/README.md b/README.md index 123b9ea..36b4bb5 100644 --- a/README.md +++ b/README.md @@ -1,13 +1,14 @@ # asger.weirsoe.dk -Personlig side (CV + blog) bygget med Astro + Bun + Tailwind. Statisk output, selvhostet bag nginx, deployet via Gitea Actions. +Personlig side (CV + blog) bygget med Zola + Tailwind. Statisk output, selvhostet bag nginx, deployet via Gitea Actions. Ingen Node i byggekæden. ## Udvikling ``` -bun install -bun run dev # http://localhost:4321 -bun run build # -> dist/ +sh scripts/hent-vaerktoej.sh # zola og tailwindcss til .tools/ +sh scripts/build.sh # -> public/ ``` -## Nyt blogindlæg -Læg en `.md`-fil i `src/content/blog/` med frontmatter (title, date, tags, description, draft). Push til main → bygges og deployes automatisk. +`build.sh` henter repos og heatmap fra Gitea (`scripts/prebuild.py`), bygger CSS, kører `zola build` og efterbehandler (`scripts/postbuild.py`: sitemap, service worker). `PW_PREVIEW=1` bygger preview'et under `/udkast`. + +## Blogindlæg +Indlæg ligger som `content/blog//index.md` med billederne ved siden af, men de lægges ikke her i hånden. Et indlæg skrives i det private repo agw/pw-drafts og udgives med "Godkend og udgiv", som stempler det og committer det hertil. CI (`scripts/verify-approvals.py`) nægter at bygge et indlæg uden gyldigt stempel. diff --git a/assets/inter.css b/assets/inter.css new file mode 100644 index 0000000..970348a --- /dev/null +++ b/assets/inter.css @@ -0,0 +1,62 @@ +@font-face { + font-family: 'Inter Variable'; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../fonts/inter-cyrillic-ext-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0460-052F,U+1C80-1C8A,U+20B4,U+2DE0-2DFF,U+A640-A69F,U+FE2E-FE2F; +} + +@font-face { + font-family: 'Inter Variable'; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../fonts/inter-cyrillic-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0301,U+0400-045F,U+0490-0491,U+04B0-04B1,U+2116; +} + +@font-face { + font-family: 'Inter Variable'; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../fonts/inter-greek-ext-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+1F00-1FFF; +} + +@font-face { + font-family: 'Inter Variable'; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../fonts/inter-greek-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0370-0377,U+037A-037F,U+0384-038A,U+038C,U+038E-03A1,U+03A3-03FF; +} + +@font-face { + font-family: 'Inter Variable'; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../fonts/inter-vietnamese-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0102-0103,U+0110-0111,U+0128-0129,U+0168-0169,U+01A0-01A1,U+01AF-01B0,U+0300-0301,U+0303-0304,U+0308-0309,U+0323,U+0329,U+1EA0-1EF9,U+20AB; +} + +@font-face { + font-family: 'Inter Variable'; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../fonts/inter-latin-ext-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0100-02BA,U+02BD-02C5,U+02C7-02CC,U+02CE-02D7,U+02DD-02FF,U+0304,U+0308,U+0329,U+1D00-1DBF,U+1E00-1E9F,U+1EF2-1EFF,U+2020,U+20A0-20AB,U+20AD-20C0,U+2113,U+2C60-2C7F,U+A720-A7FF; +} + +@font-face { + font-family: 'Inter Variable'; + font-style: normal; + font-display: swap; + font-weight: 100 900; + src: url(../fonts/inter-latin-wght-normal.woff2) format('woff2-variations'); + unicode-range: U+0000-00FF,U+0131,U+0152-0153,U+02BB-02BC,U+02C6,U+02DA,U+02DC,U+0304,U+0308,U+0329,U+2000-206F,U+20AC,U+2122,U+2191,U+2193,U+2212,U+2215,U+FEFF,U+FFFD; +} diff --git a/assets/site.css b/assets/site.css new file mode 100644 index 0000000..e62372b --- /dev/null +++ b/assets/site.css @@ -0,0 +1,83 @@ +/* Bygges med Tailwind standalone-CLI til static/css/site.css (se scripts/build.sh). */ +@import "tailwindcss" source(none); +@source "../templates"; +@source "../content"; +@import "./inter.css"; + +/* Fælles tema (Weircon-wind) */ +@import "../vendor/weirsoe-theme/adapters/tailwind.css"; +@import "../vendor/weirsoe-theme/weirsoe-theme.css" layer(components); + +@theme { + --font-sans: "Inter Variable", ui-sans-serif, system-ui, -apple-system, sans-serif; +} +html[data-theme="terminal"] { + --font-sans: ui-monospace, "Cascadia Code", "JetBrains Mono", Menlo, Consolas, monospace; +} +:root { --wt-font-sans: var(--font-sans); } +@layer base { a { color: inherit; } } + +/* Samme fade mellem sider som før */ +@view-transition { navigation: auto; } + +/* Kommandopalette */ +.palette-overlay { + position: fixed; inset: 0; z-index: 100; + background: color-mix(in srgb, var(--color-bg) 60%, transparent); + backdrop-filter: blur(4px); + display: flex; align-items: flex-start; justify-content: center; padding-top: 15vh; +} +.palette-box { + width: min(36rem, calc(100vw - 2rem)); + border: 1px solid var(--color-border); border-radius: 1rem; + background: var(--color-surface); box-shadow: 0 24px 64px rgb(0 0 0 / .45); + overflow: hidden; +} +.palette-input { + width: 100%; padding: .9rem 1.1rem; font-size: 1rem; + background: transparent; color: var(--color-text); border: 0; outline: none; + border-bottom: 1px solid var(--color-border); +} +.palette-list { max-height: 50vh; overflow-y: auto; padding: .35rem; } +.palette-item { + display: flex; justify-content: space-between; align-items: center; gap: 1rem; + padding: .55rem .8rem; border-radius: .6rem; cursor: pointer; font-size: .925rem; +} +.palette-item.is-selected, .palette-item:hover:not(.palette-empty) { background: var(--color-surface-2); } +.palette-empty { color: var(--color-muted); cursor: default; } +.palette-badge { + font-size: .7rem; color: var(--color-muted); + border: 1px solid var(--color-border); border-radius: 999px; padding: .05rem .5rem; +} +.palette-hint { + padding: .5rem 1.1rem; font-size: .72rem; color: var(--color-muted); + border-top: 1px solid var(--color-border); +} + +/* Kodeblokke */ +.prose pre { position: relative; } +.giallo-l { display: inline-block; min-height: 1lh; width: 100%; } +.copy-btn { + position: absolute; top: .45rem; right: .45rem; + font-size: .68rem; line-height: 1; padding: .35rem .6rem; + color: var(--color-muted); background: var(--color-surface-2); + border: 1px solid var(--color-border); border-radius: .5rem; + opacity: 0; transition: opacity .15s ease; cursor: pointer; +} +.prose pre:hover .copy-btn, .copy-btn:focus-visible { opacity: 1; } +.copy-btn:hover { color: var(--color-text); } +.mermaid-diagram { margin: 1.5rem 0; overflow-x: auto; } +.mermaid-diagram svg { max-width: 100%; height: auto; } + +/* /now */ +.heatmap { display: flex; gap: 3px; width: max-content; } +.hm-col { display: flex; flex-direction: column; gap: 3px; } +.hm-cell { width: 11px; height: 11px; border-radius: 2.5px; background: var(--color-surface-2); } +.hm-1 { background: color-mix(in srgb, var(--color-accent) 30%, var(--color-surface-2)); } +.hm-2 { background: color-mix(in srgb, var(--color-accent) 55%, var(--color-surface-2)); } +.hm-3 { background: color-mix(in srgb, var(--color-accent) 80%, var(--color-surface-2)); } +.hm-4 { background: var(--color-accent); } + +/* /profiler */ +.copy { transition: color 0.15s; } +.copy:hover { color: var(--color-accent); text-decoration: underline; text-underline-offset: 3px; } diff --git a/astro.config.mjs b/astro.config.mjs deleted file mode 100644 index 105fb99..0000000 --- a/astro.config.mjs +++ /dev/null @@ -1,175 +0,0 @@ -import { defineConfig } from 'astro/config'; -import sitemap from '@astrojs/sitemap'; -import AstroPWA from '@vite-pwa/astro'; -import tailwindcss from '@tailwindcss/vite'; -import { execFileSync } from 'node:child_process'; -import fs from 'node:fs'; -import path from 'node:path'; -import { fileURLToPath } from 'node:url'; - -const ROOT = path.dirname(fileURLToPath(import.meta.url)); - -// lastmod til sitemap: sidste commit for statiske sider, frontmatter for blogindlæg. -// Kræver fuld git-historik (fetch-depth: 0); ellers ingen lastmod. - -const gitLastModified = (file) => { - try { - const out = execFileSync('git', ['log', '-1', '--format=%cI', '--', file], { - cwd: ROOT, - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); - return out || null; - } catch { - return null; - } -}; - -const gitHistoryIsUsable = (() => { - try { - const n = execFileSync('git', ['rev-list', '--count', 'HEAD'], { - cwd: ROOT, - encoding: 'utf8', - stdio: ['ignore', 'pipe', 'ignore'], - }).trim(); - return Number(n) > 1; - } catch { - return false; - } -})(); - -const walk = (dir) => - fs.existsSync(dir) - ? fs.readdirSync(dir, { withFileTypes: true }).flatMap((e) => { - const p = path.join(dir, e.name); - return e.isDirectory() ? walk(p) : [p]; - }) - : []; - -// src/pages/foo.astro -> /foo/ ; src/pages/index.astro -> / ; [dynamiske] springes over -const routeForPage = (file) => { - const rel = path.relative(path.join(ROOT, 'src/pages'), file).replace(/\\/g, '/'); - if (rel.includes('[') || rel.startsWith('404.')) return null; - if (!rel.endsWith('.astro')) return null; - const noExt = rel.slice(0, -'.astro'.length); - if (noExt === 'index') return '/'; - return `/${noExt.replace(/\/index$/, '')}/`; -}; - -// Minimal frontmatter-læsning — vi skal kun bruge date/updated/tags/draft. -const readFrontmatter = (file) => { - const src = fs.readFileSync(file, 'utf8'); - const m = src.match(/^---\r?\n([\s\S]*?)\r?\n---/); - if (!m) return {}; - const fm = {}; - for (const line of m[1].split(/\r?\n/)) { - const kv = line.match(/^(\w+):\s*(.*)$/); - if (!kv) continue; - const [, k, rawv] = kv; - const v = rawv.trim().replace(/^['"]|['"]$/g, ''); - if (k === 'tags') { - const inline = v.match(/^\[(.*)\]$/); - fm.tags = inline - ? inline[1].split(',').map((t) => t.trim().replace(/^['"]|['"]$/g, '')).filter(Boolean) - : []; - } else { - fm[k] = v; - } - } - return fm; -}; - -const iso = (v) => { - const d = new Date(v); - return Number.isNaN(d.getTime()) ? null : d.toISOString(); -}; -const newest = (dates) => dates.filter(Boolean).sort().at(-1) ?? null; - -const buildLastmodMap = () => { - const map = new Map(); - if (!gitHistoryIsUsable) return map; - - for (const file of walk(path.join(ROOT, 'src/pages'))) { - const route = routeForPage(file); - if (!route) continue; - const d = gitLastModified(file); - if (d) map.set(route, d); - } - - // Blogindlæg: frontmatter er sandheden om hvornår indholdet sidst ændrede sig. - const postDates = []; - const tagDates = new Map(); - for (const file of walk(path.join(ROOT, 'src/content/blog'))) { - if (!file.endsWith('.md')) continue; - const fm = readFrontmatter(file); - if (fm.draft === 'true') continue; - const d = iso(fm.updated || fm.date); - if (!d) continue; - const slug = path.basename(file, '.md'); - map.set(`/blog/${slug}/`, d); - postDates.push(d); - for (const t of fm.tags ?? []) { - const key = t.toLowerCase(); - tagDates.set(key, newest([tagDates.get(key), d])); - } - } - - // Oversigtssider er lige så friske som det nyeste de viser. - const latest = newest(postDates); - if (latest) { - map.set('/blog/', latest); - map.set('/tags/', latest); - for (const [tag, d] of tagDates) map.set(`/tags/${tag}/`, d); - } - return map; -}; - -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: PREVIEW ? [] : [ - sitemap({ - changefreq: 'weekly', - priority: 0.7, - customPages: ['https://asger.xn--weirse-fya.dk/tarballz/'], - // noindex-sider og omdirigeringer - filter: (page) => { - const p = new URL(page).pathname.replace(/\/$/, ''); - return !p.startsWith('/tags') && !p.startsWith('/peppol') - && !['/cookies', '/status', '/chat'].includes(p); - }, - serialize(item) { - const pathname = new URL(item.url).pathname; - if (pathname === '/') item.priority = 1.0; - const lastmod = lastmodByRoute.get(pathname); - if (lastmod) item.lastmod = lastmod; - // Kanonisk form uden afsluttende skråstreg (undtagen /tarballz/) - if (pathname !== '/' && pathname !== '/tarballz/') item.url = item.url.replace(/\/$/, ''); - return item; - }, - }), - AstroPWA({ - registerType: 'autoUpdate', - manifest: false, - includeManifestIcons: false, - workbox: { - globPatterns: ['**/*.{html,css,js,svg,png,woff2,xml,txt,webmanifest}'], - cleanupOutdatedCaches: true, - clientsClaim: true, - skipWaiting: true, - // Ingen fallback til forsiden, så /tarballz/ og 404 virker - navigateFallback: null, - }, - devOptions: { enabled: false }, - }), - ], - prefetch: { prefetchAll: true, defaultStrategy: 'viewport' }, - vite: { plugins: [tailwindcss()] }, -}); diff --git a/content/_index.md b/content/_index.md new file mode 100644 index 0000000..2265439 --- /dev/null +++ b/content/_index.md @@ -0,0 +1,5 @@ +--- +template: sider/forside.html +extra: + alternates: [{lang: da, path: /}, {lang: en, path: /en}] +--- diff --git a/content/blog/_index.md b/content/blog/_index.md new file mode 100644 index 0000000..c738e8d --- /dev/null +++ b/content/blog/_index.md @@ -0,0 +1,7 @@ +--- +title: "Blog" +description: "Indlæg om selvhosting, Proxmox, AI og udvikling." +template: sider/blog.html +sort_by: date +page_template: indlaeg.html +--- diff --git a/src/content/blog/ah-fail-skaermlaas-rust.md b/content/blog/ah-fail-skaermlaas-rust/index.md similarity index 97% rename from src/content/blog/ah-fail-skaermlaas-rust.md rename to content/blog/ah-fail-skaermlaas-rust/index.md index 9772b9a..7e4fedc 100644 --- a/src/content/blog/ah-fail-skaermlaas-rust.md +++ b/content/blog/ah-fail-skaermlaas-rust/index.md @@ -2,9 +2,11 @@ title: "Ah ah ah, you didn't say the magic word.. en skærmlås-prank i Rust" description: "Hvorfor jeg droppede ANSI C til fordel for Rust, og hvordan du installerer ah-fail på Wayland, X11 og macOS." date: 2026-06-11 -tags: [rust, pam, gtklock, wayland, linux, macos] +taxonomies: + tags: [rust, pam, gtklock, wayland, linux, macos] draft: false -godkendt: "v1:c8d80c56e01731b150a66ec9dcdfca911d932ea1721145fb79e3cbf46bc26046" +extra: + 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/content/blog/authentik-oidc-faldgruber/index.md similarity index 96% rename from src/content/blog/authentik-oidc-faldgruber.md rename to content/blog/authentik-oidc-faldgruber/index.md index 464f0bb..881483b 100644 --- a/src/content/blog/authentik-oidc-faldgruber.md +++ b/content/blog/authentik-oidc-faldgruber/index.md @@ -2,9 +2,11 @@ title: "Authentik og OIDC: de faldgruber der koster en weekend" description: "redirect_uri-mismatch, X-Forwarded-Proto, sub vs. email og de andre OIDC-fælder jeg er faldet i med Authentik." date: 2026-06-01 -tags: [authentik, oidc, sso, selfhost] +taxonomies: + tags: [authentik, oidc, sso, selfhost] draft: false -godkendt: "v1:6c1d0bac31fd0547c7b0fc4e1c3dbb4ec8bf13d5960f33d8df90766c7700a1cd" +extra: + 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/content/blog/authentik-reverse-proxy-forward-auth/index.md similarity index 95% rename from src/content/blog/authentik-reverse-proxy-forward-auth.md rename to content/blog/authentik-reverse-proxy-forward-auth/index.md index 9ab57bb..68f65fa 100644 --- a/src/content/blog/authentik-reverse-proxy-forward-auth.md +++ b/content/blog/authentik-reverse-proxy-forward-auth/index.md @@ -2,9 +2,11 @@ title: "Authentik bag en reverse proxy: forward auth, outposts og 502-fælden" description: "Forward auth eller proxy provider? Og hvorfor store auth-headers giver 502 i nginx, når Authentik sidder bagved." date: 2026-06-05 -tags: [authentik, reverse-proxy, oidc, nginx] +taxonomies: + tags: [authentik, reverse-proxy, oidc, nginx] draft: false -godkendt: "v1:c0acd362adab5c144d08b0e2560513d3efa83c851632f4612dbcad2c9920a3bb" +extra: + 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/content/blog/er-mit-homelab-opdateret/index.md similarity index 97% rename from src/content/blog/er-mit-homelab-opdateret.md rename to content/blog/er-mit-homelab-opdateret/index.md index 2f16eb5..6436bda 100644 --- a/src/content/blog/er-mit-homelab-opdateret.md +++ b/content/blog/er-mit-homelab-opdateret/index.md @@ -2,9 +2,11 @@ title: "Er mit homelab egentlig opdateret?" description: "Jeg byggede et lille overblik der selv kigger ind i alle mine containere og fortæller mig hvad der er bagud, og hvad der snart holder op med at få opdateringer." date: 2026-06-13 -tags: [proxmox, homelab, lxc, selfhost] +taxonomies: + tags: [proxmox, homelab, lxc, selfhost] draft: false -godkendt: "v1:8f1db814557e81e8015ac5f9173cbe414141c8373fcfc8149b485d006547ed47" +extra: + 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/content/blog/inject-javascript-npmplus-sub-filter/index.md similarity index 97% rename from src/content/blog/inject-javascript-npmplus-sub-filter.md rename to content/blog/inject-javascript-npmplus-sub-filter/index.md index c3c31fc..96b9cec 100644 --- a/src/content/blog/inject-javascript-npmplus-sub-filter.md +++ b/content/blog/inject-javascript-npmplus-sub-filter/index.md @@ -2,9 +2,11 @@ title: "Injicér JavaScript på alle dine sider med NPMplus og sub_filter" description: "Sådan sprøjter du et script ind i HTML for hver app bag din reverse proxy med nginx sub_filter og lidt Lua .. uden at røre den enkelte app." date: 2026-06-08 -tags: [npmplus, nginx, sub-filter, reverse-proxy, selfhost, matomo] +taxonomies: + tags: [npmplus, nginx, sub-filter, reverse-proxy, selfhost, matomo] draft: false -godkendt: "v1:ab462ce1615149e1a5b65e2b5f5507e97ce231340822408ffc8d5f15956a395a" +extra: + 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/content/blog/npm-til-npmplus/index.md similarity index 95% rename from src/content/blog/npm-til-npmplus.md rename to content/blog/npm-til-npmplus/index.md index 91852cc..d2e4e9e 100644 --- a/src/content/blog/npm-til-npmplus.md +++ b/content/blog/npm-til-npmplus/index.md @@ -2,9 +2,11 @@ title: "Fra Nginx Proxy Manager til NPMplus: en migrering uden vej tilbage" description: "Hvad der gik godt, hvad der brød, og hvorfor backup ikke er til forhandling, når du skifter fra NPM til NPMplus." date: 2026-06-07 -tags: [npmplus, nginx-proxy-manager, reverse-proxy, selfhost] +taxonomies: + tags: [npmplus, nginx-proxy-manager, reverse-proxy, selfhost] draft: false -godkendt: "v1:5d850f0e2717f7928c31972a08f61f35eb7189adfe0e3c776cbd42318e39bfba" +extra: + 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/content/blog/proxmox-hjemmelab/index.md similarity index 84% rename from src/content/blog/proxmox-hjemmelab.md rename to content/blog/proxmox-hjemmelab/index.md index 2fbe4fe..3215f52 100644 --- a/src/content/blog/proxmox-hjemmelab.md +++ b/content/blog/proxmox-hjemmelab/index.md @@ -2,9 +2,11 @@ title: "Mit Proxmox-hjemmelab" description: "Et kig på opbygningen: reverse proxy, SSO, CI/CD og en masse containere." date: 2026-06-09 -tags: [proxmox, selfhost, homelab] +taxonomies: + tags: [proxmox, selfhost, homelab] draft: false -godkendt: "v1:b6deba4fbb95a8418cce1ef7b688f612ce5b4065d60bb5205cd6bf36da8e70e0" +extra: + 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/content/blog/proxmox-lxc-fejl/index.md similarity index 95% rename from src/content/blog/proxmox-lxc-fejl.md rename to content/blog/proxmox-lxc-fejl/index.md index dc825cf..57df47c 100644 --- a/src/content/blog/proxmox-lxc-fejl.md +++ b/content/blog/proxmox-lxc-fejl/index.md @@ -2,9 +2,11 @@ title: "Proxmox LXC i praksis: de fejl jeg selv lavede" description: "Unprivileged kontra privileged, uid-mapping til nobody, og hvorfor Docker i en unprivileged LXC driller på nye kerner." date: 2026-05-26 -tags: [proxmox, lxc, homelab, selfhost] +taxonomies: + tags: [proxmox, lxc, homelab, selfhost] draft: false -godkendt: "v1:e9ae34841a773a0941e783eb3d16c4bc772cd923d13f0caf22575682400347a1" +extra: + 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/content/blog/velkommen/index.md similarity index 90% rename from src/content/blog/velkommen.md rename to content/blog/velkommen/index.md index 6087f8c..189628a 100644 --- a/src/content/blog/velkommen.md +++ b/content/blog/velkommen/index.md @@ -2,9 +2,11 @@ title: "Velkommen til min nye side" description: "Hvad siden er, og hvad du kan forvente at finde her." date: 2026-06-10 -tags: [meta, selfhost] +taxonomies: + tags: [meta, selfhost] draft: false -godkendt: "v1:1d29e33762bae4023bbc57d2870ab97c401aef100074da53c1434d99fff5702f" +extra: + godkendt: "v1:1d29e33762bae4023bbc57d2870ab97c401aef100074da53c1434d99fff5702f" --- Velkommen. Det her er min nye personlige side.. bygget med Astro på Bun, hostet på diff --git a/content/chat.md b/content/chat.md new file mode 100644 index 0000000..3aea9b8 --- /dev/null +++ b/content/chat.md @@ -0,0 +1,8 @@ +--- +title: "Spørg mit CV" +description: "En lille AI-chat der svarer på spørgsmål om mig, min erfaring og mit homelab." +template: sider/chat.html +extra: + noindex: true + alternates: [{lang: da, path: /chat}, {lang: en, path: /en/chat}] +--- diff --git a/content/colophon.md b/content/colophon.md new file mode 100644 index 0000000..85c4a4b --- /dev/null +++ b/content/colophon.md @@ -0,0 +1,5 @@ +--- +title: "Kolofon" +description: "Hvordan denne side er bygget: Astro, Bun, Tailwind, self-hosted alt, og ingen JS-frameworks." +template: sider/colophon.html +--- diff --git a/content/cookies.md b/content/cookies.md new file mode 100644 index 0000000..50871fa --- /dev/null +++ b/content/cookies.md @@ -0,0 +1,7 @@ +--- +title: "Cookies" +description: "Hvilke cookies asger.weirsoe.dk bruger, og hvad de bruges til." +template: sider/cookies.html +extra: + noindex: true +--- diff --git a/content/cv.md b/content/cv.md new file mode 100644 index 0000000..9900b6a --- /dev/null +++ b/content/cv.md @@ -0,0 +1,7 @@ +--- +title: "CV" +description: "Asger Weirsøe, datalog og Technical Solution Architect. Erfaring, uddannelse og publikationer." +template: sider/cv.html +extra: + alternates: [{lang: da, path: /cv}, {lang: en, path: /en/cv}] +--- diff --git a/content/en/_index.md b/content/en/_index.md new file mode 100644 index 0000000..602b3b4 --- /dev/null +++ b/content/en/_index.md @@ -0,0 +1,8 @@ +--- +title: "Asger Weirsøe" +description: "Asger Weirsøe, computer scientist and Technical Solution Architect. Incident response, ISO/IEC 27001 compliance and Danish national e-invoicing infrastructure." +template: sider/forside-en.html +extra: + lang: en + alternates: [{lang: da, path: /}, {lang: en, path: /en}] +--- diff --git a/content/en/chat.md b/content/en/chat.md new file mode 100644 index 0000000..f8113a2 --- /dev/null +++ b/content/en/chat.md @@ -0,0 +1,9 @@ +--- +title: "Ask my CV" +description: "A small chat with Claude that answers questions about me, my experience and my homelab." +template: sider/chat-en.html +extra: + lang: en + noindex: true + alternates: [{lang: da, path: /chat}, {lang: en, path: /en/chat}] +--- diff --git a/content/en/cv.md b/content/en/cv.md new file mode 100644 index 0000000..dbb6704 --- /dev/null +++ b/content/en/cv.md @@ -0,0 +1,8 @@ +--- +title: "CV" +description: "Asger Weirsøe, computer scientist and Technical Solution Architect. Incident response, ISO/IEC 27001 compliance and national e-invoicing infrastructure." +template: sider/cv-en.html +extra: + lang: en + alternates: [{lang: da, path: /cv}, {lang: en, path: /en/cv}] +--- diff --git a/content/en/projects/_index.md b/content/en/projects/_index.md new file mode 100644 index 0000000..1a45a06 --- /dev/null +++ b/content/en/projects/_index.md @@ -0,0 +1,10 @@ +--- +title: "Projects" +description: "Tools I write and publish on the AUR, Homebrew and Scoop." +template: sider/projects-en.html +sort_by: title +page_template: projekt.html +extra: + lang: en + alternates: [{lang: da, path: /projekter}, {lang: en, path: /en/projects}] +--- diff --git a/src/content/projekter/en/ahfail.md b/content/en/projects/ahfail.md similarity index 90% rename from src/content/projekter/en/ahfail.md rename to content/en/projects/ahfail.md index 9daace1..0fc7d09 100644 --- a/src/content/projekter/en/ahfail.md +++ b/content/en/projects/ahfail.md @@ -1,15 +1,17 @@ --- title: "ahfail" description: "A screen locker for Linux and macOS. Type the wrong password and Dennis Nedry shows up to tell you \"ah ah ah, you didn't say the magic word\"." -repo: gtk-ahfail -sprog: Rust -licens: AGPL-3.0-only -platforme: [Linux (Wayland), Linux (X11), macOS] -aur: [ahfail, ahfail-bin] -tags: [linux, wayland, x11, macos, pam, rust] date: 2026-09-30 updated: 2026-09-30 -lang: en +extra: + lang: en + repo: gtk-ahfail + sprog: Rust + licens: AGPL-3.0-only + platforme: [Linux (Wayland), Linux (X11), macOS] + aur: [ahfail, ahfail-bin] + tags: [linux, wayland, x11, macos, pam, rust] + alternates: [{lang: da, path: /projekter/ahfail}, {lang: en, path: /en/projects/ahfail}] --- Get the password wrong and a looping animation of my own face photoshopped onto Dennis Nedry from Jurassic Park appears, while the "ah ah ah, you didn't say the magic word" clip plays. The volume goes all the way up, so whoever tried to sneak into your machine gets the full experience (`--no-max-volume` if you'd rather be discreet). Under the joke it's a real screen locker. diff --git a/src/content/projekter/en/pi3-smart-workspace.md b/content/en/projects/pi3-smart-workspace.md similarity index 87% rename from src/content/projekter/en/pi3-smart-workspace.md rename to content/en/projects/pi3-smart-workspace.md index 983db3b..41e5336 100644 --- a/src/content/projekter/en/pi3-smart-workspace.md +++ b/content/en/projects/pi3-smart-workspace.md @@ -1,15 +1,17 @@ --- title: "pi3-smart-workspace" description: "A small i3 helper that makes $mod+1 the first workspace on the screen your mouse is on, instead of workspace 1 somewhere else entirely." -repo: pi3-smart-workspace -sprog: Python -licens: Apache-2.0 -platforme: [Linux (i3)] -aur: [pi3-smart-workspace] -tags: [i3, linux, python] date: 2020-09-04 updated: 2026-09-30 -lang: en +extra: + lang: en + repo: pi3-smart-workspace + sprog: Python + licens: Apache-2.0 + platforme: [Linux (i3)] + aur: [pi3-smart-workspace] + tags: [i3, linux, python] + alternates: [{lang: da, path: /projekter/pi3-smart-workspace}, {lang: en, path: /en/projects/pi3-smart-workspace}] --- With several monitors in i3, workspaces are numbered globally. Press `$mod+1` and you jump to workspace 1, even if it lives on the left screen and you're working on the right one. `pi3-smart-workspace` turns that around: the key picks the N-th workspace on the screen your mouse is on. `$mod+1` is always the first workspace wherever the mouse is, and a multi-monitor setup feels a lot more coherent. diff --git a/src/content/projekter/en/ubl-tools.md b/content/en/projects/ubl-tools.md similarity index 90% rename from src/content/projekter/en/ubl-tools.md rename to content/en/projects/ubl-tools.md index 99de2d6..067e66f 100644 --- a/src/content/projekter/en/ubl-tools.md +++ b/content/en/projects/ubl-tools.md @@ -1,15 +1,17 @@ --- title: "ubl-tools" description: "Read UBL e-invoices (Peppol BIS 3.0 and OIOUBL) in the terminal or as a printable page, and pull out the attached PDF. Everything happens on your own machine." -repo: ubl-tools -sprog: Rust -licens: MIT OR Apache-2.0 -platforme: [Linux, macOS, Windows] -aur: [ubl-tools, ubl-tools-bin] -tags: [peppol, oioubl, nemhandel, e-invoicing, rust] date: 2026-09-30 updated: 2026-10-01 -lang: en +extra: + lang: en + repo: ubl-tools + sprog: Rust + licens: MIT OR Apache-2.0 + platforme: [Linux, macOS, Windows] + aur: [ubl-tools, ubl-tools-bin] + tags: [peppol, oioubl, nemhandel, e-invoicing, rust] + alternates: [{lang: da, path: /projekter/ubl-tools}, {lang: en, path: /en/projects/ubl-tools}] --- An e-invoice is an XML file. Reading one means scrolling through a few hundred lines of `cbc:` and `cac:` elements, or uploading it to an online viewer. The viewers work, but they want the whole invoice, and that includes names, VAT numbers, bank details and what the customer bought. I'm not keen on sending all that to a stranger's server just to read it. diff --git a/content/nemhandel.md b/content/nemhandel.md new file mode 100644 index 0000000..d4193c5 --- /dev/null +++ b/content/nemhandel.md @@ -0,0 +1,5 @@ +--- +title: "NemHandel-opslag" +description: "Slå et CVR-nummer op og se om virksomheden er registreret i NemHandelsregisteret, og hvilke profilpakker den understøtter." +template: sider/nemhandel.html +--- diff --git a/content/now.md b/content/now.md new file mode 100644 index 0000000..31818df --- /dev/null +++ b/content/now.md @@ -0,0 +1,5 @@ +--- +title: "Now" +description: "Hvad jeg arbejder på lige nu, plus et aktivitets-heatmap fra min self-hostede Gitea." +template: sider/now.html +--- diff --git a/content/peppol.md b/content/peppol.md new file mode 100644 index 0000000..d233c09 --- /dev/null +++ b/content/peppol.md @@ -0,0 +1,4 @@ +--- +title: "NemHandel-opslag" +template: sider/peppol.html +--- diff --git a/content/profiler.md b/content/profiler.md new file mode 100644 index 0000000..80816e0 --- /dev/null +++ b/content/profiler.md @@ -0,0 +1,5 @@ +--- +title: "Profil-opslag (Nemhandel & Peppol)" +description: "Søgbart opslag over Nemhandel OIOUBL- og Peppol BIS/PINT-profiler: profil-id'er, customization-id'er, indeholdte dokumenter og typisk anvendelse." +template: sider/profiler.html +--- diff --git a/content/projekter/_index.md b/content/projekter/_index.md new file mode 100644 index 0000000..8d36eef --- /dev/null +++ b/content/projekter/_index.md @@ -0,0 +1,8 @@ +--- +title: "Projekter" +description: "Udvalgte offentlige projekter fra min Gitea." +template: sider/projekter.html +page_template: projekt.html +extra: + alternates: [{lang: da, path: /projekter}, {lang: en, path: /en/projects}] +--- diff --git a/src/content/projekter/ahfail.md b/content/projekter/ahfail.md similarity index 90% rename from src/content/projekter/ahfail.md rename to content/projekter/ahfail.md index fb6a28c..1c2f72f 100644 --- a/src/content/projekter/ahfail.md +++ b/content/projekter/ahfail.md @@ -1,14 +1,17 @@ --- title: "ahfail" description: "En skærmlås til Linux og macOS. Skriver du forkert kodeord, dukker Dennis Nedry op og siger \"ah ah ah, you didn't say the magic word\"." -repo: gtk-ahfail -sprog: Rust -licens: AGPL-3.0-only -platforme: [Linux (Wayland), Linux (X11), macOS] -aur: [ahfail, ahfail-bin] -tags: [linux, wayland, x11, macos, pam, rust] date: 2026-09-30 updated: 2026-09-30 +extra: + lang: da + repo: gtk-ahfail + sprog: Rust + licens: AGPL-3.0-only + platforme: [Linux (Wayland), Linux (X11), macOS] + aur: [ahfail, ahfail-bin] + tags: [linux, wayland, x11, macos, pam, rust] + alternates: [{lang: da, path: /projekter/ahfail}, {lang: en, path: /en/projects/ahfail}] --- Skriver du forkert kodeord, kører en animation i loop af mit eget ansigt photoshoppet ind på Dennis Nedry fra Jurassic Park, mens klippet med "ah ah ah, you didn't say the magic word" spiller. Lyden skrues helt op, så den der prøvede at snige sig ind på din maskine også får det hele med (`--no-max-volume` hvis du hellere vil være diskret). Under joken er det en rigtig skærmlås. diff --git a/src/content/projekter/pi3-smart-workspace.md b/content/projekter/pi3-smart-workspace.md similarity index 87% rename from src/content/projekter/pi3-smart-workspace.md rename to content/projekter/pi3-smart-workspace.md index 1bc10dc..ea4d406 100644 --- a/src/content/projekter/pi3-smart-workspace.md +++ b/content/projekter/pi3-smart-workspace.md @@ -1,14 +1,17 @@ --- title: "pi3-smart-workspace" description: "Et lille værktøj til i3 der gør $mod+1 til den første workspace på den skærm musen står på, i stedet for workspace nummer 1 et helt andet sted." -repo: pi3-smart-workspace -sprog: Python -licens: Apache-2.0 -platforme: [Linux (i3)] -aur: [pi3-smart-workspace] -tags: [i3, linux, python] date: 2020-09-04 updated: 2026-09-30 +extra: + lang: da + repo: pi3-smart-workspace + sprog: Python + licens: Apache-2.0 + platforme: [Linux (i3)] + aur: [pi3-smart-workspace] + tags: [i3, linux, python] + alternates: [{lang: da, path: /projekter/pi3-smart-workspace}, {lang: en, path: /en/projects/pi3-smart-workspace}] --- Med flere skærme i i3 er workspaces nummereret globalt. Trykker du `$mod+1`, hopper du til workspace 1, også selvom den ligger på skærmen til venstre og du sidder og arbejder på den til højre. `pi3-smart-workspace` vender det om: tasten vælger den N'te workspace på den skærm musen står på. `$mod+1` er altid den første workspace der hvor musen er, og et setup med flere skærme føles meget mere sammenhængende. diff --git a/src/content/projekter/ubl-tools.md b/content/projekter/ubl-tools.md similarity index 90% rename from src/content/projekter/ubl-tools.md rename to content/projekter/ubl-tools.md index 09b224d..acc7cb7 100644 --- a/src/content/projekter/ubl-tools.md +++ b/content/projekter/ubl-tools.md @@ -1,14 +1,17 @@ --- title: "ubl-tools" description: "Læs e-fakturaer i UBL (Peppol BIS 3.0 og OIOUBL) i terminalen eller som en side du kan printe, og træk den vedhæftede PDF ud. Alt foregår på din egen maskine." -repo: ubl-tools -sprog: Rust -licens: MIT OR Apache-2.0 -platforme: [Linux, macOS, Windows] -aur: [ubl-tools, ubl-tools-bin] -tags: [peppol, oioubl, nemhandel, e-faktura, rust] date: 2026-09-30 updated: 2026-10-01 +extra: + lang: da + repo: ubl-tools + sprog: Rust + licens: MIT OR Apache-2.0 + platforme: [Linux, macOS, Windows] + aur: [ubl-tools, ubl-tools-bin] + tags: [peppol, oioubl, nemhandel, e-faktura, rust] + alternates: [{lang: da, path: /projekter/ubl-tools}, {lang: en, path: /en/projects/ubl-tools}] --- En e-faktura er en XML-fil. Vil man læse en, skal man enten scrolle gennem et par hundrede linjer `cbc:` og `cac:`, eller uploade den til en online viewer. Viewerne virker fint, men de vil have hele fakturaen, og den indeholder navne, CVR-numre, bankoplysninger og hvad kunden har købt. Det har jeg det lidt stramt med at sende til en fremmed server bare for at kunne læse den. diff --git a/content/status.md b/content/status.md new file mode 100644 index 0000000..4604e08 --- /dev/null +++ b/content/status.md @@ -0,0 +1,7 @@ +--- +title: "Status" +description: "Live driftsstatus for mit homelab, målt med Uptime Kuma." +template: sider/status.html +extra: + noindex: true +--- diff --git a/content/ubl.md b/content/ubl.md new file mode 100644 index 0000000..c7d228b --- /dev/null +++ b/content/ubl.md @@ -0,0 +1,5 @@ +--- +title: "UBL-tjek & identifikator-afkoder" +description: "Træk en UBL/OIOUBL-faktura ind eller indsæt en CustomizationID/ProfileID, så afkoder den profilen, syntaksen og EN 16931-status. Alt sker lokalt i browseren; filen sendes aldrig nogen steder." +template: sider/ubl.html +--- diff --git a/content/uses.md b/content/uses.md new file mode 100644 index 0000000..f95ac67 --- /dev/null +++ b/content/uses.md @@ -0,0 +1,5 @@ +--- +title: "Uses" +description: "Hardware, software og selvhostede tjenester bag mit homelab og denne side." +template: sider/uses.html +--- diff --git a/data/colophon.json b/data/colophon.json new file mode 100644 index 0000000..e095bd2 --- /dev/null +++ b/data/colophon.json @@ -0,0 +1,48 @@ +{ + "stack": [ + { + "k": "Generator", + "v": "Astro 5: statisk output, view transitions, content collections til bloggen" + }, + { + "k": "Runtime", + "v": "Bun (build og dev)" + }, + { + "k": "CSS", + "v": "Tailwind 4 med egne design-tokens; dark/light sat pre-paint uden flash" + }, + { + "k": "Typografi", + "v": "Inter Variable, self-hosted woff2 (ingen Google Fonts-kald)" + }, + { + "k": "JS i browseren", + "v": "Ingen frameworks. Lidt vanilla til tema-toggle, paletten og service workeren" + }, + { + "k": "PWA", + "v": "Workbox service worker, siden virker offline og kan installeres" + }, + { + "k": "Kommentarer", + "v": "Comentario, self-hosted, login via min egen Authentik" + }, + { + "k": "Analytics", + "v": "Matomo, self-hosted og first-party, ingen tredjeparts-trackere" + }, + { + "k": "Hosting", + "v": "Alpine LXC med nginx på min egen Proxmox-node, bag NPMplus som edge" + }, + { + "k": "CI/CD", + "v": "Gitea Actions på egne runnere: push til master → build → atomisk swap af webroot" + }, + { + "k": "Struktureret data", + "v": "JSON-LD: Person, ProfilePage, FAQ, BlogPosting, BreadcrumbList, plus llms.txt til LLM’er" + } + ] +} diff --git a/data/cv-en.json b/data/cv-en.json new file mode 100644 index 0000000..ce32d14 --- /dev/null +++ b/data/cv-en.json @@ -0,0 +1,159 @@ +{ + "work": [ + { + "org": "Digisense A/S", + "place": "Denmark", + "roles": [ + { + "title": "Technical Solution Architect", + "when": "May 2026 – present", + "details": [ + "Built the company’s information security management system around ISO/IEC 27001, including incident management and escalation procedures, post-incident reviews and an incident register", + "Technical point of contact across engineering, sales and support for a Peppol and NemHandel access point provider", + "Solution design for customer projects" + ] + } + ] + }, + { + "org": "CGI Danmark", + "place": "Aalborg", + "roles": [ + { + "title": "Tech Lead, NemHandel", + "when": "Aug 2023 – May 2026", + "details": [ + "Technical lead on CGI’s NemHandel team and the client’s technical adviser", + "Led the response to CVE-2022-1471 (SnakeYAML, remote code execution) in the NemHandel reference implementation: the client was notified immediately, mitigation guidance went out to operators, and the team shipped a fixed release within two weeks", + "Coordinated recovery from a production database failure during the July 2024 CrowdStrike outage, when most team workstations were unavailable", + "Translated client requirements for the development team" + ] + }, + { + "title": "Senior Consultant", + "when": "Apr 2023 – May 2026", + "details": [ + "Mentored a newly hired developer", + "Delivered installation webinars for the NemHandel reference implementation" + ] + }, + { + "title": "Consultant", + "when": "Aug 2022 – Apr 2023", + "details": [ + "Developed the open source NemHandel eDelivery reference implementation (Java, built on Oxalis)", + "Solution architect on the project" + ] + }, + { + "title": "Student Developer", + "when": "Aug 2020 – Aug 2022", + "details": [ + "Owned a rules engine (Java, Drools)", + "Deployment and operations (Docker, Docker Compose, Linux)" + ] + }, + { + "title": "Consultant", + "when": "Jun 2019 – Aug 2020", + "details": [ + "Frontend (Angular, TypeScript)", + "Backend (Go, Java, Python)" + ] + } + ] + }, + { + "org": "BilligVVS", + "place": "Egå", + "roles": [ + { + "title": "Full-stack Developer", + "when": "Sep 2018 – Jun 2019", + "details": [ + "Webshop checkout and shipping integration (Python, TypeScript)", + "Movex ERP integration", + "Internal BI dashboards (Python, Chart.js)" + ] + } + ] + } + ], + "education": [ + { + "what": "HD Part 1, Business Administration", + "where": "Aalborg University", + "when": "2025 – 2027 (in progress)", + "note": "Business economics, accounting and finance." + }, + { + "what": "M.Sc. in Computer Science (cand.scient.)", + "where": "Aalborg University", + "when": "2020 – 2022", + "note": "Specialisation in security in distributed systems. Thesis: Thread-Fuzz, mutation-based behavioural blackbox fuzzing of Thread." + }, + { + "what": "B.Sc. in Computer Science", + "where": "Aalborg University", + "when": "2015 – 2018", + "note": "" + } + ], + "skills": [ + { + "k": "Incident response", + "v": [ + "Incident coordination", + "Escalation", + "Stakeholder communication", + "Vulnerability response", + "Post-incident review" + ] + }, + { + "k": "Compliance and security", + "v": [ + "ISO/IEC 27001", + "ISMS", + "Information security" + ] + }, + { + "k": "E-invoicing", + "v": [ + "Peppol", + "eDelivery (AS4)", + "Oxalis", + "NemHandel", + "OIOUBL / UBL", + "EN 16931" + ] + }, + { + "k": "Infrastructure", + "v": [ + "Linux", + "Docker", + "Kubernetes", + "AWS", + "Proxmox", + "Datadog" + ] + }, + { + "k": "Development", + "v": [ + "Java", + "Python", + "Go", + "TypeScript", + "Angular", + "C#", + "PostgreSQL", + "RabbitMQ", + "Git / GitLab", + "Jenkins" + ] + } + ] +} diff --git a/data/cv.json b/data/cv.json new file mode 100644 index 0000000..35d68ff --- /dev/null +++ b/data/cv.json @@ -0,0 +1,176 @@ +{ + "erhverv": [ + { + "org": "Digisense A/S", + "sted": "Danmark", + "roller": [ + { + "titel": "Technical Solution Architect", + "naar": "maj 2026 – nu", + "detaljer": [ + "Har bygget virksomhedens ledelsessystem for informationssikkerhed (ISMS) efter ISO/IEC 27001, med procedurer for hændelseshåndtering og eskalering, evaluering efter hændelser og et hændelsesregister", + "Teknisk kontaktperson på tværs af udvikling, salg og support hos en Peppol- og NemHandel-access point-udbyder", + "Løsningsdesign til kundeprojekter" + ] + } + ] + }, + { + "org": "CGI", + "sted": "Aalborg", + "roller": [ + { + "titel": "Teknisk Lead", + "naar": "aug. 2023 – maj 2026", + "detaljer": [ + "Teknisk lead på CGI’s NemHandel-team og kundens tekniske rådgiver", + "Ledte responsen på CVE-2022-1471 (SnakeYAML, remote code execution) i NemHandel-referenceimplementeringen: kunden blev orienteret med det samme, der gik vejledning ud til dem der kørte løsningen, og teamet leverede en rettet version inden for to uger", + "Koordinerede genopretningen efter en databasefejl i produktion under CrowdStrike-nedbruddet i juli 2024, hvor de fleste af teamets maskiner var ude af drift", + "Formidler kundens behov til udviklingsteamet" + ] + }, + { + "titel": "Senior Konsulent", + "naar": "apr. 2023 – maj 2026", + "detaljer": [ + "Buddy for ny udvikler", + "Afholdt installationsvejledning via webinar" + ] + }, + { + "titel": "Konsulent", + "naar": "aug. 2022 – apr. 2023", + "detaljer": [ + "Med til at udvikle den open source eDelivery-løsning til NemHandel (Java, bygget på Oxalis)", + "Løsningsarkitekt" + ] + }, + { + "titel": "Studentermedhjælper", + "naar": "aug. 2020 – aug. 2022", + "detaljer": [ + "Kode-ansvarlig for en regelmotor (Java, Drools)", + "DevOps (Docker, docker-compose, Linux)" + ] + }, + { + "titel": "Konsulent", + "naar": "jun. 2019 – aug. 2020", + "detaljer": [ + "Frontend (Angular 8, TypeScript)", + "Backend (GoLang, Java, Python)" + ] + } + ] + }, + { + "org": "BilligVVS", + "sted": "Egaa", + "roller": [ + { + "titel": "Fullstack IT-udvikler", + "naar": "sep. 2018 – jun. 2019", + "detaljer": [ + "Hjemmeside (Python, TypeScript)", + "Backend (Python, Movex)", + "Webcrawler (Python)", + "BI (Python, chart.js)" + ] + } + ] + } + ], + "uddannelse": [ + { + "hvad": "HD 1. del i Erhvervsøkonomi", + "hvor": "Aalborg Universitet", + "naar": "2025 – 2027 (i gang)", + "note": "Forretningsforståelse, erhvervsøkonomi, regnskab og finansiering." + }, + { + "hvad": "Kandidat i Datalogi (cand.scient.)", + "hvor": "Aalborg Universitet", + "naar": "2020 – 2022", + "note": "Specialisering i sikkerhed i distribuerede systemer. Speciale: Thread-Fuzz, mutation-based behavioural blackbox fuzzing of Thread." + }, + { + "hvad": "Bachelor i Datalogi (B.Sc.)", + "hvor": "Aalborg Universitet", + "naar": "2015 – 2018", + "note": "" + }, + { + "hvad": "Almen studentereksamen (STX)", + "hvor": "Espergærde Gymnasium og HF", + "naar": "2012 – 2015", + "note": "" + }, + { + "hvad": "Udvekslingsår", + "hvor": "Lincoln High School, Ypsilanti, Michigan (USA)", + "naar": "2011 – 2012", + "note": "" + } + ], + "licenser": [ + { + "hvad": "Radioamatørlicens, kategori B", + "hvor": "Kaldesignal OZ0AGW", + "naar": "maj 2024", + "note": "Bestod radioamatørprøven lørdag den 18. maj 2024." + }, + { + "hvad": "Jagttegn", + "hvor": "Bestod jagtprøven", + "naar": "2020", + "note": "" + } + ], + "frivilligt": [ + { + "hvad": "LUNARK", + "naar": "2020", + "note": "Notifikationssystem i C# og React." + }, + { + "hvad": "Kirkens Korshær", + "naar": "2020 – nu", + "note": "Køkkenhjælper hver jul og nytår." + }, + { + "hvad": "AAU Campus", + "naar": "2016 – 2022", + "note": "Tutor og F-Klub-frivillig." + } + ], + "primaere": [ + "Java", + "Python", + "GoLang", + "TypeScript", + "JavaScript", + "Angular", + "Docker", + "Linux", + "Proxmox", + "PostgreSQL", + "Jenkins", + "Git / GitLab", + "BI", + "Agile" + ], + "sekundaere": [ + "C#", + "C++", + "C", + "Kubernetes", + "AWS", + "RabbitMQ", + "Datadog", + "XeLaTeX" + ], + "sprog": [ + "Dansk (modersmål)", + "Engelsk (flydende i tale og skrift)" + ] +} diff --git a/data/forside-en.json b/data/forside-en.json new file mode 100644 index 0000000..233ffef --- /dev/null +++ b/data/forside-en.json @@ -0,0 +1,56 @@ +{ + "highlights": [ + { + "title": "Incident response & compliance", + "body": "Led incident and vulnerability response on Danish national e-invoicing infrastructure, and built an ISMS around ISO/IEC 27001 with escalation and post-incident review." + }, + { + "title": "E-invoicing & architecture", + "body": "Technical Solution Architect at Digisense A/S, working on Peppol, NemHandel and Oxalis. Before that, tech lead on NemHandel at CGI." + }, + { + "title": "Self-hosting, AI & full-stack", + "body": "A Proxmox homelab with around 50 containers and VMs behind SSO (Authentik), and projects from Java, Go and Python to TypeScript and LLM-driven workflows." + } + ], + "faqs": [ + { + "q": "Who is Asger Weirsøe?", + "a": "I am a computer scientist (M.Sc., Aalborg University) and Technical Solution Architect at Digisense A/S. I have worked on Danish national e-invoicing infrastructure since 2021, and I combine technical depth with business understanding." + }, + { + "q": "What incident response experience does Asger Weirsøe have?", + "a": "As tech lead on NemHandel at CGI I coordinated recovery from a production database failure during the July 2024 CrowdStrike outage, and led the response to CVE-2022-1471 (SnakeYAML) in the NemHandel reference implementation, where we shipped a fixed release within two weeks. At Digisense I built the ISMS around ISO/IEC 27001, including incident management, escalation and post-incident review." + }, + { + "q": "What does Asger Weirsøe work with?", + "a": "Solution architecture for electronic invoicing and e-procurement: Peppol (Access Point, eDelivery/AS4 and Oxalis), NemHandel, OIOUBL/UBL and EN 16931. Plus information security, self-hosting, Proxmox and AI." + }, + { + "q": "Is Asger Weirsøe a radio amateur?", + "a": "Yes. I hold a category B licence with the call sign OZ0AGW." + } + ], + "kort": [ + { + "href": "/en/chat", + "title": "Ask my CV", + "desc": "A small chat with Claude that has read my CV and answers questions about me." + }, + { + "href": "/status", + "title": "Homelab status", + "desc": "Live status for the public parts of my homelab." + }, + { + "href": "/ubl", + "title": "UBL check (Danish)", + "desc": "Drop in an e-invoice and get its profile, syntax and EN 16931 status decoded. It all runs in the browser, the file never leaves your machine." + }, + { + "href": "/en/projects", + "title": "Projects", + "desc": "Tools I maintain and publish on the AUR, Homebrew and Scoop." + } + ] +} diff --git a/data/forside.json b/data/forside.json new file mode 100644 index 0000000..35c9b81 --- /dev/null +++ b/data/forside.json @@ -0,0 +1,70 @@ +{ + "highlights": [ + { + "title": "E-fakturering & arkitektur", + "body": "Technical Solution Architect hos Digisense A/S. Jeg står for løsningsarkitektur inden for elektronisk fakturering, Peppol, Oxalis og NemHandel." + }, + { + "title": "Forretning møder teknik", + "body": "Jeg oversætter mellem kunder, forretning og udviklingsteams, så e-faktura faktisk kommer til at virke i praksis. Læser HD i Erhvervsøkonomi ved siden af." + }, + { + "title": "Selvhosting, AI & fuld-stack", + "body": "Et Proxmox-homelab med ~50 LXC/VM’er bag SSO (Authentik), og projekter fra Java/Go/Python til TypeScript og LLM-drevne workflows." + } + ], + "faqs": [ + { + "q": "Hvem er Asger Weirsøe?", + "a": "Jeg er datalog (cand.scient. fra Aalborg Universitet) og Technical Solution Architect hos Digisense A/S. Jeg har en bred profil inden for elektronisk fakturering og forener teknisk indsigt med forretningsforståelse." + }, + { + "q": "Hvad arbejder Asger Weirsøe med?", + "a": "Jeg arbejder med løsningsarkitektur inden for elektronisk fakturering og e-procurement: Peppol (Access Point, eDelivery/AS4 og Oxalis), NemHandel, OIOUBL/UBL og EN 16931. Derudover selvhosting, Proxmox og AI." + }, + { + "q": "Hvem kan rådgive om Peppol og NemHandel i Danmark?", + "a": "Det kan jeg. Jeg har arbejdet praktisk med både Peppol og NemHandel. Hos CGI var jeg med til at udvikle den open source eDelivery-løsning til NemHandel, der er bygget på Oxalis. I dag arbejder jeg som Technical Solution Architect hos Digisense A/S med løsningsarkitektur inden for elektronisk fakturering." + }, + { + "q": "Er Asger Weirsøe radioamatør?", + "a": "Ja. Jeg har B-licens og kaldesignalet OZ0AGW, og jeg bestod radioamatørprøven lørdag den 18. maj 2024." + }, + { + "q": "Hvad er Asger Weirsøes baggrund?", + "a": "Cand.scient. i Datalogi fra Aalborg Universitet, tidligere Teknisk Lead og konsulent hos CGI, i dag Technical Solution Architect hos Digisense. Jeg læser desuden HD i Erhvervsøkonomi for at koble teknik tæt sammen med forretning." + } + ], + "kort": [ + { + "href": "/nemhandel", + "title": "NemHandel-opslag", + "desc": "Slå et CVR-nummer op og se om virksomheden er registreret i NemHandelsregisteret, og hvilke profilpakker den understøtter." + }, + { + "href": "/chat", + "title": "Spørg mit CV", + "desc": "En lille chat med Claude, der har læst mit CV og svarer på spørgsmål om mig." + }, + { + "href": "/status", + "title": "Homelab-status", + "desc": "Live driftsstatus for de offentlige dele af mit homelab." + }, + { + "href": "/now", + "title": "Now", + "desc": "Hvad jeg roder med lige nu, plus et aktivitets-heatmap fra min egen Gitea." + }, + { + "href": "/profiler", + "title": "Profil-opslag", + "desc": "Søgbart opslag over Nemhandel OIOUBL- og Peppol BIS/PINT-profiler: id'er, dokumenter og typisk brug." + }, + { + "href": "/ubl", + "title": "UBL-tjek", + "desc": "Træk en faktura ind og få profil, syntaks og EN 16931-status afkodet. Alt sker i browseren, filen forlader aldrig din maskine." + } + ] +} diff --git a/data/now.json b/data/now.json new file mode 100644 index 0000000..61fe7bb --- /dev/null +++ b/data/now.json @@ -0,0 +1,7 @@ +{ + "focus": [ + "Elektronisk fakturering hos Digisense: Peppol, NemHandel og løsningsarkitektur", + "HD 1. del i Erhvervsøkonomi på AAU (eksamener til sommer)", + "Homelab: crawlkit, denne side, og alt for mange LXC-containere" + ] +} diff --git a/data/profiles.json b/data/profiles.json new file mode 100644 index 0000000..a9a7bc3 --- /dev/null +++ b/data/profiles.json @@ -0,0 +1,961 @@ +{ + "standarder": [ + "Nemhandel OIOUBL (UBL 2.1)", + "Peppol BIS 3.0", + "Peppol BIS 4.0 / PINT" + ], + "kategorier": [ + "Fakturering", + "Ordre til fakturering", + "Simple Ordrer", + "Katalog", + "Udvidet fakturering", + "Udvidet katalog", + "Udvidet ordre til fakturering", + "ForsyningsSpecifikation", + "Diverse" + ], + "profiles": [ + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Fakturering", + "navn": "Procurement-BilSim-1.0 (simpel fakturering)", + "id": "Procurement-BilSim-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse (kun ved afvisning)" + ], + "anvendelse": "Ren fakturering: leverandør sender faktura/kreditnota/rykker til kunde. Ingen ordreproces, og kunden er ikke forpligtet til at kvittere.", + "bemaerkninger": "Obligatorisk profil og den mest udbredte rene faktura-pakke i Nemhandel. Kunden må afvise med ApplicationResponse, men skal ikke kvittere positivt.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Fakturering", + "navn": "urn:www.nesubl.eu:profiles:profile5:ver2.0 (NES5, Billing Basic)", + "id": "urn:www.nesubl.eu:profiles:profile5:ver2.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Invoice", + "CreditNote" + ], + "anvendelse": "Internationalt NES-fakturaprofil: kunde modtager faktura og kreditnota fra leverandør.", + "bemaerkninger": "Obligatorisk profil. Den profil der ligger tættest på den gamle OIOXML-faktura, kun afløftede felter er tilladt. Bruges når fakturaafsender teknisk set ikke kan modtage en ApplicationResponse.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Ordre til fakturering", + "navn": "Procurement-OrdSim-BilSim-1.0 (simpel ordre + simpel fakturering)", + "id": "Procurement-OrdSim-BilSim-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order", + "OrderResponseSimple", + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse *" + ], + "anvendelse": "Komplet simpel handel: simpel ordre efterfulgt af simpel fakturering. Dækker bestilling til betaling.", + "bemaerkninger": "Samlepakken de fleste vælger, når de vil kunne både bestille og fakturere. Ingen ordreændring/-annullering, det kræver en OrdAdv-variant.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Simple Ordrer", + "navn": "Procurement-OrdRes-1.0 (kun ordresvar)", + "id": "Procurement-OrdRes-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "OrderResponse" + ], + "anvendelse": "Enkeltdokument-profil til alene at kunne udveksle en ordrebekræftelse (OrderResponse) uden at tilmelde sig en fuld ordreproces.", + "bemaerkninger": "Registerprofil af typen \"single\", findes i Nemhandelsregistret, men er ikke beskrevet i OIOUBL Profiler-guiden (G26) v1.3. Verificér mod registret før brug.", + "kilde": { + "label": "Nemhandelsregistret", + "url": "https://nemhandel.dk/nemhandelsregisteret" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Simple Ordrer", + "navn": "Procurement-OrdSim-1.0 (simpel ordre)", + "id": "Procurement-OrdSim-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order", + "OrderResponseSimple" + ], + "anvendelse": "Ren ordreproces: kunde sender ordre, leverandør svarer med simpelt ordresvar. Ingen fakturering i profilen.", + "bemaerkninger": "Ren ordre-only proces; kombineres typisk med en faktureringsprofil.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Simple Ordrer", + "navn": "Procurement-OrdSimR-1.0 (simpel ordre m. krævet svar)", + "id": "Procurement-OrdSimR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order", + "OrderResponseSimple" + ], + "anvendelse": "Som OrdSim, men leverandør skal altid sende ordresvar.", + "bemaerkninger": "R = 'Response required': samme dokumenter som OrdSim, men svaret er obligatorisk.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Simple Ordrer", + "navn": "urn:www.nesubl.eu:profiles:profile3:ver2.0 (NES3, \"Order Only\")", + "id": "urn:www.nesubl.eu:profiles:profile3:ver2.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order" + ], + "anvendelse": "Leverandør modtager ordrer fra kunder. Intet ordresvar og ingen fakturering i profilen.", + "bemaerkninger": "Ældre NES-ordreprofil; OrdSim-varianterne anbefales i stedet.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Katalog", + "navn": "Catalogue-CatBas-1.0 (basalt katalog)", + "id": "Catalogue-CatBas-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Catalogue", + "CatalogueDeletion", + "ApplicationResponse" + ], + "anvendelse": "Leverandør sender et produktkatalog til kunden og kan slette det igen. Kunden kan afvise med ApplicationResponse.", + "bemaerkninger": "Simpleste katalogprofil, ingen prisopdatering, ingen katalogforespørgsel.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Katalog", + "navn": "Catalogue-CatPriUpd-1.0 (kun prisopdatering)", + "id": "Catalogue-CatPriUpd-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "CataloguePricingUpdate", + "ApplicationResponse" + ], + "anvendelse": "Leverandør opdaterer alene priserne i et allerede udvekslet katalog.", + "bemaerkninger": "\"Catalogue Pricing Update Only\" forudsætter at kataloget er sendt via en anden katalogprofil.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Katalog", + "navn": "Catalogue-CatPriUpdR-1.0 (kun prisopdatering m. krævet svar)", + "id": "Catalogue-CatPriUpdR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "CataloguePricingUpdate", + "ApplicationResponse" + ], + "anvendelse": "Som CatPriUpd, men kunden skal kvittere for prisopdateringen.", + "bemaerkninger": "R = svar påkrævet.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Katalog", + "navn": "Catalogue-CatSim-1.0 (katalogforespørgsel + katalog)", + "id": "Catalogue-CatSim-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "CatalogueRequest", + "Catalogue", + "CatalogueDeletion", + "ApplicationResponse" + ], + "anvendelse": "Kunden beder om et katalog (CatalogueRequest), leverandøren sender det og kan slette det igen.", + "bemaerkninger": "CatBas udvidet med katalogforespørgsel fra kunden.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet fakturering", + "navn": "Procurement-BilSimR-1.0 (simpel fakturering m. krævet svar)", + "id": "Procurement-BilSimR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse" + ], + "anvendelse": "Som BilSim, men kunden skal altid kvittere med et ApplicationResponse (accept/afvisning).", + "bemaerkninger": "R = 'Response required': kunden skal kvittere. Bemærk: tilmelding til BilSimR giver ikke automatisk BilSim; begge skal registreres hver for sig.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet fakturering", + "navn": "Procurement-BilSimReminderOnly-1.0 (kun rykker)", + "id": "Procurement-BilSimReminderOnly-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Reminder" + ], + "anvendelse": "Enkeltdokument-profil til alene at kunne modtage rykkere.", + "bemaerkninger": "Registerprofil af typen \"single\", ikke i OIOUBL Profiler-guiden (G26) v1.3. Praktisk fordi Reminder ikke findes i Peppol BIS Billing og derfor kun kan sendes i OIOUBL-profilerne.", + "kilde": { + "label": "Nemhandelsregistret", + "url": "https://nemhandel.dk/nemhandelsregisteret" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet fakturering", + "navn": "Procurement-PayBas-1.0 (kontoudtog)", + "id": "Procurement-PayBas-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Statement", + "ApplicationResponse" + ], + "anvendelse": "Leverandør sender kontoudtog (Statement) til kunden, en samlet oversigt over udestående poster, ikke en faktura.", + "bemaerkninger": "G26 kalder profilen direkte \"Statement\". Trods \"Pay\"-navnet er der ingen betalingsdokumenter i profilen.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet fakturering", + "navn": "Procurement-PayBasR-1.0 (kontoudtog m. krævet svar)", + "id": "Procurement-PayBasR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Statement", + "ApplicationResponse" + ], + "anvendelse": "Som PayBas, men kunden skal kvittere for kontoudtoget.", + "bemaerkninger": "R = svar påkrævet.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet fakturering", + "navn": "urn:www.nesubl.eu:profiles:profile8:ver2.0 (NES8)", + "id": "urn:www.nesubl.eu:profiles:profile8:ver2.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Invoice", + "CreditNote", + "ApplicationResponse" + ], + "anvendelse": "NES-faktureringsprofil med kvittering: faktura og kreditnota, hvor kunden kan svare med ApplicationResponse.", + "bemaerkninger": "NES5 udvidet med ApplicationResponse. Ingen Reminder.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet katalog", + "navn": "Catalogue-CatAdv-1.0 (avanceret katalog)", + "id": "Catalogue-CatAdv-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "CatalogueRequest", + "Catalogue", + "CatalogueItemSpecificationUpdate", + "CataloguePricingUpdate", + "CatalogueDeletion", + "ApplicationResponse" + ], + "anvendelse": "Fuld katalogproces: forespørgsel, katalog, opdatering af både varebeskrivelser og priser samt sletning.", + "bemaerkninger": "Den mest komplette katalogprofil: CatExt plus katalogforespørgsel.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet katalog", + "navn": "Catalogue-CatAdvR-1.0 (avanceret katalog m. krævet svar)", + "id": "Catalogue-CatAdvR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "CatalogueRequest", + "Catalogue", + "CatalogueItemSpecificationUpdate", + "CataloguePricingUpdate", + "CatalogueDeletion", + "ApplicationResponse" + ], + "anvendelse": "Som CatAdv, men modtageren skal kvittere for katalogdokumenterne.", + "bemaerkninger": "R = svar påkrævet.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet katalog", + "navn": "Catalogue-CatBasR-1.0 (basalt katalog m. krævet svar)", + "id": "Catalogue-CatBasR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Catalogue", + "CatalogueDeletion", + "ApplicationResponse" + ], + "anvendelse": "Som CatBas, men kunden skal kvittere for kataloget.", + "bemaerkninger": "R = svar påkrævet.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet katalog", + "navn": "Catalogue-CatExt-1.0 (udvidet katalog + opdatering)", + "id": "Catalogue-CatExt-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Catalogue", + "CatalogueItemSpecificationUpdate", + "CataloguePricingUpdate", + "CatalogueDeletion", + "ApplicationResponse" + ], + "anvendelse": "Katalogudveksling med efterfølgende opdatering af varebeskrivelser og priser samt sletning. Ingen katalogforespørgsel.", + "bemaerkninger": "CatBas udvidet med opdateringsdokumenter.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet katalog", + "navn": "Catalogue-CatExtR-1.0 (udvidet katalog + opdatering m. krævet svar)", + "id": "Catalogue-CatExtR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Catalogue", + "CatalogueItemSpecificationUpdate", + "CataloguePricingUpdate", + "CatalogueDeletion", + "ApplicationResponse" + ], + "anvendelse": "Som CatExt, men kunden skal kvittere.", + "bemaerkninger": "R = svar påkrævet.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet katalog", + "navn": "Catalogue-CatSimR-1.0 (katalogforespørgsel + katalog m. krævet svar)", + "id": "Catalogue-CatSimR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "CatalogueRequest", + "Catalogue", + "CatalogueDeletion", + "ApplicationResponse" + ], + "anvendelse": "Som CatSim, men katalogsvar er påkrævet.", + "bemaerkninger": "R = svar påkrævet.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet katalog", + "navn": "urn:www.nesubl.eu:profiles:profile1:ver2.0 (NES1, \"Catalogue Only\")", + "id": "urn:www.nesubl.eu:profiles:profile1:ver2.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Catalogue", + "ApplicationResponse" + ], + "anvendelse": "Ren katalogudsendelse: leverandør sender katalog, kunden kan afvise.", + "bemaerkninger": "Ældre NES-katalogprofil; Catalogue-CatBas-1.0 anbefales i stedet.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet katalog", + "navn": "urn:www.nesubl.eu:profiles:profile2:ver2.0 (NES2, simpel katalogproces)", + "id": "urn:www.nesubl.eu:profiles:profile2:ver2.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Catalogue", + "CatalogueItemSpecificationUpdate", + "CataloguePricingUpdate" + ], + "anvendelse": "Katalog med efterfølgende opdatering af varebeskrivelser og priser. Ingen kvittering i profilen.", + "bemaerkninger": "Ældre NES-katalogprofil; Catalogue-CatExt-1.0 anbefales i stedet.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet ordre til fakturering", + "navn": "Procurement-OrdAdv-BilSim-1.0 (avanceret ordre + simpel fakturering)", + "id": "Procurement-OrdAdv-BilSim-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order", + "OrderResponse", + "OrderResponseSimple", + "OrderChange", + "OrderCancellation", + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse *" + ], + "anvendelse": "Avanceret ordreproces (fuldt ordresvar samt ordreændring og -annullering) efterfulgt af simpel fakturering.", + "bemaerkninger": "Understøtter begge parter både OrdSim-BilSim og OrdAdv-BilSim, skal OrdAdv-BilSim bruges.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet ordre til fakturering", + "navn": "Procurement-OrdAdv-BilSimR-1.0 (avanceret ordre + fakturering m. krævet fakturasvar)", + "id": "Procurement-OrdAdv-BilSimR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order", + "OrderResponse", + "OrderResponseSimple", + "OrderChange", + "OrderCancellation", + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse *" + ], + "anvendelse": "Som OrdAdv-BilSim, men fakturasvar (ApplicationResponse) er påkrævet.", + "bemaerkninger": "R sidder på fakturadelen, ordresvaret er stadig valgfrit.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet ordre til fakturering", + "navn": "Procurement-OrdAdvR-BilSim-1.0 (avanceret ordre m. krævet ordresvar + simpel fakturering)", + "id": "Procurement-OrdAdvR-BilSim-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order", + "OrderResponse", + "OrderResponseSimple", + "OrderChange", + "OrderCancellation", + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse *" + ], + "anvendelse": "Som OrdAdv-BilSim, men ordresvar er påkrævet.", + "bemaerkninger": "R sidder på ordredelen, fakturasvaret er valgfrit.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet ordre til fakturering", + "navn": "Procurement-OrdAdvR-BilSimR-1.0 (avanceret ordre + fakturering, begge svar krævet)", + "id": "Procurement-OrdAdvR-BilSimR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order", + "OrderResponse", + "OrderResponseSimple", + "OrderChange", + "OrderCancellation", + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse *" + ], + "anvendelse": "Avanceret ordre og fakturering hvor både ordresvar og fakturasvar er påkrævet.", + "bemaerkninger": "Strammeste variant af ordre-til-faktura-kæden.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet ordre til fakturering", + "navn": "Procurement-OrdSel-BilSim-1.0 (sælgerinitieret ordre + simpel fakturering)", + "id": "Procurement-OrdSel-BilSim-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "OrderResponse", + "OrderResponseSimple", + "OrderChange", + "OrderCancellation", + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse *" + ], + "anvendelse": "Sælgerinitieret handel: leverandøren starter med en ordrebekræftelse (fx efter aftale i marken eller telefonisk), kunden kan ændre eller annullere, og der faktureres bagefter.", + "bemaerkninger": "Bemærk: ingen Order i profilen, \"Sel\" = seller initiated, så processen starter med OrderResponse.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet ordre til fakturering", + "navn": "Procurement-OrdSel-BilSimR-1.0 (sælgerinitieret ordre + fakturering m. krævet svar)", + "id": "Procurement-OrdSel-BilSimR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "OrderResponse", + "OrderResponseSimple", + "OrderChange", + "OrderCancellation", + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse *" + ], + "anvendelse": "Som OrdSel-BilSim, men fakturasvar er påkrævet.", + "bemaerkninger": "Ingen Order i profilen, processen starter hos sælger.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet ordre til fakturering", + "navn": "Procurement-OrdSim-BilSimR-1.0 (simpel ordre + fakturering m. krævet fakturasvar)", + "id": "Procurement-OrdSim-BilSimR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order", + "OrderResponseSimple", + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse *" + ], + "anvendelse": "Simpel ordre + fakturering hvor fakturasvar (ApplicationResponse) er påkrævet.", + "bemaerkninger": "R sidder på fakturadelen.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet ordre til fakturering", + "navn": "Procurement-OrdSimR-BilSim-1.0 (simpel ordre m. krævet ordresvar + fakturering)", + "id": "Procurement-OrdSimR-BilSim-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order", + "OrderResponseSimple", + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse *" + ], + "anvendelse": "Som OrdSim-BilSim, men ordresvar er påkrævet.", + "bemaerkninger": "R sidder på ordredelen.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet ordre til fakturering", + "navn": "Procurement-OrdSimR-BilSimR-1.0 (simpel ordre + fakturering, begge svar krævet)", + "id": "Procurement-OrdSimR-BilSimR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order", + "OrderResponseSimple", + "Invoice", + "CreditNote", + "Reminder", + "ApplicationResponse *" + ], + "anvendelse": "Simpel ordre og fakturering hvor både ordresvar og fakturasvar er påkrævet.", + "bemaerkninger": "Begge R’er sat, modparten skal kvittere i begge led.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Udvidet ordre til fakturering", + "navn": "urn:www.nesubl.eu:profiles:profile7:ver2.0 (NES7, ordre til faktura)", + "id": "urn:www.nesubl.eu:profiles:profile7:ver2.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "Order", + "OrderResponseSimple", + "Invoice", + "CreditNote", + "ApplicationResponse *" + ], + "anvendelse": "NES-profil for simpel ordre efterfulgt af simpel fakturering.", + "bemaerkninger": "Ældre NES-pendant til OrdSim-BilSim. Ingen Reminder i profilen.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "ForsyningsSpecifikation", + "navn": "Reference-Utility-1.0 (forsyningsspecifikation, UTS)", + "id": "Reference-Utility-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "UtilityStatement", + "ApplicationResponse" + ], + "anvendelse": "Forsyningsbranchen (el/vand/varme/gas): leverandør sender detaljeret forbrugs-/forsyningsspecifikation der ledsager fakturaen; kunden kan svare med ApplicationResponse.", + "bemaerkninger": "Bruges som tillæg til en faktureringsprofil, UTS erstatter ikke fakturaen.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "ForsyningsSpecifikation", + "navn": "Reference-UtilityR-1.0 (forsyningsspecifikation m. krævet svar)", + "id": "Reference-UtilityR-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "UtilityStatement", + "ApplicationResponse" + ], + "anvendelse": "Som Reference-Utility, men kunden skal kvittere for forsyningsspecifikationen.", + "bemaerkninger": "R = svar påkrævet.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "kategori": "Diverse", + "navn": "Procurement-TecRes-1.0 (kvittering på modtaget dokument)", + "id": "Procurement-TecRes-1.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "ApplicationResponse" + ], + "anvendelse": "Ren kvitteringsprofil: bruges til at svare på et modtaget dokument, typisk når afsendelsen ikke kan behandles pga. tekniske fejl eller en ikke-understøttet profil.", + "bemaerkninger": "Bruges som ProfileID på selve ApplicationResponse’en, ikke som en forretningsproces man handler i.", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Nemhandel OIOUBL (UBL 2.1)", + "navn": "urn:www.cenbii.eu:profile:bii30:ver2.0 (forsendelsesadvis / despatch advice)", + "id": "urn:www.cenbii.eu:profile:bii30:ver2.0", + "customization": "OIOUBL-2.x", + "dokumenter": [ + "DespatchAdvice" + ], + "anvendelse": "Leverandør varsler en forsendelse, så kunden kan forberede modtagelse og afstemme leveringen mod ordren.", + "bemaerkninger": "CEN BII-profil optaget i OIOUBL (G26 afsnit 3.3.15). Ikke placeret i en registerkategori her, bekræft gruppen i Nemhandelsregistrets dropdown før brug.", + "kategori": "", + "kilde": { + "label": "OIOUBL Profiler (G26)", + "url": "https://oioubl21.oioubl.dk/documents/en/en/Guidelines/OIOUBL_GUIDE_PROFILES.pdf" + } + }, + { + "standard": "Peppol BIS 3.0", + "navn": "Peppol BIS Billing 3.0", + "id": "urn:fdc:peppol.eu:2017:poacc:billing:01:1.0", + "customization": "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0", + "dokumenter": [ + "Invoice", + "CreditNote" + ], + "anvendelse": "Den centrale e-faktura/kreditnota-pakke i Peppol. CIUS af EN 16931. Bruges til EU-grænsehandel og DK/Peppol-fakturering.", + "bemaerkninger": "Langt den mest brugte BIS3-profil. Invoice Response er en separat profil. Bemærk: Reminder findes ikke i Peppol, brug OIOUBL-profilerne til rykkere.", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · BIS Billing 3.0", + "url": "https://docs.peppol.eu/poacc/billing/3.0/" + } + }, + { + "standard": "Peppol BIS 3.0", + "navn": "Peppol BIS Self-Billing 3.0", + "id": "urn:fdc:peppol.eu:2017:poacc:selfbilling:01:1.0", + "customization": "urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0#conformant#urn:fdc:peppol.eu:2017:poacc:selfbilling:international:3.0", + "dokumenter": [ + "Invoice (self-billed)", + "CreditNote" + ], + "anvendelse": "Selvfakturering: køber udsteder fakturaen på vegne af sælger.", + "bemaerkninger": "Genbruger Billing-syntaks med self-billing customization.", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · Self-Billing 3.0", + "url": "https://docs.peppol.eu/poacc/self-billing/3.0/" + } + }, + { + "standard": "Peppol BIS 3.0", + "navn": "Peppol BIS Invoice Response 3.0", + "id": "urn:fdc:peppol.eu:poacc:bis:invoice_response:3", + "customization": "urn:fdc:peppol.eu:poacc:trns:invoice_response:3", + "dokumenter": [ + "ApplicationResponse (Invoice Response)" + ], + "anvendelse": "Køber sender struktureret status/svar på en modtaget faktura (accepteret, afvist, under behandling, betalt).", + "bemaerkninger": "Bruges sammen med Billing til at lukke faktura-livscyklussen. Peppol-pendant til OIOUBL-ernes \"R\"-profiler.", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · BIS v3", + "url": "https://docs.peppol.eu/poacc/upgrade-3/" + } + }, + { + "standard": "Peppol BIS 3.0", + "navn": "Peppol BIS Message Level Response 3.0 (MLR)", + "id": "urn:fdc:peppol.eu:poacc:bis:mlr:3", + "customization": "urn:fdc:peppol.eu:poacc:trns:mlr:3", + "dokumenter": [ + "ApplicationResponse (Message Level Response)" + ], + "anvendelse": "Teknisk kvittering på beskedniveau: bekræfter modtagelse/validering af et hvilket som helst dokument.", + "bemaerkninger": "Transport-/syntaks-niveau svar, ikke forretningsmæssigt indhold. Peppol-pendant til Procurement-TecRes-1.0.", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · BIS v3", + "url": "https://docs.peppol.eu/poacc/upgrade-3/" + } + }, + { + "standard": "Peppol BIS 3.0", + "navn": "Peppol BIS Ordering 3.0", + "id": "urn:fdc:peppol.eu:poacc:bis:ordering:3", + "customization": "urn:fdc:peppol.eu:poacc:trns:order:3 / urn:fdc:peppol.eu:poacc:trns:order_response:3", + "dokumenter": [ + "Order", + "Order Response" + ], + "anvendelse": "Fuld ordreproces: køber sender ordre, sælger svarer med ordrebekræftelse/-svar.", + "bemaerkninger": "Order = Trdm01, Order Response = Trdm76. Peppol-pendant til Procurement-OrdSim-1.0.", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · BIS v3", + "url": "https://docs.peppol.eu/poacc/upgrade-3/" + } + }, + { + "standard": "Peppol BIS 3.0", + "navn": "Peppol BIS Order Only 3.0", + "id": "urn:fdc:peppol.eu:poacc:bis:order_only:3", + "customization": "urn:fdc:peppol.eu:poacc:trns:order:3", + "dokumenter": [ + "Order" + ], + "anvendelse": "Kun afsendelse af ordre uden forventet struktureret ordresvar.", + "bemaerkninger": "Enkel ordre-only proces. Peppol-pendant til NES3 (\"Order Only\").", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · BIS v3", + "url": "https://docs.peppol.eu/poacc/upgrade-3/" + } + }, + { + "standard": "Peppol BIS 3.0", + "navn": "Peppol BIS Order Agreement 3.0 (Advanced Ordering)", + "id": "urn:fdc:peppol.eu:poacc:bis:order_agreement:3", + "customization": "urn:fdc:peppol.eu:poacc:trns:order_agreement:3", + "dokumenter": [ + "Order", + "Order Response (Advanced)" + ], + "anvendelse": "Avanceret ordre der etablerer en bindende ordreaftale mellem parterne.", + "bemaerkninger": "Bruges hvor ordresvar danner juridisk aftale.", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · BIS v3", + "url": "https://docs.peppol.eu/poacc/upgrade-3/" + } + }, + { + "standard": "Peppol BIS 3.0", + "navn": "Peppol BIS Catalogue 3.0", + "id": "urn:fdc:peppol.eu:poacc:bis:catalogue:3", + "customization": "urn:fdc:peppol.eu:poacc:trns:catalogue:3", + "dokumenter": [ + "Catalogue", + "ApplicationResponse (Catalogue Response)" + ], + "anvendelse": "Sælger sender produktkatalog (priser/varer) til køber, der kan svare med accept/afvisning.", + "bemaerkninger": "Grundlag for efterfølgende ordrer. Peppol-pendant til Catalogue-CatBasR-1.0.", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · BIS v3", + "url": "https://docs.peppol.eu/poacc/upgrade-3/" + } + }, + { + "standard": "Peppol BIS 3.0", + "navn": "Peppol BIS Catalogue without Response 3.0", + "id": "urn:fdc:peppol.eu:poacc:bis:catalogue_wo_response:3", + "customization": "urn:fdc:peppol.eu:poacc:trns:catalogue:3", + "dokumenter": [ + "Catalogue" + ], + "anvendelse": "Katalogudsendelse uden forventet katalogsvar.", + "bemaerkninger": "Variant uden Catalogue Response.", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · BIS v3", + "url": "https://docs.peppol.eu/poacc/upgrade-3/" + } + }, + { + "standard": "Peppol BIS 3.0", + "navn": "Peppol BIS Punch Out 3.0", + "id": "urn:fdc:peppol.eu:poacc:bis:punch_out:3", + "customization": "urn:fdc:peppol.eu:poacc:trns:punch_out:3", + "dokumenter": [ + "Catalogue (Punch Out)" + ], + "anvendelse": "Punch-out katalog: køber 'hopper ud' til sælgers webshop og henter en kurv tilbage.", + "bemaerkninger": "Bruges i e-handels-/webshop-integrationer.", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · BIS v3", + "url": "https://docs.peppol.eu/poacc/upgrade-3/" + } + }, + { + "standard": "Peppol BIS 3.0", + "navn": "Peppol BIS Despatch Advice 3.0", + "id": "urn:fdc:peppol.eu:poacc:bis:despatch_advice:3", + "customization": "urn:fdc:peppol.eu:poacc:trns:despatch_advice:3", + "dokumenter": [ + "Despatch Advice (følgeseddel)" + ], + "anvendelse": "Sælger sender forsendelses-/leveringsmeddelelse (hvad er afsendt, hvornår, hvordan) inden faktura.", + "bemaerkninger": "Logistik-dokument mellem ordre og faktura. Peppol-pendant til urn:www.cenbii.eu:profile:bii30:ver2.0 i OIOUBL.", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · BIS v3", + "url": "https://docs.peppol.eu/poacc/upgrade-3/" + } + }, + { + "standard": "Peppol BIS 4.0 / PINT", + "navn": "EU PINT Billing (v1.0.1; forløber for BIS Billing 4.0)", + "id": "urn:peppol:pint:billing-1@eu-1 (PINT EU Billing)", + "customization": "urn:peppol:pint:billing-1@eu-1", + "dokumenter": [ + "Invoice", + "CreditNote" + ], + "anvendelse": "Peppols internationale fakturamodel (PINT) tilpasset EU. Implementerer BIS Billing 3.0 som PINT-kompatibel uden ændringer; baner vej for grænseoverskridende fakturering mod lande med egne PINT-specs (AU/JP/MY/NZ/SG m.fl.).", + "bemaerkninger": "v1.0.0 udgivet okt. 2025, nu v1.0.1. CIUS af EN 16931. NB: identifikator-format afviger fra BIS3, verificér mod docs.peppol.eu/poac/eu/pint-eu inden brug.", + "kategori": "", + "kilde": { + "label": "docs.peppol.eu · EU PINT Billing", + "url": "https://docs.peppol.eu/poac/eu/pint-eu/" + } + }, + { + "standard": "Peppol BIS 4.0 / PINT", + "navn": "Peppol BIS Billing 4.0 (kommende)", + "id": "(endnu ikke endeligt fastlagt)", + "customization": "(endnu ikke endeligt fastlagt)", + "dokumenter": [ + "Invoice", + "CreditNote (forventet)" + ], + "anvendelse": "Næste generation der lægger BIS og PINT sammen til ét globalt faktura-format. Forventet 2026, endnu ikke udgivet (pr. juni 2026).", + "bemaerkninger": "STATUS: under udarbejdelse pr. juni 2026, ID'er og indhold ikke endeligt publiceret. Brug BIS Billing 3.0 / EU PINT i mellemtiden.", + "kategori": "", + "kilde": { + "label": "Peppol roadmap", + "url": "https://docs.peppol.eu/poac/" + } + } + ] +} diff --git a/data/seo.json b/data/seo.json new file mode 100644 index 0000000..40c6271 --- /dev/null +++ b/data/seo.json @@ -0,0 +1,144 @@ +{ + "baseGraph": { + "@context": "https://schema.org", + "@graph": [ + { + "@type": "WebSite", + "@id": "https://asger.weirsoe.dk/#website", + "url": "https://asger.weirsoe.dk/", + "name": "Asger Weirsøe", + "description": "Asger Weirsøe er datalog (cand.scient. fra Aalborg Universitet) og Technical Solution Architect hos Digisense. Peppol, NemHandel, e-faktura, selvhosting, Proxmox og AI.", + "inLanguage": "da-DK", + "publisher": { + "@id": "https://asger.weirsoe.dk/#person" + } + }, + { + "@type": "Person", + "@id": "https://asger.weirsoe.dk/#person", + "name": "Asger Geel Weirsøe", + "alternateName": "Asger Weirsøe", + "url": "https://asger.weirsoe.dk/", + "image": "https://asger.weirsoe.dk/asger.png", + "jobTitle": [ + "Datalog", + "Technical Solution Architect" + ], + "hasOccupation": { + "@type": "Occupation", + "name": "Datalog", + "occupationalCategory": "2512 Softwareudvikler" + }, + "hasCredential": [ + { + "@type": "EducationalOccupationalCredential", + "credentialCategory": "degree", + "educationalLevel": "Master's degree", + "name": "Cand.scient. i Datalogi", + "recognizedBy": { + "@type": "CollegeOrUniversity", + "name": "Aalborg Universitet" + } + }, + { + "@type": "EducationalOccupationalCredential", + "credentialCategory": "license", + "name": "Radioamatørlicens, kategori B (kaldesignal OZ0AGW)", + "dateCreated": "2024-05-18" + }, + { + "@type": "EducationalOccupationalCredential", + "credentialCategory": "license", + "name": "Jagttegn", + "dateCreated": "2020" + } + ], + "callSign": "OZ0AGW", + "worksFor": { + "@type": "Organization", + "name": "Digisense A/S" + }, + "alumniOf": { + "@type": "CollegeOrUniversity", + "name": "Aalborg Universitet" + }, + "homeLocation": { + "@type": "Place", + "name": "Nørresundby, Danmark" + }, + "nationality": { + "@type": "Country", + "name": "Danmark" + }, + "knowsLanguage": [ + "da", + "en" + ], + "knowsAbout": [ + "Peppol", + "Peppol Access Point", + "Peppol BIS", + "Peppol eDelivery", + "Oxalis", + "NemHandel", + "Elektronisk fakturering", + "E-faktura", + "Electronic invoicing", + "OIOUBL", + "UBL", + "e-Delivery (AS4)", + "EN 16931", + "E-procurement", + "EDI", + "Softwarearkitektur", + "Løsningsarkitektur", + "Forretningsforståelse", + "Digitalisering af fakturaflows", + "Rådgivning", + "Kravspecifikation", + "Incident response", + "Incident management", + "Hændelseshåndtering", + "ISO/IEC 27001", + "Informationssikkerhed", + "ISMS", + "Sårbarhedshåndtering", + "Proxmox", + "Selvhosting", + "Kunstig intelligens", + "Linux", + "DevOps", + "OIDC", + "SSO", + "Fuld-stack udvikling", + "Docker", + "Amatørradio", + "Radioamatør", + "Radiokommunikation" + ], + "sameAs": [ + "https://www.linkedin.com/in/ag-weirs%C3%B8e/", + "https://mastodon.social/@asgerweirsoee", + "https://github.com/asger-weirsoee", + "https://gitea.weircon.dk/agw", + "https://arxiv.org/abs/2310.00999" + ], + "description": "Asger Weirsøe er datalog (cand.scient. fra Aalborg Universitet) og Technical Solution Architect hos Digisense A/S. Han bygger bro mellem forretning og teknik og har en bred profil inden for elektronisk fakturering og e-procurement: Peppol (Access Point, eDelivery/AS4, Oxalis), NemHandel og OIOUBL, kombineret med forretningsforståelse, selvhosting, Proxmox og AI. Han er desuden licenseret radioamatør med B-licens og kaldesignalet OZ0AGW." + } + ] + }, + "profilePage": { + "@context": "https://schema.org", + "@type": "ProfilePage", + "@id": "https://asger.weirsoe.dk/#profilepage", + "url": "https://asger.weirsoe.dk/", + "name": "Asger Weirsøe", + "mainEntity": { + "@id": "https://asger.weirsoe.dk/#person" + }, + "about": { + "@id": "https://asger.weirsoe.dk/#person" + }, + "inLanguage": "da-DK" + } +} diff --git a/data/uses.json b/data/uses.json new file mode 100644 index 0000000..bc690fd --- /dev/null +++ b/data/uses.json @@ -0,0 +1,67 @@ +{ + "sections": [ + { + "title": "Homelab", + "items": [ + { + "name": "Proxmox VE", + "note": "Hypervisor på egen hardware. ~50 LXC-containere og et par KVM-VM’er, én tjeneste pr. container." + }, + { + "name": "NPMplus", + "note": "Reverse proxy og TLS-terminering på kanten. Alt trafik ind går her igennem." + }, + { + "name": "Authentik", + "note": "Self-hosted SSO (OIDC/LDAP). Én identitet til alle tjenester, bygget fra source." + }, + { + "name": "Gitea + Gitea Actions", + "note": "Egen git-hosting og CI/CD. Denne side deployes automatisk ved push." + }, + { + "name": "MariaDB / Redis / PostgreSQL", + "note": "Databaser som dedikerede containere." + }, + { + "name": "Matomo", + "note": "Self-hosted analytics, first-party og uden tredjeparter." + }, + { + "name": "WireGuard", + "note": "Tunneler og netns-baserede egress-profiler til alt der skal ud af huset på en pæn måde." + } + ] + }, + { + "title": "Udvikling", + "items": [ + { + "name": "Linux + code-server", + "note": "Udvikling foregår i en dedikeret LXC med VS Code i browseren." + }, + { + "name": "Java · Go · Python · TypeScript", + "note": "Java til Peppol/eDelivery-verdenen, Python/TS til alt det andet." + }, + { + "name": "Claude Code", + "note": "LLM-drevne workflows, fra blogindlæg til Proxmox-administration." + }, + { + "name": "Docker", + "note": "Hvor det giver mening. Ellers foretrækker jeg native installationer i LXC." + } + ] + }, + { + "title": "Denne side", + "items": [ + { + "name": "Astro 5 + Bun + Tailwind 4", + "note": "Statisk output, ingen JS-frameworks i browseren. Se kolofonen for detaljer." + } + ] + } + ] +} diff --git a/package.json b/package.json deleted file mode 100644 index c8e28f9..0000000 --- a/package.json +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "asger-weirsoe-dk", - "type": "module", - "private": true, - "scripts": { - "dev": "astro dev --host", - "build": "astro build", - "preview": "astro preview" - }, - "dependencies": { - "@astrojs/rss": "^4.0.11", - "@astrojs/sitemap": "^3.3.0", - "@fontsource-variable/inter": "^5.2.5", - "@tailwindcss/vite": "^4.1.5", - "@vite-pwa/astro": "^0.5.0", - "astro": "^5.7.0", - "mermaid": "^11.15.0", - "sharp": "^0.34.5", - "tailwindcss": "^4.1.5" - } -} diff --git a/public/.well-known/security.txt b/public/.well-known/security.txt deleted file mode 100644 index dd0564d..0000000 --- a/public/.well-known/security.txt +++ /dev/null @@ -1,5 +0,0 @@ -Contact: mailto:asger@weirsoe.dk -Expires: 2027-06-30T22:00:00.000Z -Preferred-Languages: da, en -Canonical: https://asger.xn--weirse-fya.dk/.well-known/security.txt -Policy: https://asger.xn--weirse-fya.dk/colophon diff --git a/scripts/build.sh b/scripts/build.sh new file mode 100755 index 0000000..29967e2 --- /dev/null +++ b/scripts/build.sh @@ -0,0 +1,8 @@ +#!/bin/sh +# Bygger sitet til public/. PW_PREVIEW=1 giver preview-buildet under /udkast. +set -eu +TOOLS="${TOOLS:-.tools}" +python3 scripts/prebuild.py +"$TOOLS/tailwindcss" --input assets/site.css --output static/css/site.css --minify +"$TOOLS/zola" build +python3 scripts/postbuild.py diff --git a/scripts/godkendelse.py b/scripts/godkendelse.py new file mode 100755 index 0000000..1ddca0d --- /dev/null +++ b/scripts/godkendelse.py @@ -0,0 +1,61 @@ +"""Godkendelsesstemplet på blogindlæg: HMAC-SHA256 over sti, indhold og billeder. + +canonical() skal matche godkendelsestjenesten (service/app.py i agw/pw-drafts) byte for byte. +""" +import hashlib +import hmac +import os +import re + +BLOG = "content/blog" + + +def canonical(rel, text, assets=()): + """assets: [(sti, sha256-hex)] for billederne i bundlen, sorteret efter sti.""" + lines = text.replace("\r\n", "\n").split("\n") + if lines and lines[0] == "---" and "---" in lines[1:]: + end = lines.index("---", 1) + lines = [l for i, l in enumerate(lines) if not (0 < i < end and re.match(r"\s*godkendt:", l))] + return f"{rel}\n" + "\n".join(lines) + "".join(f"\n@asset {p} {h}" for p, h in assets) + + +def sign(key, rel, text, assets=()): + return "v1:" + hmac.new(key, canonical(rel, text, assets).encode(), hashlib.sha256).hexdigest() + + +def sha256_file(path): + with open(path, "rb") as f: + return hashlib.sha256(f.read()).hexdigest() + + +def assets_for(slug): + """Alt i bundlen ud over index.md er billeder, og de ligger fladt.""" + d = f"{BLOG}/{slug}" + out = [] + for name in os.listdir(d): + if name == "index.md": + continue + p = f"{d}/{name}" + if not os.path.isfile(p): + raise ValueError(f"{p}: undermapper understøttes ikke") + if name.endswith(".md"): + raise ValueError(f"{p}: kun index.md må være markdown i en bundle") + out.append((p, sha256_file(p))) + return sorted(out) + + +def read_text(path): + with open(path, "rb") as f: + return f.read().decode() + + +def stamp_of(text): + """(stempel eller None, om indlægget er markeret draft: true)""" + lines = text.replace("\r\n", "\n").split("\n") + end = lines.index("---", 1) if lines[0] == "---" and "---" in lines[1:] else -1 + if end < 0: + return None, False + fm = lines[1:end] + g = next((l for l in fm if re.match(r"\s*godkendt:", l)), None) + draft = any(re.fullmatch(r"draft:\s*true\s*", l) for l in fm) + return (re.sub(r"^\s*godkendt:\s*", "", g).strip("'\"") if g else None), draft diff --git a/scripts/hent-vaerktoej.sh b/scripts/hent-vaerktoej.sh new file mode 100755 index 0000000..c5ba0d6 --- /dev/null +++ b/scripts/hent-vaerktoej.sh @@ -0,0 +1,21 @@ +#!/bin/sh +# Henter zola og Tailwind standalone-CLI til .tools/, fastlåst på version og sha256. +# Bruges af CI her og i agw/pw-drafts. +set -eu +ZOLA=0.23.6 +ZOLA_SHA256=7d77a4220a4f699cba2fc0e0c7859f780e887e269159ad4fab90e11ea16c906f +TAILWIND=4.3.3 +TAILWIND_SHA256=dc61b3ac6b8c9ca874c0cc4c57b2409791a64c5540404ca5f5367360babc313a + +mkdir -p .tools +curl -fsSL -o .tools/zola.tar.gz \ + "https://github.com/getzola/zola/releases/download/v$ZOLA/zola-v$ZOLA-x86_64-unknown-linux-musl.tar.gz" +curl -fsSL -o .tools/tailwindcss \ + "https://github.com/tailwindlabs/tailwindcss/releases/download/v$TAILWIND/tailwindcss-linux-x64" +sha256sum -c - < { - try { - return execFileSync('git', args, { encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim(); - } catch { - return null; - } -}; - -const sitemapUrls = () => { - const xml = fs.readFileSync('dist/sitemap-0.xml', 'utf8'); - return new Set([...xml.matchAll(/([^<]+)<\/loc>/g)].map((m) => m[1])); -}; - -// src/pages/foo.astro -> /foo/ ; src/content/blog/bar.md -> /blog/bar/ -const routeForFile = (f) => { - let m = f.match(/^src\/pages\/(.+)\.astro$/); - if (m) { - if (m[1].includes('[')) return null; // dynamiske ruter kan vi ikke slå op - if (m[1] === 'index') return '/'; - return `/${m[1].replace(/\/index$/, '')}/`; - } - m = f.match(/^src\/content\/blog\/(.+)\.md$/); - if (m) return `/blog/${m[1]}/`; - return null; -}; - -const changedFiles = () => { - const { GITHUB_EVENT_BEFORE, GITHUB_SHA } = process.env; - const zero = /^0*$/; - if (GITHUB_EVENT_BEFORE && !zero.test(GITHUB_EVENT_BEFORE)) { - const d = git(['diff', '--name-only', GITHUB_EVENT_BEFORE, GITHUB_SHA || 'HEAD']); - if (d !== null) return d ? d.split('\n') : []; - } - const d = git(['diff', '--name-only', 'HEAD~1', 'HEAD']); - return d ? d.split('\n') : null; -}; - -const files = changedFiles(); -if (files === null) { - console.log('IndexNow: kan ikke bestemme hvad der ændrede sig (shallow clone?) — springer over.'); - process.exit(0); -} - -const valid = sitemapUrls(); -const urls = [...new Set(files.map(routeForFile).filter(Boolean).map((r) => ORIGIN + r))].filter((u) => - valid.has(u), -); - -if (!urls.length) { - console.log('IndexNow: ingen sider med ændret indhold i dette push — springer over.'); - process.exit(0); -} - -const body = { - host: HOST, - key: KEY, - keyLocation: `${ORIGIN}/${KEY}.txt`, - urlList: urls, -}; - -console.log(`IndexNow: indsender ${urls.length} URL(er):`); -for (const u of urls) console.log(' ', u); - -// `node scripts/indexnow.mjs --dry-run` viser hvad der ville blive sendt. -if (process.argv.includes('--dry-run')) { - console.log('IndexNow: --dry-run, sender ikke.'); - process.exit(0); -} - -const res = await fetch('https://api.indexnow.org/indexnow', { - method: 'POST', - headers: { 'Content-Type': 'application/json; charset=utf-8' }, - body: JSON.stringify(body), -}); - -console.log(`IndexNow: HTTP ${res.status} ${res.statusText}`); -if (![200, 202].includes(res.status)) { - console.log(await res.text().catch(() => '')); - console.log('IndexNow: afvist — men deployet er allerede ude, så vi fejler ikke buildet.'); -} diff --git a/scripts/indexnow.py b/scripts/indexnow.py new file mode 100755 index 0000000..f3c5569 --- /dev/null +++ b/scripts/indexnow.py @@ -0,0 +1,77 @@ +#!/usr/bin/env python3 +"""Pinger IndexNow med de sider der er ændret i dette push. Kør efter buildet +(bruger public/sitemap-0.xml). --dry-run viser hvad der ville blive sendt.""" +import json +import os +import re +import subprocess +import sys +import urllib.error +import urllib.request + +HOST = "asger.weirsoe.dk" +KEY = "b68b31655cb9c1a662d53905dd0b1b06" +ORIGIN = f"https://{HOST}" + + +def git(*args): + r = subprocess.run(["git", *args], capture_output=True, text=True) + return r.stdout.strip() if r.returncode == 0 else None + + +def sitemap_urls(): + with open("public/sitemap-0.xml") as f: + return set(re.findall(r"([^<]+)", f.read())) + + +def route_for_file(f): + """Kanonisk form uden afsluttende skråstreg, som i sitemap'et.""" + m = re.fullmatch(r"content/(.+?)(/index|/_index|_index)?\.md", f) + if m: + return "/" + m[1].rstrip("/") if m[1] != "_index" else "/" + m = re.fullmatch(r"templates/sider/(.+)\.html", f) or re.fullmatch(r"data/(.+)\.json", f) + if m: + name = {"forside": "", "forside-en": "en", "profiles": "profiler"}.get(m[1], m[1]) + return "/en/" + name[:-3] if name.endswith("-en") else "/" + name + return None + + +def changed_files(): + before, sha = os.environ.get("GITHUB_EVENT_BEFORE"), os.environ.get("GITHUB_SHA") + if before and not re.fullmatch(r"0*", before): + d = git("diff", "--name-only", before, sha or "HEAD") + if d is not None: + return d.split("\n") if d else [] + d = git("diff", "--name-only", "HEAD~1", "HEAD") + return d.split("\n") if d else None + + +def main(): + files = changed_files() + if files is None: + print("IndexNow: kan ikke bestemme hvad der ændrede sig (shallow clone?), springer over.") + return + valid = sitemap_urls() + urls = sorted({ORIGIN + r for r in map(route_for_file, files) if r} & valid) + if not urls: + print("IndexNow: ingen sider med ændret indhold i dette push, springer over.") + return + print(f"IndexNow: indsender {len(urls)} URL(er):") + for u in urls: + print(" ", u) + if "--dry-run" in sys.argv: + print("IndexNow: --dry-run, sender ikke.") + return + body = {"host": HOST, "key": KEY, "keyLocation": f"{ORIGIN}/{KEY}.txt", "urlList": urls} + req = urllib.request.Request("https://api.indexnow.org/indexnow", data=json.dumps(body).encode(), + headers={"Content-Type": "application/json; charset=utf-8"}, method="POST") + try: + with urllib.request.urlopen(req, timeout=30) as r: + print(f"IndexNow: HTTP {r.status} {r.reason}") + except urllib.error.URLError as e: + # Deployet er allerede ude, så vi fejler ikke buildet. + print(f"IndexNow: afvist: {e}") + + +if __name__ == "__main__": + main() diff --git a/scripts/omstempl.py b/scripts/omstempl.py new file mode 100755 index 0000000..677b1b0 --- /dev/null +++ b/scripts/omstempl.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python3 +"""Stempler de udgivne indlæg om efter flytningen fra src/content/blog/.md til +content/blog//index.md. Stien og frontmatter indgår i stemplet, så de gamle er ugyldige. + +Kør på zola-branchen, med master stadig på Astro-versionen: + + PUBLISH_KEY= python3 scripts/omstempl.py [--fra master] + PUBLISH_KEY= python3 scripts/verify-approvals.py + git commit -am "blog: stempl indlæg om efter flytning til Zola" + +Scriptet underskriver kun det der allerede var godkendt. For hvert indlæg tjekker det at +det gamle stempel er gyldigt, og at den nye fil er præcis den gamle i nyt format. Fejler +ét indlæg, skrives der intet. +""" +import argparse +import hashlib +import hmac +import os +import re +import subprocess +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from godkendelse import BLOG, assets_for, read_text, sign, stamp_of # noqa: E402 + +OLD = "src/content/blog" + + +def git(*args, binary=False): + return subprocess.run(["git", *args], check=True, capture_output=True, text=not binary).stdout + + +def old_sign(key, rel, text, assets): + """Det gamle format: stemplet stod som topniveau-linje og er det eneste der skæres fra.""" + lines = text.replace("\r\n", "\n").split("\n") + end = lines.index("---", 1) + lines = [l for i, l in enumerate(lines) if not (0 < i < end and l.startswith("godkendt:"))] + tail = "".join(f"\n@asset {p} {h}" for p, h in assets) + body = f"{rel}\n" + "\n".join(lines) + tail + return "v1:" + hmac.new(key, body.encode(), hashlib.sha256).hexdigest() + + +def migrated(text, stamp): + """Den gamle fil i det nye format: tags under taxonomies, stemplet under extra.""" + lines = text.split("\n") + end = lines.index("---", 1) + head = [] + for l in lines[1:end]: + if l.startswith("godkendt:"): + continue + head += ["taxonomies:", " " + l] if l.startswith("tags:") else [l] + return "\n".join(["---", *head, "extra:", f' godkendt: "{stamp}"', *lines[end:]]) + + +def old_assets(ref, slug): + out = git("ls-tree", "--name-only", ref, f"{OLD}/{slug}/").split() + return sorted((p, hashlib.sha256(git("show", f"{ref}:{p}", binary=True)).hexdigest()) for p in out) + + +def main(): + ap = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter) + ap.add_argument("--fra", default="master", help="ref med de gamle, godkendte indlæg") + args = ap.parse_args() + if not os.environ.get("PUBLISH_KEY"): + sys.exit("PUBLISH_KEY mangler.") + key = bytes.fromhex(os.environ["PUBLISH_KEY"]) + + slugs = sorted(n for n in os.listdir(BLOG) if os.path.isdir(f"{BLOG}/{n}")) + new, errors = {}, [] + for slug in slugs: + rel = f"{BLOG}/{slug}/index.md" + old_rel = f"{OLD}/{slug}.md" + try: + old = git("show", f"{args.fra}:{old_rel}") + except subprocess.CalledProcessError: + errors.append(f"{rel}: findes ikke i {args.fra} som {old_rel}; kun godkendelsesflowet må stemple nyt") + continue + text = read_text(rel) + stamp_gl, draft = stamp_of(old) + o_assets = old_assets(args.fra, slug) + n_assets = assets_for(slug) + if draft or not stamp_gl or not hmac.compare_digest(stamp_gl, old_sign(key, old_rel, old, o_assets)): + errors.append(f"{old_rel}: det gamle stempel er ikke gyldigt med denne nøgle") + continue + if text != migrated(old, stamp_of(text)[0]): + errors.append(f"{rel}: indholdet er ændret i forhold til {args.fra}") + continue + if [h for _, h in o_assets] != [h for _, h in n_assets]: + errors.append(f"{rel}: billederne er ændret i forhold til {args.fra}") + continue + new[rel] = migrated(old, sign(key, rel, text, n_assets)) + if errors: + print("\n".join(f"✗ {e}" for e in errors), file=sys.stderr) + sys.exit(f"\n{len(errors)} fejl. Intet er skrevet.") + for rel, text in new.items(): + with open(rel, "w", encoding="utf-8", newline="") as f: + f.write(text) + print(f"✓ {rel}") + print(f"\n{len(new)} indlæg stemplet om. Kør scripts/verify-approvals.py og commit.") + + +if __name__ == "__main__": + main() diff --git a/scripts/postbuild.py b/scripts/postbuild.py new file mode 100755 index 0000000..83bf50f --- /dev/null +++ b/scripts/postbuild.py @@ -0,0 +1,157 @@ +#!/usr/bin/env python3 +"""Efterbehandling af Zolas output i public/: + +- overskrift-id'er som på den gamle side (bevarer æøå, så gamle #ankre stadig virker) +- sitemap-index.xml + sitemap-0.xml med lastmod fra git og frontmatter +- sw.js med precache-liste +- PW_PREVIEW=1: hele sitet flyttes ind under /udkast, uden sitemap og service worker +""" +import hashlib +import html +import json +import os +import pathlib +import re +import subprocess + +OUT = pathlib.Path("public") +SITE = "https://asger.weirsoe.dk" +PREVIEW = os.environ.get("PW_PREVIEW") == "1" +BASE = "/udkast" + + +def pages(): + return sorted(OUT.rglob("*.html")) + + +def slug(text): + """Samme regler som github-slugger, som den gamle build brugte.""" + s = html.unescape(re.sub(r"<[^>]+>", "", text)).strip().lower() + s = re.sub(r"[^\w\- ]", "", s) + return s.replace(" ", "-") + + +def heading_ids(): + for f in pages(): + src = f.read_text() + if 'class="prose' not in src: + continue + seen = {} + + def fix(m): + s = slug(m[3]) + n = seen.get(s, 0) + seen[s] = n + 1 + return f'' if n else f'' + + f.write_text(re.sub(r']*)>(.*?)(?=)', + lambda m: fix(m) + m[3], src, flags=re.S)) + + +def git_date(*files): + if int(subprocess.run(["git", "rev-list", "--count", "HEAD"], capture_output=True, text=True).stdout or 0) < 2: + return None + out = subprocess.run(["git", "log", "-1", "--format=%cI", "--", *files], capture_output=True, text=True).stdout + return out.strip() or None + + +def frontmatter(path): + text = path.read_text() + head = text.split("---\n", 2)[1] + return {k: v.strip().strip("'\"") for k, v in re.findall(r"^\s*(\w+):(.*)$", head, re.M)} + + +def iso(date): + return f"{date}T00:00:00.000Z" + + +def lastmods(): + """Rute -> lastmod: sidste commit for statiske sider, frontmatter for blogindlæg.""" + routes = {} + for md in pathlib.Path("content").rglob("*.md"): + fm = frontmatter(md) + if "blog" in md.parts or md.parts[1] == "projekter" or "projects" in md.parts: + continue + route = "/" + str(md.relative_to("content").with_suffix("")).removesuffix("_index") + route = route.rstrip("/") or "/" + tpl = fm.get("template") + d = git_date(str(md), f"templates/{tpl}") if tpl else git_date(str(md)) + if d: + routes[route] = d + posts = [] + for md in pathlib.Path("content/blog").glob("*/index.md"): + fm = frontmatter(md) + d = iso(fm.get("updated") or fm["date"]) + routes[f"/blog/{md.parent.name}"] = d + posts.append(d) + if posts: + routes["/blog"] = max(posts) + return routes + + +def sitemap(): + skip = ("/tags", "/peppol") + skip_exact = ("/cookies", "/status", "/chat") + routes = [] + for f in pages(): + rel = f.relative_to(OUT) + if rel.name != "index.html": + continue + r = "/" + str(rel.parent).replace(".", "", 1) if str(rel.parent) == "." else f"/{rel.parent}" + if r.startswith(skip) or r in skip_exact: + continue + routes.append(r) + routes.append("/tarballz/") + mods = lastmods() + urls = [] + for r in sorted(routes): + mod = mods.get(r) + urls.append(f"{SITE}{r}" + (f"{mod}" if mod else "") + + f"weekly{'1.0' if r == '/' else '0.7'}") + ns = 'xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"' + (OUT / "sitemap-0.xml").write_text(f'{"".join(urls)}') + newest = max(mods.values(), default=None) + (OUT / "sitemap-index.xml").write_text( + f'{SITE}/sitemap-0.xml' + + (f"{newest}" if newest else "") + "") + + +def service_worker(): + """Precache-listen afhænger af buildet, resten af sw.js ligger i scripts/sw.js.""" + exts = {".html", ".css", ".js", ".svg", ".png", ".woff2", ".xml", ".txt", ".webmanifest"} + files = sorted(p for p in OUT.rglob("*") if p.is_file() and p.suffix in exts + and p.name not in ("sw.js", "mermaid.min.js")) + h = hashlib.sha256() + for p in files: + h.update(p.read_bytes()) + urls = ["/" + str(p.relative_to(OUT)) for p in files] + src = pathlib.Path("scripts/sw.js").read_text() + src = src.replace("__VERSION__", h.hexdigest()[:12]).replace("__URLS__", json.dumps(urls, ensure_ascii=False)) + (OUT / "sw.js").write_text(src) + + +def rebase(): + """Rod-relative links og billeder i HTML peger ind under /udkast.""" + attr = re.compile(r'\b(href|src|action)="/(?!/|udkast)') + srcset = re.compile(r'\bsrcset="([^"]*)"') + + def fix_set(m): + return 'srcset="' + re.sub(r"(^|, )/(?!/)", rf"\1{BASE}/", m[1]) + '"' + + for f in pages(): + src = f.read_text() + f.write_text(srcset.sub(fix_set, attr.sub(rf'\1="{BASE}/', src))) + + +def main(): + (OUT / "sitemap.xml").unlink(missing_ok=True) + heading_ids() + if PREVIEW: + rebase() + else: + sitemap() + service_worker() + + +if __name__ == "__main__": + main() diff --git a/scripts/prebuild.py b/scripts/prebuild.py new file mode 100755 index 0000000..26eed7a --- /dev/null +++ b/scripts/prebuild.py @@ -0,0 +1,75 @@ +#!/usr/bin/env python3 +"""Henter det sitet viser fra Gitea ved build og skriver data/gitea.json: +repos til /projekter og aktivitets-heatmap til /now. Er Gitea nede, bygges sitet +alligevel, med en fejlbesked på /projekter og en tom graf på /now.""" +import datetime as dt +import json +import urllib.request + +BASE, USER = "https://gitea.weircon.dk", "agw" +HIDE = set() # repo-navne der skjules helt +PINNED = ["ubl-tools", "gtk-ahfail", "pi3-smart-workspace"] # vises først, i den rækkefølge +WEEKS = 26 +MAANEDER = ["januar", "februar", "marts", "april", "maj", "juni", "juli", "august", + "september", "oktober", "november", "december"] + + +def get(path): + req = urllib.request.Request(f"{BASE}/api/v1{path}", headers={"Accept": "application/json"}) + with urllib.request.urlopen(req, timeout=30) as r: + return json.load(r) + + +def repos(): + data = get(f"/users/{USER}/repos?limit=50&sort=updated") + rs = [r for r in data if not (r["fork"] or r["archived"] or r["private"] or r["name"].lower() in HIDE)] + pinned = [p.lower() for p in PINNED] + rs.sort(key=lambda r: r["updated_at"], reverse=True) + rs.sort(key=lambda r: pinned.index(r["name"].lower()) if r["name"].lower() in pinned else len(pinned)) + keep = ("name", "description", "html_url", "language", "stars_count") + return [{k: r[k] for k in keep} for r in rs] + + +def level(n): + return 0 if n == 0 else 1 if n < 3 else 2 if n < 6 else 3 if n < 12 else 4 + + +def heatmap(): + per_day = {} + try: + for b in get(f"/users/{USER}/heatmap"): + d = dt.datetime.fromtimestamp(b["timestamp"], dt.timezone.utc).date().isoformat() + per_day[d] = per_day.get(d, 0) + b["contributions"] + except Exception as e: + print(f"heatmap: {e}") + # 26 uger bagud, uger som kolonner (man-søn) + today = dt.date.today() + cur = today - dt.timedelta(days=WEEKS * 7 - 1 + today.weekday()) + weeks = [] + while cur <= today: + week = [] + for _ in range(7): + if cur > today: + break + n = per_day.get(cur.isoformat(), 0) + week.append({"date": cur.isoformat(), "n": n, "level": level(n)}) + cur += dt.timedelta(days=1) + weeks.append(week) + return weeks, sum(per_day.values()), f"{today.day}. {MAANEDER[today.month - 1]} {today.year}" + + +def main(): + try: + rs, error = repos(), False + except Exception as e: + print(f"repos: {e}") + rs, error = [], True + weeks, total, updated = heatmap() + with open("data/gitea.json", "w") as f: + json.dump({"repos": rs, "error": error, "weeks": weeks, "weeks_shown": WEEKS, + "total": total, "updated": updated}, f, ensure_ascii=False) + print(f"gitea: {len(rs)} repos, {total} bidrag") + + +if __name__ == "__main__": + main() diff --git a/scripts/sw.js b/scripts/sw.js new file mode 100644 index 0000000..f78b3ee --- /dev/null +++ b/scripts/sw.js @@ -0,0 +1,24 @@ +// Håndskrevet service worker: precacher sitet, så det virker offline. +// postbuild.py sætter version og URL-liste ind. Alt der ikke er precachet, går til netværket. +const CACHE = 'pw-__VERSION__'; +const URLS = __URLS__; +const known = new Set(URLS); + +self.addEventListener('install', (e) => { + e.waitUntil(caches.open(CACHE).then((c) => c.addAll(URLS)).then(() => self.skipWaiting())); +}); + +// Rydder forrige version, også de gamle workbox-caches. +self.addEventListener('activate', (e) => { + e.waitUntil(caches.keys() + .then((keys) => Promise.all(keys.filter((k) => k !== CACHE).map((k) => caches.delete(k)))) + .then(() => self.clients.claim())); +}); + +self.addEventListener('fetch', (e) => { + const url = new URL(e.request.url); + if (e.request.method !== 'GET' || url.origin !== location.origin) return; + const p = url.pathname; + const key = [p, p.replace(/\/?$/, '/index.html')].find((k) => known.has(k)); + if (key) e.respondWith(caches.match(key).then((r) => r || fetch(e.request))); +}); diff --git a/scripts/verify-approvals.mjs b/scripts/verify-approvals.mjs deleted file mode 100644 index c04f4c3..0000000 --- a/scripts/verify-approvals.mjs +++ /dev/null @@ -1,79 +0,0 @@ -// Stopper buildet hvis et blogindlæg mangler et gyldigt `godkendt:`-stempel -// (HMAC-SHA256 over sti, indhold og billeder). -// -// PUBLISH_KEY= node scripts/verify-approvals.mjs -import crypto from 'node:crypto'; -import fs from 'node:fs'; - -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, assets = []) => { - 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); - } - } - const tail = assets.map(([p, h]) => `\n@asset ${p} ${h}`).join(''); - return `${relPath}\n${lines.join('\n')}${tail}`; -}; - -const sha256 = (buf) => crypto.createHash('sha256').update(buf).digest('hex'); - -// Billeder til .md ligger fladt i mappen / ved siden af. -const assetsFor = (rel) => { - const dir = rel.replace(/\.mdx?$/, ''); - if (!fs.existsSync(dir)) return []; - return fs.readdirSync(dir, { withFileTypes: true }) - .map((e) => { - if (!e.isFile()) throw new Error(`${dir}/${e.name}: undermapper understøttes ikke`); - const p = `${dir}/${e.name}`; - return [p, sha256(fs.readFileSync(p))]; - }) - .sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0)); -}; - -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 entries = fs.readdirSync(DIR, { withFileTypes: true }); -const files = entries.filter((e) => e.isFile() && /\.mdx?$/.test(e.name)).map((e) => `${DIR}/${e.name}`); - -let bad = 0; -// En billedmappe uden et indlæg ved siden af er aldrig blevet godkendt. -for (const e of entries.filter((e) => e.isDirectory())) { - if (!files.some((f) => f.replace(/\.mdx?$/, '') === `${DIR}/${e.name}`)) { - console.error(`✗ ${DIR}/${e.name}/: filer uden et godkendt indlæg`); bad++; - } -} -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; } - let assets; - try { assets = assetsFor(rel); } catch (err) { console.error(`✗ ${err.message}`); bad++; continue; } - const want = 'v1:' + crypto.createHmac('sha256', Buffer.from(key, 'hex')).update(canonical(rel, text, assets)).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}${assets.length ? ` (+${assets.length} billeder)` : ''}`); -} -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/scripts/verify-approvals.py b/scripts/verify-approvals.py new file mode 100755 index 0000000..6e2bb15 --- /dev/null +++ b/scripts/verify-approvals.py @@ -0,0 +1,65 @@ +#!/usr/bin/env python3 +"""Stopper buildet hvis et blogindlæg mangler et gyldigt `godkendt:`-stempel. + + PUBLISH_KEY= python3 scripts/verify-approvals.py +""" +import hmac +import os +import sys + +sys.path.insert(0, os.path.dirname(os.path.abspath(__file__))) +from godkendelse import BLOG, assets_for, read_text, sign, stamp_of # noqa: E402 + + +def main(): + key = os.environ.get("PUBLISH_KEY") + if not key: + print("PUBLISH_KEY mangler; kan ikke verificere godkendelser.", file=sys.stderr) + return 1 + key = bytes.fromhex(key) + bad = ok = 0 + + def fail(msg): + nonlocal bad + print(f"✗ {msg}", file=sys.stderr) + bad += 1 + + for name in sorted(os.listdir(BLOG)): + path = f"{BLOG}/{name}" + if name == "_index.md": + continue + # Zola bygger enhver .md under content/blog, så intet må ligge uden for en bundle. + if not os.path.isdir(path): + fail(f"{path}: et indlæg skal ligge som {BLOG}//index.md") + continue + rel = f"{path}/index.md" + if not os.path.isfile(rel): + fail(f"{path}/: filer uden et godkendt indlæg") + continue + text = read_text(rel) + stamp, draft = stamp_of(text) + if draft: + fail(f"{rel}: udkast (draft: true) må ikke ligge i det offentlige repo") + continue + if not stamp: + fail(f"{rel}: mangler godkendelse") + continue + try: + assets = assets_for(name) + except ValueError as e: + fail(str(e)) + continue + if not hmac.compare_digest(stamp, sign(key, rel, text, assets)): + fail(f"{rel}: godkendelsen passer ikke til indholdet (ændret efter godkendelse?)") + continue + ok += 1 + print(f"✓ {rel}" + (f" (+{len(assets)} billeder)" if assets else "")) + if bad: + print(f"\n{bad} indlæg er ikke godkendt. Intet bliver udgivet.", file=sys.stderr) + return 1 + print(f"\nAlle {ok} indlæg er godkendt.") + return 0 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/src/assets/asger.png b/src/assets/asger.png deleted file mode 100644 index 47f3306..0000000 Binary files a/src/assets/asger.png and /dev/null differ diff --git a/src/components/Head.astro b/src/components/Head.astro deleted file mode 100644 index f525299..0000000 --- a/src/components/Head.astro +++ /dev/null @@ -1,117 +0,0 @@ ---- -import { SITE } from '../consts'; -import { baseGraph } from '../seo'; - -interface Props { - title?: string; - description?: string; - image?: string; - type?: 'website' | 'article'; - publishedTime?: string; - modifiedTime?: string; - tags?: string[]; - jsonLd?: unknown[]; - // noindex, follow - noindex?: boolean; - // Sidens sprog (standard: dansk) og dens oversættelser, som hreflang-links. - lang?: 'da' | 'en'; - alternates?: { lang: 'da' | 'en'; path: string }[]; -} -const { - title, description, image, type = 'website', - publishedTime, modifiedTime, tags = [], jsonLd = [], noindex = false, - lang = 'da', alternates = [], -} = Astro.props; -const hreflang = alternates.map((a) => ({ lang: a.lang, href: new URL(a.path, SITE.url).href })); -const xDefault = hreflang.find((a) => a.lang === 'da'); - -const pageTitle = title ? `${title} · ${SITE.name}` : `${SITE.title} · Datalog & Technical Solution Architect`; -const desc = description ?? SITE.description; -// Kanonisk form er uden afsluttende skråstreg, som i sitemap og interne links. -const canonicalPath = Astro.url.pathname.replace(/\/$/, '') || '/'; -const canonical = new URL(canonicalPath, SITE.url).href; -const ogImage = new URL(image ?? '/og.png', SITE.url).href; -const graphs = [baseGraph, ...jsonLd]; ---- - - -{pageTitle} - - - - - - - - - - - - - -{hreflang.map((a) => )} -{xDefault && } - - - - -{type === 'article' && publishedTime && } -{type === 'article' && modifiedTime && } -{type === 'article' && } -{type === 'article' && tags.map((t) => )} - - - - - - - - - - - - - - - - -{graphs.map((g) => diff --git a/src/components/PostCard.astro b/src/components/PostCard.astro deleted file mode 100644 index 2f77c9c..0000000 --- a/src/components/PostCard.astro +++ /dev/null @@ -1,19 +0,0 @@ ---- -interface Props { href: string; title: string; date: Date; description?: string; tags?: string[]; } -const { href, title, date, description, tags = [] } = Astro.props; -const d = date.toLocaleDateString('da-DK', { year: 'numeric', month: 'long', day: 'numeric' }); ---- - diff --git a/src/components/ProjektSide.astro b/src/components/ProjektSide.astro deleted file mode 100644 index ee0948e..0000000 --- a/src/components/ProjektSide.astro +++ /dev/null @@ -1,78 +0,0 @@ ---- -// Én projektside, på dansk eller engelsk. Ruterne /projekter/[slug] og -// /en/projects/[slug] giver bare indgangen og evt. oversættelsen med. -import type { CollectionEntry } from 'astro:content'; -import { render } from 'astro:content'; -import Base from '../layouts/Base.astro'; -import { SITE } from '../consts'; -import { softwareSourceCode, breadcrumb } from '../seo'; - -interface Props { - side: CollectionEntry<'projekter'>; - oversat?: CollectionEntry<'projekter'>; // samme projekt på det andet sprog -} -const { side, oversat } = Astro.props; -const { Content } = await render(side); -const d = side.data; -const en = d.lang === 'en'; -const slug = side.id.replace(/^en\//, ''); -const sti = (lang: 'da' | 'en') => (lang === 'en' ? `/en/projects/${slug}` : `/projekter/${slug}`); -const oversigt = en ? '/en/projects' : '/projekter'; - -const T = en - ? { hjem: 'Home', projekter: 'Projects', sprog: 'Language', licens: 'Licence', koerer: 'Runs on', - kilde: 'Source on Gitea', bin: 'Prebuilt packages' } - : { hjem: 'Forside', projekter: 'Projekter', sprog: 'Sprog', licens: 'Licens', koerer: 'Kører på', - kilde: 'Kildekode på Gitea', bin: 'Færdigbyggede pakker' }; - -const url = new URL(sti(d.lang), SITE.url).href; -const repo = `${SITE.giteaBase}/${SITE.giteaUser}/${d.repo}`; -const aur = d.aur.map((n) => ({ navn: n, url: `https://aur.archlinux.org/packages/${n}` })); -const published = d.date.toISOString(); -const modified = (d.updated ?? d.date).toISOString(); -const ld = softwareSourceCode({ - url, name: d.title, description: d.description, codeRepository: repo, - language: d.sprog, license: d.licens, platforms: d.platforme, - sameAs: aur.map((a) => a.url), dateCreated: published, dateModified: modified, keywords: d.tags, - lang: d.lang, -}); -const crumbs = breadcrumb([ - { name: T.hjem, url: `${SITE.url}${en ? '/en' : '/'}` }, - { name: T.projekter, url: `${SITE.url}${oversigt}` }, - { name: d.title, url }, -]); -const alternates = oversat - ? [{ lang: d.lang, path: sti(d.lang) }, { lang: oversat.data.lang, path: sti(oversat.data.lang) }] - : []; ---- - -
- -
-
-
-

{d.title}

-

{d.description}

-
-
{T.sprog}
{d.sprog}
-
{T.licens}
{d.licens}
- {d.platforme.length > 0 &&
{T.koerer}
{d.platforme.join(', ')}
} -
-
- {T.kilde} - {aur.map((a) => ( - AUR: {a.navn} - ))} - {d.aur.some((n) => n.endsWith('-bin')) && ( - {T.bin} - )} -
-
-
-
- diff --git a/src/consts.ts b/src/consts.ts deleted file mode 100644 index 55d07ef..0000000 --- a/src/consts.ts +++ /dev/null @@ -1,43 +0,0 @@ -export const SITE = { - name: 'Asger Weirsøe', - title: 'Asger Weirsøe', - description: 'Asger Weirsøe er datalog (cand.scient. fra Aalborg Universitet) og Technical Solution Architect hos Digisense. Peppol, NemHandel, e-faktura, selvhosting, Proxmox og AI.', - url: 'https://asger.xn--weirse-fya.dk', - lang: 'da', - email: 'asger@weirsoe.dk', - location: 'Nørresundby, Danmark', - giteaUser: 'agw', - giteaBase: 'https://gitea.weircon.dk', -}; - -export const NAV = [ - { href: '/', label: 'Forside' }, - { href: '/cv', label: 'CV' }, - { href: '/projekter', label: 'Projekter' }, - { href: '/blog', label: 'Blog' }, -]; - -export const NAV_EN = [ - { href: '/en', label: 'Home' }, - { href: '/en/cv', label: 'CV' }, - { href: '/en/projects', label: 'Projects' }, - { href: '/blog', label: 'Blog (Danish)' }, -]; - -export const SOCIALS = [ - { href: 'https://www.linkedin.com/in/ag-weirs%C3%B8e/', label: 'LinkedIn' }, - { href: 'https://mastodon.social/@asgerweirsoee', label: 'Mastodon' }, - { href: 'https://github.com/asger-weirsoee', label: 'GitHub' }, - { href: 'https://gitea.weircon.dk/agw', label: 'Gitea' }, - { href: 'mailto:asger@weirsoe.dk', label: 'Email' }, -]; - -// Styr hvad der vises på /projekter. -// hide: repo-navne der skjules helt. pinned: vises først (i den rækkefølge). -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 deleted file mode 100644 index 70ba381..0000000 --- a/src/content.config.ts +++ /dev/null @@ -1,41 +0,0 @@ -import { defineCollection, z } from 'astro:content'; -import { glob } from 'astro/loaders'; - -const blog = defineCollection({ - loader: glob({ pattern: '**/*.md', base: './src/content/blog' }), - schema: () => - z.object({ - title: z.string(), - description: z.string().default(''), - date: z.coerce.date(), - 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(), - }), -}); - -// Projektsider: én markdown-fil pr. værktøj jeg udgiver. Dansk i roden -// (/projekter/), engelsk i en/ (/en/projects/) med samme filnavn. -// Frontmatter bærer de faste links, så siden og søgeindekset kan vise dem ens. -const projekter = defineCollection({ - loader: glob({ pattern: '**/*.md', base: './src/content/projekter' }), - schema: () => - z.object({ - title: z.string(), - description: z.string(), - repo: z.string(), // repo-navn på Gitea under SITE.giteaUser - sprog: z.string(), // fx Rust, Python - licens: z.string(), - platforme: z.array(z.string()).default([]), - aur: z.array(z.string()).default([]), // AUR-pakkenavne - tags: z.array(z.string()).default([]), - date: z.coerce.date(), // første udgivelse - updated: z.coerce.date().optional(), - draft: z.boolean().default(false), - lang: z.enum(['da', 'en']).default('da'), - }), -}); - -export const collections = { blog, projekter }; diff --git a/src/data/profiles.ts b/src/data/profiles.ts deleted file mode 100644 index 59470e9..0000000 --- a/src/data/profiles.ts +++ /dev/null @@ -1,546 +0,0 @@ -// E-dokumentprofiler (Nemhandel OIOUBL + Peppol BIS/PINT) til /profiler-opslaget. -// Data vedligeholdes her; siden bygger søge-/filter-UI'et af det. -// -// De danske profiler er ordnet som i Nemhandelsregistrets profil-dropdown, og `kategori` -// er registrets egen gruppeoverskrift ("Profiler for: …"). Dokumentlisterne følger -// OIOUBL Profiler-guiden (G26) v1.3, tabel 1-3. -// * = ApplicationResponse bruges her kun til afvisning; det forretningsmæssige svar på en -// ordre er altid OrderResponse/OrderResponseSimple. -export interface EProfile { - standard: string; - /** Nemhandelsregistrets gruppeoverskrift ("Profiler for: …"). Kun danske profiler. */ - kategori?: string; - navn: string; - id: string; - customization: string; - dokumenter: string[]; - anvendelse: string; - bemaerkninger: string; -} - -const NH = 'Nemhandel OIOUBL (UBL 2.1)'; - -export const profiles: EProfile[] = [ - // ── Profiler for: Fakturering ────────────────────────────────────────────── - { - standard: NH, - kategori: 'Fakturering', - navn: 'Procurement-BilSim-1.0 (simpel fakturering)', - id: 'Procurement-BilSim-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse (kun ved afvisning)'], - anvendelse: 'Ren fakturering: leverandør sender faktura/kreditnota/rykker til kunde. Ingen ordreproces, og kunden er ikke forpligtet til at kvittere.', - bemaerkninger: 'Obligatorisk profil og den mest udbredte rene faktura-pakke i Nemhandel. Kunden må afvise med ApplicationResponse, men skal ikke kvittere positivt.', - }, - { - standard: NH, - kategori: 'Fakturering', - navn: 'urn:www.nesubl.eu:profiles:profile5:ver2.0 (NES5, Billing Basic)', - id: 'urn:www.nesubl.eu:profiles:profile5:ver2.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Invoice', 'CreditNote'], - anvendelse: 'Internationalt NES-fakturaprofil: kunde modtager faktura og kreditnota fra leverandør.', - bemaerkninger: 'Obligatorisk profil. Den profil der ligger tættest på den gamle OIOXML-faktura, kun afløftede felter er tilladt. Bruges når fakturaafsender teknisk set ikke kan modtage en ApplicationResponse.', - }, - - // ── Profiler for: Ordre til fakturering ──────────────────────────────────── - { - standard: NH, - kategori: 'Ordre til fakturering', - navn: 'Procurement-OrdSim-BilSim-1.0 (simpel ordre + simpel fakturering)', - id: 'Procurement-OrdSim-BilSim-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order', 'OrderResponseSimple', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'], - anvendelse: 'Komplet simpel handel: simpel ordre efterfulgt af simpel fakturering. Dækker bestilling til betaling.', - bemaerkninger: 'Samlepakken de fleste vælger, når de vil kunne både bestille og fakturere. Ingen ordreændring/-annullering, det kræver en OrdAdv-variant.', - }, - - // ── Profiler for: Simple Ordrer ──────────────────────────────────────────── - { - standard: NH, - kategori: 'Simple Ordrer', - navn: 'Procurement-OrdRes-1.0 (kun ordresvar)', - id: 'Procurement-OrdRes-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['OrderResponse'], - anvendelse: 'Enkeltdokument-profil til alene at kunne udveksle en ordrebekræftelse (OrderResponse) uden at tilmelde sig en fuld ordreproces.', - bemaerkninger: 'Registerprofil af typen "single", findes i Nemhandelsregistret, men er ikke beskrevet i OIOUBL Profiler-guiden (G26) v1.3. Verificér mod registret før brug.', - }, - { - standard: NH, - kategori: 'Simple Ordrer', - navn: 'Procurement-OrdSim-1.0 (simpel ordre)', - id: 'Procurement-OrdSim-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order', 'OrderResponseSimple'], - anvendelse: 'Ren ordreproces: kunde sender ordre, leverandør svarer med simpelt ordresvar. Ingen fakturering i profilen.', - bemaerkninger: 'Ren ordre-only proces; kombineres typisk med en faktureringsprofil.', - }, - { - standard: NH, - kategori: 'Simple Ordrer', - navn: 'Procurement-OrdSimR-1.0 (simpel ordre m. krævet svar)', - id: 'Procurement-OrdSimR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order', 'OrderResponseSimple'], - anvendelse: 'Som OrdSim, men leverandør skal altid sende ordresvar.', - bemaerkninger: "R = 'Response required': samme dokumenter som OrdSim, men svaret er obligatorisk.", - }, - { - standard: NH, - kategori: 'Simple Ordrer', - navn: 'urn:www.nesubl.eu:profiles:profile3:ver2.0 (NES3, "Order Only")', - id: 'urn:www.nesubl.eu:profiles:profile3:ver2.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order'], - anvendelse: 'Leverandør modtager ordrer fra kunder. Intet ordresvar og ingen fakturering i profilen.', - bemaerkninger: 'Ældre NES-ordreprofil; OrdSim-varianterne anbefales i stedet.', - }, - - // ── Profiler for: Katalog ────────────────────────────────────────────────── - { - standard: NH, - kategori: 'Katalog', - navn: 'Catalogue-CatBas-1.0 (basalt katalog)', - id: 'Catalogue-CatBas-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Catalogue', 'CatalogueDeletion', 'ApplicationResponse'], - anvendelse: 'Leverandør sender et produktkatalog til kunden og kan slette det igen. Kunden kan afvise med ApplicationResponse.', - bemaerkninger: 'Simpleste katalogprofil, ingen prisopdatering, ingen katalogforespørgsel.', - }, - { - standard: NH, - kategori: 'Katalog', - navn: 'Catalogue-CatPriUpd-1.0 (kun prisopdatering)', - id: 'Catalogue-CatPriUpd-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['CataloguePricingUpdate', 'ApplicationResponse'], - anvendelse: 'Leverandør opdaterer alene priserne i et allerede udvekslet katalog.', - bemaerkninger: '"Catalogue Pricing Update Only" forudsætter at kataloget er sendt via en anden katalogprofil.', - }, - { - standard: NH, - kategori: 'Katalog', - navn: 'Catalogue-CatPriUpdR-1.0 (kun prisopdatering m. krævet svar)', - id: 'Catalogue-CatPriUpdR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['CataloguePricingUpdate', 'ApplicationResponse'], - anvendelse: 'Som CatPriUpd, men kunden skal kvittere for prisopdateringen.', - bemaerkninger: 'R = svar påkrævet.', - }, - { - standard: NH, - kategori: 'Katalog', - navn: 'Catalogue-CatSim-1.0 (katalogforespørgsel + katalog)', - id: 'Catalogue-CatSim-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['CatalogueRequest', 'Catalogue', 'CatalogueDeletion', 'ApplicationResponse'], - anvendelse: 'Kunden beder om et katalog (CatalogueRequest), leverandøren sender det og kan slette det igen.', - bemaerkninger: 'CatBas udvidet med katalogforespørgsel fra kunden.', - }, - - // ── Profiler for: Udvidet fakturering ────────────────────────────────────── - { - standard: NH, - kategori: 'Udvidet fakturering', - navn: 'Procurement-BilSimR-1.0 (simpel fakturering m. krævet svar)', - id: 'Procurement-BilSimR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse'], - anvendelse: 'Som BilSim, men kunden skal altid kvittere med et ApplicationResponse (accept/afvisning).', - bemaerkninger: "R = 'Response required': kunden skal kvittere. Bemærk: tilmelding til BilSimR giver ikke automatisk BilSim; begge skal registreres hver for sig.", - }, - { - standard: NH, - kategori: 'Udvidet fakturering', - navn: 'Procurement-BilSimReminderOnly-1.0 (kun rykker)', - id: 'Procurement-BilSimReminderOnly-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Reminder'], - anvendelse: 'Enkeltdokument-profil til alene at kunne modtage rykkere.', - bemaerkninger: 'Registerprofil af typen "single", ikke i OIOUBL Profiler-guiden (G26) v1.3. Praktisk fordi Reminder ikke findes i Peppol BIS Billing og derfor kun kan sendes i OIOUBL-profilerne.', - }, - { - standard: NH, - kategori: 'Udvidet fakturering', - navn: 'Procurement-PayBas-1.0 (kontoudtog)', - id: 'Procurement-PayBas-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Statement', 'ApplicationResponse'], - anvendelse: 'Leverandør sender kontoudtog (Statement) til kunden, en samlet oversigt over udestående poster, ikke en faktura.', - bemaerkninger: 'G26 kalder profilen direkte "Statement". Trods "Pay"-navnet er der ingen betalingsdokumenter i profilen.', - }, - { - standard: NH, - kategori: 'Udvidet fakturering', - navn: 'Procurement-PayBasR-1.0 (kontoudtog m. krævet svar)', - id: 'Procurement-PayBasR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Statement', 'ApplicationResponse'], - anvendelse: 'Som PayBas, men kunden skal kvittere for kontoudtoget.', - bemaerkninger: 'R = svar påkrævet.', - }, - { - standard: NH, - kategori: 'Udvidet fakturering', - navn: 'urn:www.nesubl.eu:profiles:profile8:ver2.0 (NES8)', - id: 'urn:www.nesubl.eu:profiles:profile8:ver2.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Invoice', 'CreditNote', 'ApplicationResponse'], - anvendelse: 'NES-faktureringsprofil med kvittering: faktura og kreditnota, hvor kunden kan svare med ApplicationResponse.', - bemaerkninger: 'NES5 udvidet med ApplicationResponse. Ingen Reminder.', - }, - - // ── Profiler for: Udvidet katalog ────────────────────────────────────────── - { - standard: NH, - kategori: 'Udvidet katalog', - navn: 'Catalogue-CatAdv-1.0 (avanceret katalog)', - id: 'Catalogue-CatAdv-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['CatalogueRequest', 'Catalogue', 'CatalogueItemSpecificationUpdate', 'CataloguePricingUpdate', 'CatalogueDeletion', 'ApplicationResponse'], - anvendelse: 'Fuld katalogproces: forespørgsel, katalog, opdatering af både varebeskrivelser og priser samt sletning.', - bemaerkninger: 'Den mest komplette katalogprofil: CatExt plus katalogforespørgsel.', - }, - { - standard: NH, - kategori: 'Udvidet katalog', - navn: 'Catalogue-CatAdvR-1.0 (avanceret katalog m. krævet svar)', - id: 'Catalogue-CatAdvR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['CatalogueRequest', 'Catalogue', 'CatalogueItemSpecificationUpdate', 'CataloguePricingUpdate', 'CatalogueDeletion', 'ApplicationResponse'], - anvendelse: 'Som CatAdv, men modtageren skal kvittere for katalogdokumenterne.', - bemaerkninger: 'R = svar påkrævet.', - }, - { - standard: NH, - kategori: 'Udvidet katalog', - navn: 'Catalogue-CatBasR-1.0 (basalt katalog m. krævet svar)', - id: 'Catalogue-CatBasR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Catalogue', 'CatalogueDeletion', 'ApplicationResponse'], - anvendelse: 'Som CatBas, men kunden skal kvittere for kataloget.', - bemaerkninger: 'R = svar påkrævet.', - }, - { - standard: NH, - kategori: 'Udvidet katalog', - navn: 'Catalogue-CatExt-1.0 (udvidet katalog + opdatering)', - id: 'Catalogue-CatExt-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Catalogue', 'CatalogueItemSpecificationUpdate', 'CataloguePricingUpdate', 'CatalogueDeletion', 'ApplicationResponse'], - anvendelse: 'Katalogudveksling med efterfølgende opdatering af varebeskrivelser og priser samt sletning. Ingen katalogforespørgsel.', - bemaerkninger: 'CatBas udvidet med opdateringsdokumenter.', - }, - { - standard: NH, - kategori: 'Udvidet katalog', - navn: 'Catalogue-CatExtR-1.0 (udvidet katalog + opdatering m. krævet svar)', - id: 'Catalogue-CatExtR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Catalogue', 'CatalogueItemSpecificationUpdate', 'CataloguePricingUpdate', 'CatalogueDeletion', 'ApplicationResponse'], - anvendelse: 'Som CatExt, men kunden skal kvittere.', - bemaerkninger: 'R = svar påkrævet.', - }, - { - standard: NH, - kategori: 'Udvidet katalog', - navn: 'Catalogue-CatSimR-1.0 (katalogforespørgsel + katalog m. krævet svar)', - id: 'Catalogue-CatSimR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['CatalogueRequest', 'Catalogue', 'CatalogueDeletion', 'ApplicationResponse'], - anvendelse: 'Som CatSim, men katalogsvar er påkrævet.', - bemaerkninger: 'R = svar påkrævet.', - }, - { - standard: NH, - kategori: 'Udvidet katalog', - navn: 'urn:www.nesubl.eu:profiles:profile1:ver2.0 (NES1, "Catalogue Only")', - id: 'urn:www.nesubl.eu:profiles:profile1:ver2.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Catalogue', 'ApplicationResponse'], - anvendelse: 'Ren katalogudsendelse: leverandør sender katalog, kunden kan afvise.', - bemaerkninger: 'Ældre NES-katalogprofil; Catalogue-CatBas-1.0 anbefales i stedet.', - }, - { - standard: NH, - kategori: 'Udvidet katalog', - navn: 'urn:www.nesubl.eu:profiles:profile2:ver2.0 (NES2, simpel katalogproces)', - id: 'urn:www.nesubl.eu:profiles:profile2:ver2.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Catalogue', 'CatalogueItemSpecificationUpdate', 'CataloguePricingUpdate'], - anvendelse: 'Katalog med efterfølgende opdatering af varebeskrivelser og priser. Ingen kvittering i profilen.', - bemaerkninger: 'Ældre NES-katalogprofil; Catalogue-CatExt-1.0 anbefales i stedet.', - }, - - // ── Profiler for: Udvidet ordre til fakturering ──────────────────────────── - { - standard: NH, - kategori: 'Udvidet ordre til fakturering', - navn: 'Procurement-OrdAdv-BilSim-1.0 (avanceret ordre + simpel fakturering)', - id: 'Procurement-OrdAdv-BilSim-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order', 'OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'], - anvendelse: 'Avanceret ordreproces (fuldt ordresvar samt ordreændring og -annullering) efterfulgt af simpel fakturering.', - bemaerkninger: 'Understøtter begge parter både OrdSim-BilSim og OrdAdv-BilSim, skal OrdAdv-BilSim bruges.', - }, - { - standard: NH, - kategori: 'Udvidet ordre til fakturering', - navn: 'Procurement-OrdAdv-BilSimR-1.0 (avanceret ordre + fakturering m. krævet fakturasvar)', - id: 'Procurement-OrdAdv-BilSimR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order', 'OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'], - anvendelse: 'Som OrdAdv-BilSim, men fakturasvar (ApplicationResponse) er påkrævet.', - bemaerkninger: 'R sidder på fakturadelen, ordresvaret er stadig valgfrit.', - }, - { - standard: NH, - kategori: 'Udvidet ordre til fakturering', - navn: 'Procurement-OrdAdvR-BilSim-1.0 (avanceret ordre m. krævet ordresvar + simpel fakturering)', - id: 'Procurement-OrdAdvR-BilSim-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order', 'OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'], - anvendelse: 'Som OrdAdv-BilSim, men ordresvar er påkrævet.', - bemaerkninger: 'R sidder på ordredelen, fakturasvaret er valgfrit.', - }, - { - standard: NH, - kategori: 'Udvidet ordre til fakturering', - navn: 'Procurement-OrdAdvR-BilSimR-1.0 (avanceret ordre + fakturering, begge svar krævet)', - id: 'Procurement-OrdAdvR-BilSimR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order', 'OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'], - anvendelse: 'Avanceret ordre og fakturering hvor både ordresvar og fakturasvar er påkrævet.', - bemaerkninger: 'Strammeste variant af ordre-til-faktura-kæden.', - }, - { - standard: NH, - kategori: 'Udvidet ordre til fakturering', - navn: 'Procurement-OrdSel-BilSim-1.0 (sælgerinitieret ordre + simpel fakturering)', - id: 'Procurement-OrdSel-BilSim-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'], - anvendelse: 'Sælgerinitieret handel: leverandøren starter med en ordrebekræftelse (fx efter aftale i marken eller telefonisk), kunden kan ændre eller annullere, og der faktureres bagefter.', - bemaerkninger: 'Bemærk: ingen Order i profilen, "Sel" = seller initiated, så processen starter med OrderResponse.', - }, - { - standard: NH, - kategori: 'Udvidet ordre til fakturering', - navn: 'Procurement-OrdSel-BilSimR-1.0 (sælgerinitieret ordre + fakturering m. krævet svar)', - id: 'Procurement-OrdSel-BilSimR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['OrderResponse', 'OrderResponseSimple', 'OrderChange', 'OrderCancellation', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'], - anvendelse: 'Som OrdSel-BilSim, men fakturasvar er påkrævet.', - bemaerkninger: 'Ingen Order i profilen, processen starter hos sælger.', - }, - { - standard: NH, - kategori: 'Udvidet ordre til fakturering', - navn: 'Procurement-OrdSim-BilSimR-1.0 (simpel ordre + fakturering m. krævet fakturasvar)', - id: 'Procurement-OrdSim-BilSimR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order', 'OrderResponseSimple', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'], - anvendelse: 'Simpel ordre + fakturering hvor fakturasvar (ApplicationResponse) er påkrævet.', - bemaerkninger: 'R sidder på fakturadelen.', - }, - { - standard: NH, - kategori: 'Udvidet ordre til fakturering', - navn: 'Procurement-OrdSimR-BilSim-1.0 (simpel ordre m. krævet ordresvar + fakturering)', - id: 'Procurement-OrdSimR-BilSim-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order', 'OrderResponseSimple', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'], - anvendelse: 'Som OrdSim-BilSim, men ordresvar er påkrævet.', - bemaerkninger: 'R sidder på ordredelen.', - }, - { - standard: NH, - kategori: 'Udvidet ordre til fakturering', - navn: 'Procurement-OrdSimR-BilSimR-1.0 (simpel ordre + fakturering, begge svar krævet)', - id: 'Procurement-OrdSimR-BilSimR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order', 'OrderResponseSimple', 'Invoice', 'CreditNote', 'Reminder', 'ApplicationResponse *'], - anvendelse: 'Simpel ordre og fakturering hvor både ordresvar og fakturasvar er påkrævet.', - bemaerkninger: 'Begge R’er sat, modparten skal kvittere i begge led.', - }, - { - standard: NH, - kategori: 'Udvidet ordre til fakturering', - navn: 'urn:www.nesubl.eu:profiles:profile7:ver2.0 (NES7, ordre til faktura)', - id: 'urn:www.nesubl.eu:profiles:profile7:ver2.0', - customization: 'OIOUBL-2.x', - dokumenter: ['Order', 'OrderResponseSimple', 'Invoice', 'CreditNote', 'ApplicationResponse *'], - anvendelse: 'NES-profil for simpel ordre efterfulgt af simpel fakturering.', - bemaerkninger: 'Ældre NES-pendant til OrdSim-BilSim. Ingen Reminder i profilen.', - }, - - // ── Profiler for: ForsyningsSpecifikation ────────────────────────────────── - { - standard: NH, - kategori: 'ForsyningsSpecifikation', - navn: 'Reference-Utility-1.0 (forsyningsspecifikation, UTS)', - id: 'Reference-Utility-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['UtilityStatement', 'ApplicationResponse'], - anvendelse: 'Forsyningsbranchen (el/vand/varme/gas): leverandør sender detaljeret forbrugs-/forsyningsspecifikation der ledsager fakturaen; kunden kan svare med ApplicationResponse.', - bemaerkninger: 'Bruges som tillæg til en faktureringsprofil, UTS erstatter ikke fakturaen.', - }, - { - standard: NH, - kategori: 'ForsyningsSpecifikation', - navn: 'Reference-UtilityR-1.0 (forsyningsspecifikation m. krævet svar)', - id: 'Reference-UtilityR-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['UtilityStatement', 'ApplicationResponse'], - anvendelse: 'Som Reference-Utility, men kunden skal kvittere for forsyningsspecifikationen.', - bemaerkninger: 'R = svar påkrævet.', - }, - - // ── Profiler for: Diverse ────────────────────────────────────────────────── - { - standard: NH, - kategori: 'Diverse', - navn: 'Procurement-TecRes-1.0 (kvittering på modtaget dokument)', - id: 'Procurement-TecRes-1.0', - customization: 'OIOUBL-2.x', - dokumenter: ['ApplicationResponse'], - anvendelse: 'Ren kvitteringsprofil: bruges til at svare på et modtaget dokument, typisk når afsendelsen ikke kan behandles pga. tekniske fejl eller en ikke-understøttet profil.', - bemaerkninger: 'Bruges som ProfileID på selve ApplicationResponse’en, ikke som en forretningsproces man handler i.', - }, - { - standard: NH, - navn: 'urn:www.cenbii.eu:profile:bii30:ver2.0 (forsendelsesadvis / despatch advice)', - id: 'urn:www.cenbii.eu:profile:bii30:ver2.0', - customization: 'OIOUBL-2.x', - dokumenter: ['DespatchAdvice'], - anvendelse: 'Leverandør varsler en forsendelse, så kunden kan forberede modtagelse og afstemme leveringen mod ordren.', - bemaerkninger: 'CEN BII-profil optaget i OIOUBL (G26 afsnit 3.3.15). Ikke placeret i en registerkategori her, bekræft gruppen i Nemhandelsregistrets dropdown før brug.', - }, - - // ── Peppol BIS 3.0 ───────────────────────────────────────────────────────── - { - standard: 'Peppol BIS 3.0', - navn: 'Peppol BIS Billing 3.0', - id: 'urn:fdc:peppol.eu:2017:poacc:billing:01:1.0', - customization: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0', - dokumenter: ['Invoice', 'CreditNote'], - anvendelse: 'Den centrale e-faktura/kreditnota-pakke i Peppol. CIUS af EN 16931. Bruges til EU-grænsehandel og DK/Peppol-fakturering.', - bemaerkninger: 'Langt den mest brugte BIS3-profil. Invoice Response er en separat profil. Bemærk: Reminder findes ikke i Peppol, brug OIOUBL-profilerne til rykkere.', - }, - { - standard: 'Peppol BIS 3.0', - navn: 'Peppol BIS Self-Billing 3.0', - id: 'urn:fdc:peppol.eu:2017:poacc:selfbilling:01:1.0', - customization: 'urn:cen.eu:en16931:2017#compliant#urn:fdc:peppol.eu:2017:poacc:billing:3.0#conformant#urn:fdc:peppol.eu:2017:poacc:selfbilling:international:3.0', - dokumenter: ['Invoice (self-billed)', 'CreditNote'], - anvendelse: 'Selvfakturering: køber udsteder fakturaen på vegne af sælger.', - bemaerkninger: 'Genbruger Billing-syntaks med self-billing customization.', - }, - { - standard: 'Peppol BIS 3.0', - navn: 'Peppol BIS Invoice Response 3.0', - id: 'urn:fdc:peppol.eu:poacc:bis:invoice_response:3', - customization: 'urn:fdc:peppol.eu:poacc:trns:invoice_response:3', - dokumenter: ['ApplicationResponse (Invoice Response)'], - anvendelse: 'Køber sender struktureret status/svar på en modtaget faktura (accepteret, afvist, under behandling, betalt).', - bemaerkninger: 'Bruges sammen med Billing til at lukke faktura-livscyklussen. Peppol-pendant til OIOUBL-ernes "R"-profiler.', - }, - { - standard: 'Peppol BIS 3.0', - navn: 'Peppol BIS Message Level Response 3.0 (MLR)', - id: 'urn:fdc:peppol.eu:poacc:bis:mlr:3', - customization: 'urn:fdc:peppol.eu:poacc:trns:mlr:3', - dokumenter: ['ApplicationResponse (Message Level Response)'], - anvendelse: 'Teknisk kvittering på beskedniveau: bekræfter modtagelse/validering af et hvilket som helst dokument.', - bemaerkninger: 'Transport-/syntaks-niveau svar, ikke forretningsmæssigt indhold. Peppol-pendant til Procurement-TecRes-1.0.', - }, - { - standard: 'Peppol BIS 3.0', - navn: 'Peppol BIS Ordering 3.0', - id: 'urn:fdc:peppol.eu:poacc:bis:ordering:3', - customization: 'urn:fdc:peppol.eu:poacc:trns:order:3 / urn:fdc:peppol.eu:poacc:trns:order_response:3', - dokumenter: ['Order', 'Order Response'], - anvendelse: 'Fuld ordreproces: køber sender ordre, sælger svarer med ordrebekræftelse/-svar.', - bemaerkninger: 'Order = Trdm01, Order Response = Trdm76. Peppol-pendant til Procurement-OrdSim-1.0.', - }, - { - standard: 'Peppol BIS 3.0', - navn: 'Peppol BIS Order Only 3.0', - id: 'urn:fdc:peppol.eu:poacc:bis:order_only:3', - customization: 'urn:fdc:peppol.eu:poacc:trns:order:3', - dokumenter: ['Order'], - anvendelse: 'Kun afsendelse af ordre uden forventet struktureret ordresvar.', - bemaerkninger: 'Enkel ordre-only proces. Peppol-pendant til NES3 ("Order Only").', - }, - { - standard: 'Peppol BIS 3.0', - navn: 'Peppol BIS Order Agreement 3.0 (Advanced Ordering)', - id: 'urn:fdc:peppol.eu:poacc:bis:order_agreement:3', - customization: 'urn:fdc:peppol.eu:poacc:trns:order_agreement:3', - dokumenter: ['Order', 'Order Response (Advanced)'], - anvendelse: 'Avanceret ordre der etablerer en bindende ordreaftale mellem parterne.', - bemaerkninger: 'Bruges hvor ordresvar danner juridisk aftale.', - }, - { - standard: 'Peppol BIS 3.0', - navn: 'Peppol BIS Catalogue 3.0', - id: 'urn:fdc:peppol.eu:poacc:bis:catalogue:3', - customization: 'urn:fdc:peppol.eu:poacc:trns:catalogue:3', - dokumenter: ['Catalogue', 'ApplicationResponse (Catalogue Response)'], - anvendelse: 'Sælger sender produktkatalog (priser/varer) til køber, der kan svare med accept/afvisning.', - bemaerkninger: 'Grundlag for efterfølgende ordrer. Peppol-pendant til Catalogue-CatBasR-1.0.', - }, - { - standard: 'Peppol BIS 3.0', - navn: 'Peppol BIS Catalogue without Response 3.0', - id: 'urn:fdc:peppol.eu:poacc:bis:catalogue_wo_response:3', - customization: 'urn:fdc:peppol.eu:poacc:trns:catalogue:3', - dokumenter: ['Catalogue'], - anvendelse: 'Katalogudsendelse uden forventet katalogsvar.', - bemaerkninger: 'Variant uden Catalogue Response.', - }, - { - standard: 'Peppol BIS 3.0', - navn: 'Peppol BIS Punch Out 3.0', - id: 'urn:fdc:peppol.eu:poacc:bis:punch_out:3', - customization: 'urn:fdc:peppol.eu:poacc:trns:punch_out:3', - dokumenter: ['Catalogue (Punch Out)'], - anvendelse: "Punch-out katalog: køber 'hopper ud' til sælgers webshop og henter en kurv tilbage.", - bemaerkninger: 'Bruges i e-handels-/webshop-integrationer.', - }, - { - standard: 'Peppol BIS 3.0', - navn: 'Peppol BIS Despatch Advice 3.0', - id: 'urn:fdc:peppol.eu:poacc:bis:despatch_advice:3', - customization: 'urn:fdc:peppol.eu:poacc:trns:despatch_advice:3', - dokumenter: ['Despatch Advice (følgeseddel)'], - anvendelse: 'Sælger sender forsendelses-/leveringsmeddelelse (hvad er afsendt, hvornår, hvordan) inden faktura.', - bemaerkninger: 'Logistik-dokument mellem ordre og faktura. Peppol-pendant til urn:www.cenbii.eu:profile:bii30:ver2.0 i OIOUBL.', - }, - - // ── Peppol BIS 4.0 / PINT ────────────────────────────────────────────────── - { - standard: 'Peppol BIS 4.0 / PINT', - navn: 'EU PINT Billing (v1.0.1; forløber for BIS Billing 4.0)', - id: 'urn:peppol:pint:billing-1@eu-1 (PINT EU Billing)', - customization: 'urn:peppol:pint:billing-1@eu-1', - dokumenter: ['Invoice', 'CreditNote'], - anvendelse: 'Peppols internationale fakturamodel (PINT) tilpasset EU. Implementerer BIS Billing 3.0 som PINT-kompatibel uden ændringer; baner vej for grænseoverskridende fakturering mod lande med egne PINT-specs (AU/JP/MY/NZ/SG m.fl.).', - bemaerkninger: 'v1.0.0 udgivet okt. 2025, nu v1.0.1. CIUS af EN 16931. NB: identifikator-format afviger fra BIS3, verificér mod docs.peppol.eu/poac/eu/pint-eu inden brug.', - }, - { - standard: 'Peppol BIS 4.0 / PINT', - navn: 'Peppol BIS Billing 4.0 (kommende)', - id: '(endnu ikke endeligt fastlagt)', - customization: '(endnu ikke endeligt fastlagt)', - dokumenter: ['Invoice', 'CreditNote (forventet)'], - anvendelse: 'Næste generation der lægger BIS og PINT sammen til ét globalt faktura-format. Forventet 2026, endnu ikke udgivet (pr. juni 2026).', - bemaerkninger: "STATUS: under udarbejdelse pr. juni 2026, ID'er og indhold ikke endeligt publiceret. Brug BIS Billing 3.0 / EU PINT i mellemtiden.", - }, -]; - -export const standarder = [...new Set(profiles.map((p) => p.standard))]; - -/** Nemhandelsregistrets kategorier i registrets egen rækkefølge. */ -export const kategorier = [...new Set(profiles.map((p) => p.kategori).filter((k): k is string => Boolean(k)))]; diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro deleted file mode 100644 index aa96d6d..0000000 --- a/src/layouts/Base.astro +++ /dev/null @@ -1,94 +0,0 @@ ---- -import { ClientRouter } from 'astro:transitions'; -import Head from '../components/Head.astro'; -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, PREVIEW } from '../consts'; -import '../styles/global.css'; -interface Props { - title?: string; - description?: string; - image?: string; - type?: 'website' | 'article'; - publishedTime?: string; - modifiedTime?: string; - tags?: string[]; - jsonLd?: unknown[]; - noindex?: boolean; - lang?: 'da' | 'en'; - alternates?: { lang: 'da' | 'en'; path: string }[]; -} -const { title, description, image, type, publishedTime, modifiedTime, tags, jsonLd, noindex, lang, alternates } = Astro.props; ---- - - - - {/* Skal stå i , ellers dropper Astro og . */} - `} /> - - - - {!PREVIEW && } - - - {PREVIEW && ( -
- UDKAST, ikke udgivet. Tilbage til godkendelse -
- )} -
-
- -
-