Files
pw/src/seo.ts
T
Asger Geel WeirsøeandClaude Opus 4.8 d8bfff1cc4
Build & Deploy / deploy (push) Successful in 1m20s
seo: BreadcrumbList + CollectionPage + wordCount, web-manifest/ikoner, FAQ i foerste person
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>
2026-06-11 21:50:00 +02:00

143 lines
5.1 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { SITE } from './consts';
export const personId = `${SITE.url}/#person`;
export const websiteId = `${SITE.url}/#website`;
// Canonical Person entity — referenced by @id from every other entity.
export const person = {
'@type': 'Person',
'@id': personId,
name: 'Asger Geel Weirsøe',
alternateName: 'Asger Weirsøe',
url: `${SITE.url}/`,
image: `${SITE.url}/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' },
},
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',
'Proxmox', 'Selvhosting', 'Kunstig intelligens', 'Linux', 'DevOps',
'OIDC', 'SSO', 'Fuld-stack udvikling', 'Docker',
],
sameAs: [
'https://www.linkedin.com/in/ag-weirs%C3%B8e/',
'https://mastodon.social/@asgerweirsoee',
'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.',
};
export const website = {
'@type': 'WebSite',
'@id': websiteId,
url: `${SITE.url}/`,
name: SITE.name,
description: SITE.description,
inLanguage: 'da-DK',
publisher: { '@id': personId },
};
// Default graph present on every page (Google de-dupes by @id).
export const baseGraph = { '@context': 'https://schema.org', '@graph': [website, person] };
// Marks the front page as a ProfilePage about the Person — a strong "this site is
// about this human" signal that helps Google build a knowledge-panel entity.
export const profilePage = {
'@context': 'https://schema.org',
'@type': 'ProfilePage',
'@id': `${SITE.url}/#profilepage`,
url: `${SITE.url}/`,
name: 'Asger Weirsøe',
mainEntity: { '@id': personId },
about: { '@id': personId },
inLanguage: 'da-DK',
};
// FAQ pairs — natural-language Q&A that search engines and LLMs can lift verbatim.
export function faqPage(items: { q: string; a: string }[]) {
return {
'@context': 'https://schema.org',
'@type': 'FAQPage',
inLanguage: 'da-DK',
mainEntity: items.map((it) => ({
'@type': 'Question',
name: it.q,
acceptedAnswer: { '@type': 'Answer', text: it.a },
})),
};
}
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',
'@type': 'BlogPosting',
headline: opts.headline,
description: opts.description,
url: opts.url,
mainEntityOfPage: opts.url,
datePublished: opts.datePublished,
dateModified: opts.dateModified,
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 },
};
}