Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions public/brand/devrodri-wordmark-w1-black.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions scripts/verify-http.mjs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import assert from "node:assert/strict";
import { createHash } from "node:crypto";
import { once } from "node:events";
import { readdir, readFile } from "node:fs/promises";
import path from "node:path";
Expand Down Expand Up @@ -340,6 +341,23 @@ try {
assert.ok(!sitemap.includes("/gracias"));
assert.ok(!sitemap.includes("/en/thank-you"));

const brandLogoResponse = await request(
"/brand/devrodri-wordmark-w1-black.svg",
);
assert.equal(brandLogoResponse.status, 200, "brand logo");
assert.equal(
brandLogoResponse.headers.get("content-type"),
"image/svg+xml",
"brand logo content type",
);
const brandLogo = Buffer.from(await brandLogoResponse.arrayBuffer());
assert.equal(brandLogo.byteLength, 3968, "brand logo byte length");
assert.equal(
createHash("sha256").update(brandLogo).digest("hex"),
"3be5eaecfa9569652886dc812f4f34e6c9b1c5f4407ac6e476b16300fed9715f",
"brand logo SHA-256",
);

const homeHtml = await readFile(
path.join(projectRoot, "dist", "index.html"),
"utf8",
Expand Down
98 changes: 96 additions & 2 deletions scripts/verify-prerender.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -279,22 +279,38 @@ const expectedStructuredData = new Map([
[
"/",
{
types: ["WebSite", "Person", "EducationalOccupationalCredential"],
types: [
"WebSite",
"Person",
"EducationalOccupationalCredential",
"Brand",
"ImageObject",
],
ids: [
"https://www.devrodri.com/#website",
"https://www.devrodri.com/#person",
"https://www.devrodri.com/#ibm-full-stack-credential",
"https://www.devrodri.com/#brand",
"https://www.devrodri.com/#brand-logo",
],
},
],
[
"/en",
{
types: ["WebSite", "Person", "EducationalOccupationalCredential"],
types: [
"WebSite",
"Person",
"EducationalOccupationalCredential",
"Brand",
"ImageObject",
],
ids: [
"https://www.devrodri.com/#website",
"https://www.devrodri.com/#person",
"https://www.devrodri.com/#ibm-full-stack-credential",
"https://www.devrodri.com/#brand",
"https://www.devrodri.com/#brand-logo",
],
},
],
Expand Down Expand Up @@ -877,6 +893,52 @@ for (const route of expectedRoutes) {
personContent.jobTitle,
`${route.pathname}: Person jobTitle`,
);
assert.deepEqual(person.sameAs, [
"https://github.com/devrodri-com",
"https://www.linkedin.com/in/rodrigo-opalo-b56685390/",
]);
assert.deepEqual(person.brand, {
"@id": "https://www.devrodri.com/#brand",
});

const website = parsed["@graph"].find(
(node) => node["@type"] === "WebSite",
);
assert.deepEqual(website.creator, {
"@id": "https://www.devrodri.com/#person",
});
assert.deepEqual(website.publisher, {
"@id": "https://www.devrodri.com/#person",
});
assert.deepEqual(website.mainEntity, {
"@id": "https://www.devrodri.com/#person",
});
assert.deepEqual(website.about, {
"@id": "https://www.devrodri.com/#brand",
});

const brand = parsed["@graph"].find(
(node) => node["@type"] === "Brand",
);
assert.deepEqual(brand, {
"@type": "Brand",
"@id": "https://www.devrodri.com/#brand",
name: "devrodri",
url: "https://www.devrodri.com/",
owner: { "@id": "https://www.devrodri.com/#person" },
logo: { "@id": "https://www.devrodri.com/#brand-logo" },
});

const brandLogo = parsed["@graph"].find(
(node) => node["@type"] === "ImageObject",
);
assert.deepEqual(brandLogo, {
"@type": "ImageObject",
"@id": "https://www.devrodri.com/#brand-logo",
contentUrl:
"https://www.devrodri.com/brand/devrodri-wordmark-w1-black.svg",
encodingFormat: "image/svg+xml",
});
}
const hash = sha256Source(jsonLd);
liveInlineScriptHashes.add(hash);
Expand All @@ -896,6 +958,10 @@ for (const route of expectedRoutes) {
'"review"',
'"operatingSystem"',
"SoftwareApplication",
'"@type":"Organization"',
'"@type":"ProfessionalService"',
'"@type":"LocalBusiness"',
'"@type":"ProfilePage"',
]) {
assert.ok(!jsonLd.includes(forbidden), `${route.pathname}: ${forbidden}`);
}
Expand All @@ -914,6 +980,34 @@ for (const route of expectedRoutes) {
}
}

const brandLogoBuffer = await readFile(
path.join(distDirectory, "brand", "devrodri-wordmark-w1-black.svg"),
);
assert.equal(brandLogoBuffer.byteLength, 3968, "brand logo byte length");
assert.equal(
createHash("sha256").update(brandLogoBuffer).digest("hex"),
"3be5eaecfa9569652886dc812f4f34e6c9b1c5f4407ac6e476b16300fed9715f",
"brand logo SHA-256",
);
const brandLogoSource = brandLogoBuffer.toString("utf8");
assert.equal(count(brandLogoSource, "<svg "), 1, "brand logo SVG element");
assert.equal(count(brandLogoSource, "<path "), 1, "brand logo path element");
assert.ok(brandLogoSource.includes('viewBox="0 0 8158.408 1584"'));
for (const forbidden of [
"<text",
"font-family",
"font-face",
"<image",
"data:image",
"<script",
"javascript:",
"href=",
"xlink:href",
"<use",
]) {
assert.ok(!brandLogoSource.toLowerCase().includes(forbidden), forbidden);
}

assert.equal(new Set(renderedOgImageUrls).size, 12, "unique OG image URLs");
assert.equal(new Set(renderedOgImageHashes).size, 12, "unique OG image hashes");
assert.equal(expectedRoutes.filter((route) => route.lang === "es").length, 6);
Expand Down
10 changes: 5 additions & 5 deletions src/__tests__/webDeliveryPolicy.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,10 +73,10 @@ const vercelConfigurationPath = path.join(projectRoot, "vercel.json");
const indexHtmlPath = path.join(projectRoot, "index.html");

const contentSecurityPolicyBeforeCleanup =
"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' https://fonts.googleapis.com; style-src-attr 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; 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";
"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-XA+ZeYpYjPALeWHZDLTsu9Q27JN3EwR8PVvF5Par4g8=' 'sha256-vrx1r20Rqw1cq9I7n5yLC75yDekDRRhN4KlTEbAWDuo=' 'sha256-5JAfEolKKBpyKuNkJyFVP4QM03AqudwLaL6W4YE9Dow=' 'sha256-MWbnLG8L270wPoFC3v581s2nVNl/XC/TRKETtAkKpjY='; script-src-attr 'none'; style-src 'self' https://fonts.googleapis.com; style-src-attr 'unsafe-inline'; font-src 'self' https://fonts.gstatic.com; 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";

const contentSecurityPolicyBeforeNoJavaScript =
"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'; 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";
"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-XA+ZeYpYjPALeWHZDLTsu9Q27JN3EwR8PVvF5Par4g8=' 'sha256-vrx1r20Rqw1cq9I7n5yLC75yDekDRRhN4KlTEbAWDuo=' 'sha256-5JAfEolKKBpyKuNkJyFVP4QM03AqudwLaL6W4YE9Dow=' 'sha256-MWbnLG8L270wPoFC3v581s2nVNl/XC/TRKETtAkKpjY='; script-src-attr 'none'; style-src 'self'; 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";

const noJavaScriptStyle =
"[data-nojs-visible]{opacity:1!important;transform:none!important}[data-nojs-hide]{display:none!important}[data-nojs-language]{display:flex!important}@media(max-width:639px){[data-nojs-navbar]{position:static!important}[data-nojs-mobile-nav]{display:flex!important}}";
Expand All @@ -88,7 +88,7 @@ const noJavaScriptBlock =
`<noscript><style>${noJavaScriptStyle}</style></noscript>`;

const expectedContentSecurityPolicy =
"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";
"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-XA+ZeYpYjPALeWHZDLTsu9Q27JN3EwR8PVvF5Par4g8=' 'sha256-vrx1r20Rqw1cq9I7n5yLC75yDekDRRhN4KlTEbAWDuo=' '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";

const publishedStructuredData = [
STRUCTURED_DATA_BY_ROUTE["home:es"],
Expand Down Expand Up @@ -372,8 +372,8 @@ describe("web delivery policy", () => {
[
"'self'",
"https://www.googletagmanager.com",
"'sha256-O8l3yTyim5wlcCf0NH8eooMDbrLdmZb7wspJwSqm1F8='",
"'sha256-14RAKb0fSoF/0NrAPsGFzhEcabDY08YgW52+HTiwOC8='",
"'sha256-XA+ZeYpYjPALeWHZDLTsu9Q27JN3EwR8PVvF5Par4g8='",
"'sha256-vrx1r20Rqw1cq9I7n5yLC75yDekDRRhN4KlTEbAWDuo='",
"'sha256-5JAfEolKKBpyKuNkJyFVP4QM03AqudwLaL6W4YE9Dow='",
"'sha256-MWbnLG8L270wPoFC3v581s2nVNl/XC/TRKETtAkKpjY='",
],
Expand Down
50 changes: 49 additions & 1 deletion src/seo/structuredData.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,10 @@ const SITE_URL = "https://www.devrodri.com/";

export const WEBSITE_ID = "https://www.devrodri.com/#website";
export const PERSON_ID = "https://www.devrodri.com/#person";
export const BRAND_ID = "https://www.devrodri.com/#brand";
export const BRAND_LOGO_ID = "https://www.devrodri.com/#brand-logo";
export const BRAND_LOGO_URL =
"https://www.devrodri.com/brand/devrodri-wordmark-w1-black.svg";
export const IBM_CREDENTIAL_ID =
"https://www.devrodri.com/#ibm-full-stack-credential";
export const LEM_BOX_WEB_APPLICATION_ID =
Expand All @@ -29,6 +33,9 @@ type WebSiteNode = {
name: "devrodri";
inLanguage: readonly ["es", "en"];
creator: Reference;
publisher: Reference;
mainEntity: Reference;
about: Reference;
};

type PersonNode = {
Expand All @@ -44,6 +51,7 @@ type PersonNode = {
"https://www.linkedin.com/in/rodrigo-opalo-b56685390/",
];
hasCredential: Reference;
brand: Reference;
};

type CredentialNode = {
Expand All @@ -55,6 +63,22 @@ type CredentialNode = {
description: string;
};

type BrandNode = {
"@type": "Brand";
"@id": typeof BRAND_ID;
name: "devrodri";
url: typeof SITE_URL;
owner: Reference;
logo: Reference;
};

type BrandLogoNode = {
"@type": "ImageObject";
"@id": typeof BRAND_LOGO_ID;
contentUrl: typeof BRAND_LOGO_URL;
encodingFormat: "image/svg+xml";
};

type CreativeWorkNode = {
"@type": "CreativeWork";
"@id": string;
Expand All @@ -80,7 +104,13 @@ type WebApplicationNode = {

export type HomeStructuredData = {
"@context": typeof SCHEMA_CONTEXT;
"@graph": readonly [WebSiteNode, PersonNode, CredentialNode];
"@graph": readonly [
WebSiteNode,
PersonNode,
CredentialNode,
BrandNode,
BrandLogoNode,
];
};

export type LemBoxStructuredData = {
Expand Down Expand Up @@ -122,6 +152,9 @@ function createHomeStructuredData(locale: Language): HomeStructuredData {
name: "devrodri",
inLanguage: ["es", "en"],
creator: reference(PERSON_ID),
publisher: reference(PERSON_ID),
mainEntity: reference(PERSON_ID),
about: reference(BRAND_ID),
},
{
"@type": "Person",
Expand All @@ -136,6 +169,7 @@ function createHomeStructuredData(locale: Language): HomeStructuredData {
"https://www.linkedin.com/in/rodrigo-opalo-b56685390/",
],
hasCredential: reference(IBM_CREDENTIAL_ID),
brand: reference(BRAND_ID),
},
{
"@type": "EducationalOccupationalCredential",
Expand All @@ -145,6 +179,20 @@ function createHomeStructuredData(locale: Language): HomeStructuredData {
url: IBM_CREDLY_URL,
description: credentialDescriptions[locale],
},
{
"@type": "Brand",
"@id": BRAND_ID,
name: "devrodri",
url: SITE_URL,
owner: reference(PERSON_ID),

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Remove the unsupported Brand owner property

When a crawler validates this graph against Schema.org, owner is not recognized for a Brand: its documented domains are CreativeWork and Product, so consumers can warn on or discard this ownership edge. The supported Person.brand relationship added above already connects Rodrigo to devrodri; remove this property or model the reverse relationship using a property whose domain includes Brand.

Useful? React with 👍 / 👎.

logo: reference(BRAND_LOGO_ID),
},
{
"@type": "ImageObject",
"@id": BRAND_LOGO_ID,
contentUrl: BRAND_LOGO_URL,
encodingFormat: "image/svg+xml",
},
],
};
}
Expand Down
Loading
Loading