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>
143 lines
5.1 KiB
TypeScript
143 lines
5.1 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' },
|
||
},
|
||
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 },
|
||
};
|
||
}
|