diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png new file mode 100644 index 0000000..fe5e67c Binary files /dev/null and b/public/apple-touch-icon.png differ diff --git a/public/icon-192.png b/public/icon-192.png new file mode 100644 index 0000000..d24de1a Binary files /dev/null and b/public/icon-192.png differ diff --git a/public/icon-512.png b/public/icon-512.png new file mode 100644 index 0000000..ca0ecee Binary files /dev/null and b/public/icon-512.png differ diff --git a/public/site.webmanifest b/public/site.webmanifest new file mode 100644 index 0000000..f6603a6 --- /dev/null +++ b/public/site.webmanifest @@ -0,0 +1,16 @@ +{ + "name": "Asger Weirsøe", + "short_name": "Asger Weirsøe", + "description": "Datalog & Technical Solution Architect — Peppol, NemHandel, e-faktura, selvhosting, Proxmox og AI.", + "lang": "da", + "start_url": "/", + "scope": "/", + "display": "standalone", + "background_color": "#0b0f17", + "theme_color": "#0b0f17", + "icons": [ + { "src": "/icon-192.png", "sizes": "192x192", "type": "image/png", "purpose": "any" }, + { "src": "/icon-512.png", "sizes": "512x512", "type": "image/png", "purpose": "any" }, + { "src": "/favicon.svg", "sizes": "any", "type": "image/svg+xml", "purpose": "any" } + ] +} diff --git a/src/components/Head.astro b/src/components/Head.astro index 1edd286..46e4f5f 100644 --- a/src/components/Head.astro +++ b/src/components/Head.astro @@ -57,6 +57,8 @@ const graphs = [baseGraph, ...jsonLd]; + + diff --git a/src/pages/blog/[...slug].astro b/src/pages/blog/[...slug].astro index df1adc5..e666504 100644 --- a/src/pages/blog/[...slug].astro +++ b/src/pages/blog/[...slug].astro @@ -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 }, +]); --- - ← Tilbage til blog +
{d}
diff --git a/src/pages/blog/index.astro b/src/pages/blog/index.astro index 6f3644a..8d396c0 100644 --- a/src/pages/blog/index.astro +++ b/src/pages/blog/index.astro @@ -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 }]), +]; --- - +

Blog

Tags diff --git a/src/pages/index.astro b/src/pages/index.astro index a4829de..ff33296 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -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.', }, ]; diff --git a/src/seo.ts b/src/seo.ts index 26f5b27..fcb3cf5 100644 --- a/src/seo.ts +++ b/src/seo.ts @@ -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 }, + }; +}