From 4636f814e374469c6cf6e0ed5b024427cc208648 Mon Sep 17 00:00:00 2001 From: Rodrigo Opalo Balog Date: Tue, 4 Aug 2026 17:57:26 -0400 Subject: [PATCH 1/2] feat(seo): enrich Rodrigo Opalo Person entity Add localized description and jobTitle to the canonical Person node on Home, sourced verbatim from the visible About-section copy. Recompute the two affected JSON-LD CSP hashes accordingly. --- src/seo/structuredData.ts | 16 ++++++++++++++++ vercel.json | 4 ++-- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/seo/structuredData.ts b/src/seo/structuredData.ts index 6fdddb9..b13310a 100644 --- a/src/seo/structuredData.ts +++ b/src/seo/structuredData.ts @@ -35,6 +35,8 @@ type PersonNode = { "@type": "Person"; "@id": typeof PERSON_ID; name: "Rodrigo Opalo"; + jobTitle: string; + description: string; url: typeof SITE_URL; image: "https://www.devrodri.com/img/sobremi.jpg"; sameAs: readonly [ @@ -93,6 +95,18 @@ const credentialDescriptions = { en: "Professional certification verified by IBM Skills Network and Credly.", } as const satisfies Record; +// Mirrors the visible About-section copy in SobreMiSection.tsx verbatim. +export const PERSON_JOB_TITLES = { + es: "Integrador de tecnología con mentalidad de producto", + en: "Technology integrator with a product mindset", +} as const satisfies Record; + +// Mirrors the visible About-section copy in SobreMiSection.tsx verbatim. +export const PERSON_DESCRIPTIONS = { + es: "Creo sitios, aplicaciones y sistemas a medida combinando estrategia, experiencia de usuario y tecnología. También implemento automatizaciones, integraciones y asistentes con IA para conectar herramientas, optimizar procesos y reducir trabajo manual.", + en: "I create custom websites, applications, and systems by combining strategy, user experience, and technology. I also implement automations, integrations, and AI assistants to connect tools, optimize processes, and reduce manual work.", +} as const satisfies Record; + function reference(id: string): Reference { return { "@id": id }; } @@ -113,6 +127,8 @@ function createHomeStructuredData(locale: Language): HomeStructuredData { "@type": "Person", "@id": PERSON_ID, name: "Rodrigo Opalo", + jobTitle: PERSON_JOB_TITLES[locale], + description: PERSON_DESCRIPTIONS[locale], url: SITE_URL, image: "https://www.devrodri.com/img/sobremi.jpg", sameAs: [ diff --git a/vercel.json b/vercel.json index b3d13a3..44c1175 100644 --- a/vercel.json +++ b/vercel.json @@ -24,7 +24,7 @@ }, { "key": "Content-Security-Policy", - "value": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self' https://formsubmit.co; script-src 'self' https://www.googletagmanager.com 'sha256-PFtIWoSjDUGI9FDBejrY6GsmT+jNY2fnEk/Wu0PMTxo=' 'sha256-NJsY0F2k7FEFgpKyakOXbgKzmylETn07jGhl+846ouI=' 'sha256-5JAfEolKKBpyKuNkJyFVP4QM03AqudwLaL6W4YE9Dow=' 'sha256-MWbnLG8L270wPoFC3v581s2nVNl/XC/TRKETtAkKpjY='; script-src-attr 'none'; style-src 'self' 'sha256-F3e8uFPgP10pK68RX7B5e1ZHd++LBK67gz3K7SNPrgA='; style-src-attr 'unsafe-inline'; font-src 'self'; img-src 'self' https://*.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://formsubmit.co https://www.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com; frame-src 'none'; manifest-src 'self'; worker-src 'none'; upgrade-insecure-requests" + "value": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self' https://formsubmit.co; script-src 'self' https://www.googletagmanager.com 'sha256-O8l3yTyim5wlcCf0NH8eooMDbrLdmZb7wspJwSqm1F8=' 'sha256-14RAKb0fSoF/0NrAPsGFzhEcabDY08YgW52+HTiwOC8=' 'sha256-5JAfEolKKBpyKuNkJyFVP4QM03AqudwLaL6W4YE9Dow=' 'sha256-MWbnLG8L270wPoFC3v581s2nVNl/XC/TRKETtAkKpjY='; script-src-attr 'none'; style-src 'self' 'sha256-F3e8uFPgP10pK68RX7B5e1ZHd++LBK67gz3K7SNPrgA='; style-src-attr 'unsafe-inline'; font-src 'self'; img-src 'self' https://*.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://formsubmit.co https://www.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com; frame-src 'none'; manifest-src 'self'; worker-src 'none'; upgrade-insecure-requests" } ] }, @@ -68,7 +68,7 @@ "Referrer-Policy": "strict-origin-when-cross-origin", "Permissions-Policy": "camera=(), microphone=(), geolocation=(), payment=(), usb=()", "X-Frame-Options": "DENY", - "Content-Security-Policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self' https://formsubmit.co; script-src 'self' https://www.googletagmanager.com 'sha256-PFtIWoSjDUGI9FDBejrY6GsmT+jNY2fnEk/Wu0PMTxo=' 'sha256-NJsY0F2k7FEFgpKyakOXbgKzmylETn07jGhl+846ouI=' 'sha256-5JAfEolKKBpyKuNkJyFVP4QM03AqudwLaL6W4YE9Dow=' 'sha256-MWbnLG8L270wPoFC3v581s2nVNl/XC/TRKETtAkKpjY='; script-src-attr 'none'; style-src 'self' 'sha256-F3e8uFPgP10pK68RX7B5e1ZHd++LBK67gz3K7SNPrgA='; style-src-attr 'unsafe-inline'; font-src 'self'; img-src 'self' https://*.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://formsubmit.co https://www.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com; frame-src 'none'; manifest-src 'self'; worker-src 'none'; upgrade-insecure-requests" + "Content-Security-Policy": "default-src 'self'; base-uri 'self'; object-src 'none'; frame-ancestors 'none'; form-action 'self' https://formsubmit.co; script-src 'self' https://www.googletagmanager.com 'sha256-O8l3yTyim5wlcCf0NH8eooMDbrLdmZb7wspJwSqm1F8=' 'sha256-14RAKb0fSoF/0NrAPsGFzhEcabDY08YgW52+HTiwOC8=' 'sha256-5JAfEolKKBpyKuNkJyFVP4QM03AqudwLaL6W4YE9Dow=' 'sha256-MWbnLG8L270wPoFC3v581s2nVNl/XC/TRKETtAkKpjY='; script-src-attr 'none'; style-src 'self' 'sha256-F3e8uFPgP10pK68RX7B5e1ZHd++LBK67gz3K7SNPrgA='; style-src-attr 'unsafe-inline'; font-src 'self'; img-src 'self' https://*.google-analytics.com https://www.googletagmanager.com; media-src 'self'; connect-src 'self' https://formsubmit.co https://www.googletagmanager.com https://*.google-analytics.com https://*.analytics.google.com; frame-src 'none'; manifest-src 'self'; worker-src 'none'; upgrade-insecure-requests" } } ] From 7cd7c820299cad38737a5474fef6933ed6718ac8 Mon Sep 17 00:00:00 2001 From: Rodrigo Opalo Balog Date: Tue, 4 Aug 2026 17:57:39 -0400 Subject: [PATCH 2/2] test(seo): verify Person hydration and CSP contracts Assert the enriched Person node on the static structured-data unit tests, extend verify-prerender.mjs to check the localized description/jobTitle and single canonical Person @id in the built HTML, and add a real hydrateRoot integration test proving the prerendered head reconciles to exactly one JSON-LD script with no duplication. --- scripts/verify-prerender.mjs | 52 +++++++ src/__tests__/webDeliveryPolicy.test.ts | 16 ++- src/test/HeadHydration.test.tsx | 173 ++++++++++++++++++++++++ src/test/structuredData.test.ts | 14 ++ 4 files changed, 250 insertions(+), 5 deletions(-) create mode 100644 src/test/HeadHydration.test.tsx diff --git a/scripts/verify-prerender.mjs b/scripts/verify-prerender.mjs index d05eb1e..7975127 100644 --- a/scripts/verify-prerender.mjs +++ b/scripts/verify-prerender.mjs @@ -119,6 +119,24 @@ const suspenseFallbacks = [ "Cargando caso LEM-BOX…", "Loading LEM-BOX case study…", ]; +const expectedPersonContent = new Map([ + [ + "/", + { + description: + "Creo sitios, aplicaciones y sistemas a medida combinando estrategia, experiencia de usuario y tecnología. También implemento automatizaciones, integraciones y asistentes con IA para conectar herramientas, optimizar procesos y reducir trabajo manual.", + jobTitle: "Integrador de tecnología con mentalidad de producto", + }, + ], + [ + "/en", + { + description: + "I create custom websites, applications, and systems by combining strategy, user experience, and technology. I also implement automations, integrations, and AI assistants to connect tools, optimize processes, and reduce manual work.", + jobTitle: "Technology integrator with a product mindset", + }, + ], +]); const expectedStructuredData = new Map([ [ "/", @@ -616,6 +634,13 @@ for (const route of expectedRoutes) { const expectedGraph = expectedStructuredData.get(route.pathname); const expectedJsonLdCount = expectedGraph === undefined ? 0 : 1; assert.equal(scripts.length, expectedJsonLdCount, route.pathname); + if (expectedJsonLdCount === 1) { + assert.match( + html, + /