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
156 lines
5.6 KiB
TypeScript
156 lines
5.6 KiB
TypeScript
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 },
|
||
};
|
||
}
|