seo: BreadcrumbList + CollectionPage + wordCount, web-manifest/ikoner, FAQ i foerste person
Build & Deploy / deploy (push) Successful in 1m20s
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:
co-authored by
Claude Opus 4.8
parent
3ed486b775
commit
d8bfff1cc4
@@ -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) -->
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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 },
|
||||
};
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user