seo: BreadcrumbList + CollectionPage + wordCount, web-manifest/ikoner, FAQ i foerste person
Build & Deploy / deploy (push) Successful in 1m20s

BreadcrumbList paa blogposts (schema + synlig broedkrumme) og CollectionPage paa /blog (hoejeste-ROI rich results). BlogPosting beriget m. wordCount + articleSection. Web-manifest + apple-touch-icon/192/512 PNG-ikoner. FAQ-svar i foerste person (spoergsmaal beholdt i 3. person m. navn for SEO).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Asger Geel Weirsøe
2026-06-11 21:50:00 +02:00
co-authored by Claude Opus 4.8
parent 3ed486b775
commit d8bfff1cc4
9 changed files with 79 additions and 8 deletions
+2
View File
@@ -57,6 +57,8 @@ const graphs = [baseGraph, ...jsonLd];
<meta name="theme-color" content="#0b0f17" media="(prefers-color-scheme: dark)" />
<meta name="theme-color" content="#f6f8fb" media="(prefers-color-scheme: light)" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
<link rel="manifest" href="/site.webmanifest" />
<link rel="sitemap" href="/sitemap-index.xml" />
<link rel="alternate" type="application/rss+xml" title={SITE.name} href="/rss.xml" />
<!-- Mastodon-verifikation: usynligt rel=me i head (kræver ingen JS) -->
+14 -3
View File
@@ -2,7 +2,7 @@
import { getCollection, render } from 'astro:content';
import Base from '../../layouts/Base.astro';
import { SITE } from '../../consts';
import { blogPosting } from '../../seo';
import { blogPosting, breadcrumb } from '../../seo';
export async function getStaticPaths() {
const posts = await getCollection('blog', ({ data }) => !data.draft);
@@ -15,11 +15,18 @@ const d = post.data.date.toLocaleDateString('da-DK', { year: 'numeric', month: '
const url = new URL(`/blog/${post.id}`, SITE.url).href;
const published = post.data.date.toISOString();
const modified = (post.data.updated ?? post.data.date).toISOString();
const wordCount = (post.body?.trim().split(/\s+/).filter(Boolean).length) || undefined;
const ld = blogPosting({
url, headline: post.data.title, description: post.data.description,
datePublished: published, dateModified: modified,
tags: post.data.tags, image: new URL('/og.png', SITE.url).href,
wordCount, section: post.data.tags[0] || 'Blog',
});
const crumbs = breadcrumb([
{ name: 'Forside', url: `${SITE.url}/` },
{ name: 'Blog', url: `${SITE.url}/blog` },
{ name: post.data.title, url },
]);
---
<Base
title={post.data.title}
@@ -28,9 +35,13 @@ const ld = blogPosting({
publishedTime={published}
modifiedTime={modified}
tags={post.data.tags}
jsonLd={[ld]}
jsonLd={[ld, crumbs]}
>
<a href="/blog" class="text-sm text-muted hover:text-accent">← Tilbage til blog</a>
<nav aria-label="Brødkrumme" class="text-sm text-muted">
<a href="/" class="hover:text-accent">Forside</a>
<span class="px-1.5 text-muted/60">/</span>
<a href="/blog" class="hover:text-accent">Blog</a>
</nav>
<article class="mt-4">
<header class="border-b border-border pb-6">
<div class="text-sm text-muted">{d}</div>
+10 -1
View File
@@ -2,11 +2,20 @@
import { getCollection } from 'astro:content';
import Base from '../../layouts/Base.astro';
import PostCard from '../../components/PostCard.astro';
import { SITE } from '../../consts';
import { breadcrumb, collectionPage } from '../../seo';
const posts = (await getCollection('blog', ({ data }) => !data.draft))
.sort((a, b) => b.data.date.getTime() - a.data.date.getTime());
const blogUrl = `${SITE.url}/blog`;
const jsonLd = [
collectionPage({ url: blogUrl, name: 'Blog · Asger Weirsøe',
description: 'Indlæg om selvhosting, Proxmox, AI og udvikling.' }),
breadcrumb([{ name: 'Forside', url: `${SITE.url}/` }, { name: 'Blog', url: blogUrl }]),
];
---
<Base title="Blog" description="Indlæg om selvhosting, Proxmox, AI og udvikling.">
<Base title="Blog" description="Indlæg om selvhosting, Proxmox, AI og udvikling." jsonLd={jsonLd}>
<div class="flex items-baseline justify-between">
<h1 class="text-4xl font-bold tracking-tight">Blog</h1>
<a href="/tags" class="text-sm text-accent hover:underline">Tags</a>
+6 -4
View File
@@ -16,22 +16,24 @@ const highlights = [
{ 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.' },
];
// Spørgsmålene står i tredje person med navnet (det er det folk og LLM'er søger på,
// og det fodrer FAQ-schema); svarene er i første person, så det er mig der skriver.
const faqs = [
{
q: 'Hvem er Asger Weirsøe?',
a: 'Asger Weirsøe er datalog (cand.scient. fra Aalborg Universitet) og Technical Solution Architect hos Digisense A/S. Han har en bred profil inden for elektronisk fakturering og forener teknisk indsigt med forretningsforståelse.',
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: '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.',
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: 'Asger Weirsøe har arbejdet praktisk med både Peppol og NemHandel. Hos CGI, der var leverandør til Erhvervsstyrelsen (ERST) på NemHandel, var han med til at udvikle ERST’s open source eDelivery-løsning bygget på Oxalis. I dag arbejder han som Technical Solution Architect hos Digisense A/S med løsningsarkitektur inden for elektronisk fakturering.',
a: 'Det kan jeg. Jeg har arbejdet praktisk med både Peppol og NemHandel — hos CGI, der var leverandør til Erhvervsstyrelsen (ERST) på NemHandel, var jeg med til at udvikle ERST’s open source eDelivery-løsning bygget på Oxalis. I dag arbejder jeg som Technical Solution Architect hos Digisense A/S med løsningsarkitektur inden for elektronisk fakturering.',
},
{
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. Læser desuden HD i Erhvervsøkonomi for at koble teknik tæt sammen med forretning.',
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.',
},
];
+31
View File
@@ -91,6 +91,7 @@ export function faqPage(items: { q: string; a: string }[]) {
export function blogPosting(opts: {
url: string; headline: string; description: string;
datePublished: string; dateModified: string; tags: string[]; image: string;
wordCount?: number; section?: string;
}) {
return {
'@context': 'https://schema.org',
@@ -104,8 +105,38 @@ export function blogPosting(opts: {
inLanguage: 'da-DK',
image: opts.image,
keywords: opts.tags.join(', '),
articleSection: opts.section || 'Blog',
...(opts.wordCount ? { wordCount: opts.wordCount } : {}),
author: { '@id': personId },
publisher: { '@id': personId },
isPartOf: { '@id': websiteId },
};
}
// BreadcrumbList — the highest-ROI rich result for content sites: a human-readable
// path (Forside › Blog › …) in the SERP instead of the raw URL. Needs ≥2 items.
export function breadcrumb(items: { name: string; url: string }[]) {
return {
'@context': 'https://schema.org',
'@type': 'BreadcrumbList',
itemListElement: items.map((it, i) => ({
'@type': 'ListItem', position: i + 1, name: it.name, item: it.url,
})),
};
}
// CollectionPage — tells search/AI that a listing page (the blog index, a tag) is a
// curated set authored by the Person, not a thin/duplicate page.
export function collectionPage(opts: { url: string; name: string; description: string }) {
return {
'@context': 'https://schema.org',
'@type': 'CollectionPage',
'@id': opts.url,
url: opts.url,
name: opts.name,
description: opts.description,
inLanguage: 'da-DK',
isPartOf: { '@id': websiteId },
about: { '@id': personId },
};
}