feat: refine portfolio content, contact, and supporting media
This commit is contained in:
@@ -98,6 +98,7 @@ const readManifest = async (repositoryRoot) => {
|
||||
|
||||
const assetReferences = (content) => {
|
||||
const references = [];
|
||||
if (content.contact?.avatarAssetId !== undefined) references.push(content.contact.avatarAssetId);
|
||||
const projects = content.projects;
|
||||
if (typeof projects !== 'object' || projects === null || Array.isArray(projects)) {
|
||||
throw new Error('Portfolio content must contain project collections.');
|
||||
|
||||
@@ -9,12 +9,11 @@ const publicMetadata = Object.freeze({
|
||||
name: 'Luca Pîslaru',
|
||||
role: 'System Administrator & Full-Stack Developer',
|
||||
title: 'Luca Pîslaru — System Administrator & Full-Stack Developer',
|
||||
description: 'Portfolio of Luca Pîslaru, a Bucharest-based System Administrator and Full-Stack Developer working across Linux infrastructure and end-to-end software delivery.',
|
||||
email: 'mailto:luca.pislaru@gmail.com',
|
||||
description: 'I’m Luca Pîslaru, a System Administrator & Full-Stack Developer based in Bucharest. Explore my work in Linux infrastructure and end-to-end application development.',
|
||||
sameAs: Object.freeze([
|
||||
'https://linkedin.com/in/luca-pisl',
|
||||
'https://github.com/LucaPisl',
|
||||
'https://gitlab.com/luca_pisl',
|
||||
'https://linkedin.com/in/luca-pisl',
|
||||
]),
|
||||
location: Object.freeze({ city: 'Bucharest', countryCode: 'RO' }),
|
||||
socialImagePath: '/assets/project-social-preview.webp',
|
||||
@@ -121,13 +120,13 @@ function verifyMetadata(html, canonicalUrl) {
|
||||
try { jsonLd = JSON.parse(jsonLdText); } catch { throw new Error('JSON-LD is not valid JSON.'); }
|
||||
exactObjectKeys(jsonLd, ['@context', '@type', 'name', 'description', 'url', 'mainEntity'], 'ProfilePage JSON-LD');
|
||||
const person = jsonLd.mainEntity;
|
||||
exactObjectKeys(person, ['@type', 'name', 'jobTitle', 'description', 'url', 'email', 'homeLocation', 'sameAs'], 'Person JSON-LD');
|
||||
exactObjectKeys(person, ['@type', 'name', 'jobTitle', 'description', 'url', 'homeLocation', 'sameAs'], 'Person JSON-LD');
|
||||
exactObjectKeys(person.homeLocation, ['@type', 'address'], 'homeLocation JSON-LD');
|
||||
exactObjectKeys(person.homeLocation.address, ['@type', 'addressLocality', 'addressCountry'], 'PostalAddress JSON-LD');
|
||||
if (jsonLd['@context'] !== 'https://schema.org' || jsonLd['@type'] !== 'ProfilePage' ||
|
||||
jsonLd.name !== publicMetadata.name || jsonLd.description !== description || jsonLd.url !== canonicalUrl ||
|
||||
person['@type'] !== 'Person' || person.name !== publicMetadata.name || person.jobTitle !== publicMetadata.role ||
|
||||
person.description !== description || person.url !== canonicalUrl || person.email !== publicMetadata.email ||
|
||||
person.description !== description || person.url !== canonicalUrl ||
|
||||
person.homeLocation['@type'] !== 'Place' || person.homeLocation.address['@type'] !== 'PostalAddress' ||
|
||||
person.homeLocation.address.addressLocality !== publicMetadata.location.city ||
|
||||
person.homeLocation.address.addressCountry !== publicMetadata.location.countryCode ||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { dirname, relative, resolve, sep } from 'node:path';
|
||||
import { fileURLToPath, pathToFileURL } from 'node:url';
|
||||
import { loadProductionInputs } from '../src/content/load-production-inputs.ts';
|
||||
import { createPageMetadata, createStaticHostArtifacts } from '../src/content/metadata.ts';
|
||||
import { contactAddress, readableContactAddress } from '../src/content/contact-email.ts';
|
||||
import { Window } from 'happy-dom';
|
||||
import { parseArtifactContract, parseBuildAuthority } from './build-contract.mjs';
|
||||
import { parseExactConfigArgument } from './config-argument.mjs';
|
||||
@@ -232,19 +233,31 @@ const assertSemanticHtml = (html, inputs, contract) => {
|
||||
...inputs.content.infrastructure.bands.flatMap((band) => [band.title, band.summary]),
|
||||
...inputs.content.projects.selected.flatMap((project) => [project.title, project.summary]),
|
||||
...inputs.content.toolkit.flatMap((group) => [group.title, ...group.technologies.map((technology) => technology.name)]),
|
||||
inputs.content.contact.email,
|
||||
];
|
||||
for (const copy of requiredVisibleCopy) {
|
||||
if (!semanticText.includes(copy)) throw new Error(`Missing visible semantic evidence: ${copy}`);
|
||||
}
|
||||
const mailLinks = [...document.querySelectorAll('a[href^="mailto:"]')];
|
||||
const startMailLinks = [...document.querySelectorAll('#start a[href^="mailto:"]')];
|
||||
const contactMailLinks = [...document.querySelectorAll('#contact a[href^="mailto:"]')];
|
||||
if (mailLinks.length !== 2 || startMailLinks.length !== 1 || contactMailLinks.length !== 1 || mailLinks.some((link) =>
|
||||
link.getAttribute('href') !== `mailto:${inputs.content.contact.email}` || link.textContent !== 'Email Luca'
|
||||
)) {
|
||||
throw new Error('Mailto links must be exactly one Email Luca action beneath each hero and contact section.');
|
||||
const contactActions = [...document.querySelectorAll('#start a[href="#contact"]')];
|
||||
if (contactActions.length !== 1 || contactActions[0].textContent !== 'Contact Me' ||
|
||||
!isVisibleSemanticNode(contactActions[0], appRoot)) {
|
||||
throw new Error('The hero must contain one visible Contact Me link to the contact section.');
|
||||
}
|
||||
if (document.querySelector('a[href^="mailto:"]') || html.includes(contactAddress(inputs.content.contact.email))) {
|
||||
throw new Error('The initial document must not expose a complete email address or mailto link.');
|
||||
}
|
||||
const disclosures = [...document.querySelectorAll('#contact details#contact-email-details')];
|
||||
if (disclosures.length !== 1 || disclosures[0].hasAttribute('open') ||
|
||||
disclosures[0].querySelector(':scope > summary')?.textContent !== 'Email Me' ||
|
||||
!isVisibleSemanticNode(disclosures[0], appRoot)) {
|
||||
throw new Error('Contact must contain a closed native Email Me disclosure.');
|
||||
}
|
||||
disclosures[0].setAttribute('open', '');
|
||||
const readableEmail = disclosures[0].querySelector('.contact-email__address');
|
||||
if (readableEmail?.textContent !== readableContactAddress(inputs.content.contact.email) ||
|
||||
!isVisibleSemanticNode(readableEmail, appRoot)) {
|
||||
throw new Error('Email disclosure must provide a readable address without JavaScript.');
|
||||
}
|
||||
disclosures[0].removeAttribute('open');
|
||||
|
||||
const moduleScript = document.querySelector(`script[type="module"][src="/${contract.entryModule}"]`);
|
||||
const snapshot = document.querySelector('script#portfolio-inputs[type="application/json"]');
|
||||
@@ -353,7 +366,7 @@ const assertMetadata = (html, expected) => {
|
||||
} catch {
|
||||
throw new Error('JSON-LD is malformed.');
|
||||
}
|
||||
if (/"(?:streetAddress|postalCode|telephone|image|interactionStatistic)"\s*:/iu.test(jsonLdMatch[1])) {
|
||||
if (/"(?:streetAddress|postalCode|telephone|image|interactionStatistic|email)"\s*:/iu.test(jsonLdMatch[1])) {
|
||||
throw new Error('JSON-LD contains unsafe private, portrait, or interaction metadata.');
|
||||
}
|
||||
if (jsonLd['@type'] !== 'ProfilePage' || jsonLd.mainEntity?.['@type'] !== 'Person') {
|
||||
|
||||
Reference in New Issue
Block a user