65 lines
3.3 KiB
JavaScript
65 lines
3.3 KiB
JavaScript
function siteOrigin(value) {
|
|
if (!value) return null;
|
|
const url = new URL(value);
|
|
if (!['https:', 'http:'].includes(url.protocol) || url.username || url.password || url.pathname !== '/' || url.search || url.hash) {
|
|
throw new Error('SITE_URL must be an absolute HTTP(S) origin, e.g. https://resume.example.com');
|
|
}
|
|
return url.origin + '/';
|
|
}
|
|
|
|
function buildSeo(data, siteUrl) {
|
|
const canonical = siteOrigin(siteUrl);
|
|
const title = `${data.name} — ${data.header}`;
|
|
const description = data.about.replace(/\s+/g, ' ').trim().slice(0, 160);
|
|
const language = /^[a-z]{2,3}(?:-[a-zA-Z0-9]{2,8})*$/.test(data.language || '') ? data.language : 'en';
|
|
const image = canonical ? new URL(data.picture, canonical).href : null;
|
|
const skills = [...new Set(data.menus.flatMap((menu) => (menu.subentries || []).map((skill) => typeof skill === 'string' ? skill : skill.name || skill.title || skill.skill)).filter(Boolean))];
|
|
const person = {
|
|
'@type': 'Person',
|
|
...(canonical ? { '@id': canonical + '#person', url: canonical } : {}),
|
|
name: data.name,
|
|
jobTitle: data.header,
|
|
description: data.about,
|
|
...(image ? { image } : {}),
|
|
knowsAbout: skills
|
|
};
|
|
// Only explicitly identified profile links represent the same person.
|
|
const sameAs = data.links.filter((link) => link.sameAs === true && /^https?:\/\//.test(link.link)).map((link) => link.link);
|
|
if (sameAs.length) person.sameAs = sameAs;
|
|
const schema = {
|
|
'@context': 'https://schema.org',
|
|
'@type': 'ProfilePage',
|
|
...(canonical ? { '@id': canonical + '#profile', url: canonical } : {}),
|
|
name: title,
|
|
description,
|
|
inLanguage: language,
|
|
mainEntity: person
|
|
};
|
|
return { canonical, title, description, language, image, schema: JSON.stringify(schema).replace(/</g, '\\u003c') };
|
|
}
|
|
|
|
function escapeXml(text) {
|
|
return text.replace(/[<>&'\"]/g, (char) => ({ '<': '<', '>': '>', '&': '&', "'": ''', '"': '"' })[char]);
|
|
}
|
|
|
|
function buildSummary(data, canonical) {
|
|
const lines = [`# ${data.name}`, '', `> ${data.header}`, '', data.about, ''];
|
|
if (canonical) lines.push(`Profile: ${canonical}`, '');
|
|
lines.push('## Experience', '');
|
|
for (const work of data.workexperiences) {
|
|
lines.push(`### ${work.desc}`, `${work.title} · ${work.date}`, ...(work.highlights || []).map((point) => `- ${point}`), '');
|
|
}
|
|
if (data.projects?.length) {
|
|
lines.push('## Projects', '');
|
|
for (const project of data.projects) lines.push(`### ${project.name}`, project.summary || '', ...(project.stack?.length ? [`Stack: ${project.stack.join(', ')}`] : []), ...(project.link ? [`Project: ${project.link}`] : []), '');
|
|
}
|
|
lines.push('## Skills', '');
|
|
for (const menu of data.menus) lines.push(`### ${menu.title}`, (menu.subentries || []).map((s) => typeof s === 'string' ? s : s.name || s.title || s.skill).filter(Boolean).join(', '), '');
|
|
lines.push('## Education', '');
|
|
for (const education of data.educations) lines.push(`- ${education.desc1} · ${education.desc2} · ${education.date}`);
|
|
lines.push('', '## Interests', '');
|
|
for (const interest of data.interests) lines.push(`- ${interest.title}${interest.subentries?.length ? ': ' + interest.subentries.join(', ') : ''}`);
|
|
return lines.join('\n') + '\n';
|
|
}
|
|
|
|
module.exports = { buildSeo, siteOrigin, escapeXml, buildSummary };
|