fix: validate social preview metadata

This commit is contained in:
2026-09-05 00:57:01 +03:00
parent eae03b2cd2
commit c958d4a05f
4 changed files with 254 additions and 14 deletions

View File

@@ -4,6 +4,21 @@ import { pathToFileURL } from 'node:url';
const sha256Pattern = /^[a-f0-9]{64}$/u;
const privateIdentifiers = /(?:simpastil|simpasteel|neon[ -]?debugger|yourhealth)/iu;
const sourceMapReference = /(?:sourceMappingURL\s*=|["'(]\/?[^"')\s]+\.map(?:[?"')\s]|$))/iu;
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',
sameAs: Object.freeze([
'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',
socialImageType: 'image/webp',
});
function rootHttpsUrl(value, label) {
let url;
@@ -65,19 +80,63 @@ function exactTagContent(html, expression, label) {
return decodedText(values[0]);
}
function exactObjectKeys(value, expectedKeys, label) {
if (typeof value !== 'object' || value === null || Array.isArray(value) ||
JSON.stringify(Object.keys(value).sort()) !== JSON.stringify([...expectedKeys].sort())) {
throw new Error(`${label} has missing or forbidden fields.`);
}
}
function verifyMetadata(html, canonicalUrl) {
const canonical = exactTagContent(html, /<link\b(?=[^>]*\brel=["']canonical["'])[^>]*\bhref=["']([^"']+)["'][^>]*>/giu, 'canonical link');
const ogUrl = exactTagContent(html, /<meta\b(?=[^>]*\bproperty=["']og:url["'])[^>]*\bcontent=["']([^"']+)["'][^>]*>/giu, 'og:url');
const robots = exactTagContent(html, /<meta\b(?=[^>]*\bname=["']robots["'])[^>]*\bcontent=["']([^"']+)["'][^>]*>/giu, 'robots meta');
exactTagContent(html, /<meta\b(?=[^>]*\bname=["']description["'])[^>]*\bcontent=["']([^"']+)["'][^>]*>/giu, 'description meta');
exactTagContent(html, /<meta\b(?=[^>]*\bproperty=["']og:title["'])[^>]*\bcontent=["']([^"']+)["'][^>]*>/giu, 'og:title');
exactTagContent(html, /<meta\b(?=[^>]*\bproperty=["']og:description["'])[^>]*\bcontent=["']([^"']+)["'][^>]*>/giu, 'og:description');
if (canonical !== canonicalUrl || ogUrl !== canonicalUrl || !/^index\s*,\s*follow$/iu.test(robots)) throw new Error('Canonical, Open Graph, or robots metadata is inconsistent.');
const title = exactTagContent(html, /<title\b[^>]*>([\s\S]*?)<\/title>/giu, 'title');
const canonicalLink = exactTagContent(html, /<link\b(?=[^>]*\brel=["']canonical["'])(?=[^>]*\bhref=["']([^"']+)["'])[^>]*>/giu, 'canonical link');
const description = exactTagContent(html, /<meta\b(?=[^>]*\bname=["']description["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/giu, 'description meta');
const robots = exactTagContent(html, /<meta\b(?=[^>]*\bname=["']robots["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/giu, 'robots meta');
const ogType = exactTagContent(html, /<meta\b(?=[^>]*\bproperty=["']og:type["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/giu, 'og:type');
const ogTitle = exactTagContent(html, /<meta\b(?=[^>]*\bproperty=["']og:title["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/giu, 'og:title');
const ogDescription = exactTagContent(html, /<meta\b(?=[^>]*\bproperty=["']og:description["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/giu, 'og:description');
const ogUrl = exactTagContent(html, /<meta\b(?=[^>]*\bproperty=["']og:url["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/giu, 'og:url');
const ogImage = exactTagContent(html, /<meta\b(?=[^>]*\bproperty=["']og:image["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/giu, 'og:image');
const twitterCard = exactTagContent(html, /<meta\b(?=[^>]*\bname=["']twitter:card["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/giu, 'twitter:card');
const twitterTitle = exactTagContent(html, /<meta\b(?=[^>]*\bname=["']twitter:title["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/giu, 'twitter:title');
const twitterDescription = exactTagContent(html, /<meta\b(?=[^>]*\bname=["']twitter:description["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/giu, 'twitter:description');
const twitterImage = exactTagContent(html, /<meta\b(?=[^>]*\bname=["']twitter:image["'])(?=[^>]*\bcontent=["']([^"']+)["'])[^>]*>/giu, 'twitter:image');
if (title !== publicMetadata.title || description !== publicMetadata.description ||
canonicalLink !== canonicalUrl || !/^index\s*,\s*follow$/iu.test(robots) ||
ogType !== 'profile' || ogTitle !== title || ogDescription !== description || ogUrl !== canonicalLink ||
twitterCard !== 'summary_large_image' || twitterTitle !== title || twitterDescription !== description ||
twitterImage !== ogImage) {
throw new Error('Canonical, title, description, Open Graph, Twitter, or robots metadata is inconsistent.');
}
let socialImage;
try { socialImage = new URL(ogImage); } catch { throw new Error('Social image URL is invalid.'); }
const canonicalOrigin = new URL(canonicalUrl);
if (socialImage.origin !== canonicalOrigin.origin || socialImage.username || socialImage.password || socialImage.port ||
socialImage.pathname !== publicMetadata.socialImagePath || socialImage.search || socialImage.hash) {
throw new Error('Social image must use the expected path on the canonical host.');
}
const jsonLdText = exactTagContent(html, /<script\b(?=[^>]*\btype=["']application\/ld\+json["'])[^>]*>([\s\S]*?)<\/script>/giu, 'JSON-LD block');
let jsonLd;
try { jsonLd = JSON.parse(jsonLdText); } catch { throw new Error('JSON-LD is not valid JSON.'); }
if (jsonLd?.['@context'] !== 'https://schema.org' || jsonLd?.name !== 'Luca Pîslaru' || jsonLd?.url !== canonicalUrl) throw new Error('JSON-LD identity or canonical URL is inconsistent.');
return allMatches(html, /<(?:script|link)\b(?=[^>]*(?:src|href)=["'](\/assets\/[^"']+)["'])[^>]*>/giu);
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.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.homeLocation['@type'] !== 'Place' || person.homeLocation.address['@type'] !== 'PostalAddress' ||
person.homeLocation.address.addressLocality !== publicMetadata.location.city ||
person.homeLocation.address.addressCountry !== publicMetadata.location.countryCode ||
JSON.stringify(person.sameAs) !== JSON.stringify(publicMetadata.sameAs)) {
throw new Error('ProfilePage and Person JSON-LD do not match the exact public identity metadata.');
}
return {
assetPaths: allMatches(html, /<(?:script|link)\b(?=[^>]*(?:src|href)=["'](\/assets\/[^"']+)["'])[^>]*>/giu),
socialImage,
};
}
async function request(fetchImpl, url, init = {}) {
@@ -105,7 +164,7 @@ export async function verifyProductionSmoke(options) {
const html = await rootResponse.text();
if (privateIdentifiers.test(html) || sourceMapReference.test(html)) throw new Error('Production HTML exposes a private identifier or source map.');
const noJs = verifyNoJavaScriptDocument(html);
const assetPaths = verifyMetadata(html, canonical.href);
const { assetPaths, socialImage } = verifyMetadata(html, canonical.href);
if (assetPaths.length === 0 || assetPaths.some((path) => path.endsWith('.map'))) throw new Error('Production HTML lacks a safe hashed asset reference.');
const assetResponse = await request(fetchImpl, new URL(assetPaths[0], canonical), { headers: { 'accept-encoding': 'br, gzip' } });
if (assetResponse.status !== 200) throw new Error('Production asset did not return 200.');
@@ -115,6 +174,15 @@ export async function verifyProductionSmoke(options) {
requireHeader(assetResponse, 'content-encoding', /^(?:br|gzip)$/u);
const assetText = await assetResponse.text();
if (privateIdentifiers.test(assetText) || sourceMapReference.test(assetText)) throw new Error('Production asset exposes a private identifier or source map.');
const socialImageResponse = await request(fetchImpl, socialImage);
if (socialImageResponse.status !== 200) throw new Error('Production social image did not return 200.');
verifySecurityHeaders(socialImageResponse, options.expectedManifestDigest);
requireHeader(socialImageResponse, 'cache-control', /max-age=31536000.*immutable/u);
const socialImageType = socialImageResponse.headers.get('content-type');
if (socialImageType === null || !new RegExp(`^${publicMetadata.socialImageType}(?:;|$)`, 'u').test(socialImageType)) {
throw new Error('Production social image content-type is missing or unsupported.');
}
if ((await socialImageResponse.arrayBuffer()).byteLength === 0) throw new Error('Production social image response is empty.');
for (const path of ['/robots.txt', '/sitemap.xml']) {
const response = await request(fetchImpl, new URL(path, canonical));
if (response.status !== 200) throw new Error(`${path} did not return 200.`);
@@ -135,7 +203,8 @@ export async function verifyProductionSmoke(options) {
videoRangeVerified = true;
}
return { schema: 'portfolio-production-smoke/v1', canonicalUrl: canonical.href, manifestDigest: options.expectedManifestDigest,
jsDisabledContentVerified: true, headings: noJs.headings, assetCompressionVerified: true, videoRangeVerified };
jsDisabledContentVerified: true, headings: noJs.headings, assetCompressionVerified: true,
socialImageVerified: true, videoRangeVerified };
}
function parseArguments(args) {