Files
pw/src/seo.ts
T
root b816ceb55a content: radioamatør OZ0AGW flettet ind i profilen
Asger bestod radioamatørprøven lørdag den 18. maj 2024 og har B-licens med
kaldesignalet OZ0AGW. Det står nu de steder profilen faktisk læses fra — både
af mennesker og af maskiner — i stedet for som en løsrevet notits:

- /cv: ny "Licenser"-sektion (licens, kaldesignal, prøvedato) og en linje under
  Interesser, der binder radioen sammen med resten af profilen — protokoller og
  infrastruktur, bare uden kabler.
- Forsiden: hero-teksten nævner det som en del af fritiden, og der er en ny
  FAQ ("Er Asger Weirsøe radioamatør?") med dato og kaldesignal, så både
  FAQPage-schemaet og en LLM kan citere svaret ordret.
- seo.ts: Person får schema.org/callSign = OZ0AGW — en entydig identifikator på
  linje med sameAs-profilerne, så opslag på kaldesignalet peger på personen.
  hasCredential bliver et array (grad + licens med dateCreated 2024-05-18), og
  knowsAbout/description udvides tilsvarende.
- llms.txt og cv.txt (curl-versionen): faktalinje, ekspertise-punkt, FAQ-svar og
  kaldesignal i hovedet. Chat-proxyen på /chat henter netop de to filer ved
  opstart, så "Spørg mit CV" kan svare på det efter en genstart.

Claude-Session: https://claude.ai/code/session_0198n4owY35jQpwht1wCGzfh
2026-09-22 17:40:00 +02:00

156 lines
5.6 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' },
},
{
'@type': 'EducationalOccupationalCredential',
credentialCategory: 'license',
name: 'Radioamatørlicens, kategori B (kaldesignal OZ0AGW)',
dateCreated: '2024-05-18',
},
],
// schema.org/callSign — kaldesignalet er en entydig identifikator på linje med
// sameAs-profilerne og knytter OZ0AGW til personen.
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',
'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.',
};
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 },
};
}