@osdeibi/bucky-seo-react
Version:
React component for managing meta tags, Open Graph, and dynamic JSON-LD
507 lines (476 loc) • 13.7 kB
JavaScript
import { useEffect } from 'react';
function useDynamicHead(cfg) {
useEffect(() => {
const metaElements = [];
const ogElements = [];
const scriptElements = [];
// Title
if (cfg.title) {
document.title = cfg.title;
}
// Generic metas
(cfg.metas || []).forEach(({ name, content }) => {
const m = document.createElement("meta");
m.setAttribute("name", name);
m.setAttribute("content", content);
document.head.appendChild(m);
metaElements.push(m);
});
// Open Graph
(cfg.ogs || []).forEach(({ property, content }) => {
const m = document.createElement("meta");
m.setAttribute("property", property);
m.setAttribute("content", content);
document.head.appendChild(m);
ogElements.push(m);
});
// JSON-LD
(cfg.jsonLd || []).forEach((obj) => {
const s = document.createElement("script");
s.type = "application/ld+json";
s.text = JSON.stringify(obj);
document.head.appendChild(s);
scriptElements.push(s);
});
return () => {
metaElements.forEach((el) => document.head.removeChild(el));
ogElements.forEach((el) => document.head.removeChild(el));
scriptElements.forEach((el) => document.head.removeChild(el));
};
}, [
cfg.title,
JSON.stringify(cfg.metas),
JSON.stringify(cfg.ogs),
JSON.stringify(cfg.jsonLd),
]);
}
function DynamicHead({ metaTags, structuredData, }) {
// Construir arrays para el hook
const metas = [
(metaTags === null || metaTags === void 0 ? void 0 : metaTags.description) && { name: "description", content: metaTags.description },
(metaTags === null || metaTags === void 0 ? void 0 : metaTags.canonicalUrl) && { name: "canonical", content: metaTags.canonicalUrl },
(metaTags === null || metaTags === void 0 ? void 0 : metaTags.robots) && { name: "robots", content: metaTags.robots },
].filter(Boolean);
const ogs = Object.entries((metaTags === null || metaTags === void 0 ? void 0 : metaTags.og) || {}).map(([key, val]) => ({
property: `og:${key}`,
content: val,
}));
const jsonLd = (structuredData || []).map((sd) => ({
"@context": "https://schema.org",
"@type": sd.type,
...sd.data,
}));
// Invocación del hook
useDynamicHead({
title: metaTags === null || metaTags === void 0 ? void 0 : metaTags.title,
metas,
ogs,
jsonLd,
});
// No renderiza nada en el DOM
return null;
}
function videoObject(data) {
const obj = {
name: data.name,
thumbnailUrl: data.thumbnailUrl,
uploadDate: data.uploadDate,
...(data.description && { description: data.description }),
...(data.contentUrl && { contentUrl: data.contentUrl }),
...(data.duration && { duration: data.duration }),
...(data.embedUrl && { embedUrl: data.embedUrl }),
...(data.expires && { expires: data.expires }),
// BroadcastEvent for LIVE badge
...(data.publication && {
publication: {
"@type": "BroadcastEvent",
...data.publication,
},
}),
// InteractionCounter instead of legacy interactionCount
...(data.interactionStatistic && {
interactionStatistic: {
"@type": "InteractionCounter",
interactionType: { "@type": "WatchAction" },
userInteractionCount: data.interactionStatistic,
},
}),
// Clip markup for key moments
...(data.hasPart && {
hasPart: data.hasPart.map((clip) => ({
"@type": "Clip",
...clip,
})),
}),
// SeekToAction for automatic key moments
...(data.potentialAction && {
potentialAction: {
"@type": "SeekToAction",
target: data.potentialAction.target,
"startOffset-input": `required name=${data.potentialAction.placeholderName}`,
},
}),
// Regions allowed or ineligible
...(data.regionsAllowed && { regionsAllowed: data.regionsAllowed }),
...(data.ineligibleRegion && { ineligibleRegion: data.ineligibleRegion }),
};
return {
type: "VideoObject",
data: obj,
};
}
/**
* Generador de esquema VacationRental que inyecta el JSON-LD con @context y @type.
* Basado en el ejemplo oficial de Google Search Central. :contentReference[oaicite:0]{index=0}
*/
function vacationRental(opts) {
return {
type: "VacationRental",
data: {
"@context": "https://schema.org",
"@type": "VacationRental",
...opts,
},
};
}
function paywalledContent(opts) {
const { contentType, ...restProps } = opts;
return {
type: contentType,
data: {
"@context": "https://schema.org",
"@type": contentType,
...restProps
},
};
}
function speakable(opts) {
var _a;
const workType = (_a = opts.type) !== null && _a !== void 0 ? _a : "WebPage";
const { speakable, type, ...rest } = opts;
return {
type: workType,
data: {
"@context": "https://schema.org",
"@type": workType,
...rest,
speakable,
},
};
}
function softwareApplication(opts) {
return {
type: "SoftwareApplication",
data: {
"@context": "https://schema.org",
"@type": "SoftwareApplication",
...opts,
},
};
}
function reviewSnippet(opts) {
const { contentType, ...rest } = opts;
return {
type: contentType,
data: {
"@context": "https://schema.org",
"@type": contentType,
...rest,
},
};
}
function recipe(opts) {
return {
type: "Recipe",
data: {
"@context": "https://schema.org",
"@type": "Recipe",
...opts,
},
};
}
/**
* Generador de QAPage según Google Search Central :contentReference[oaicite:0]{index=0}.
*/
function qaPage(opts) {
return {
type: "QAPage",
data: {
"@context": "https://schema.org",
"@type": "QAPage",
mainEntity: opts.mainEntity,
},
};
}
/**
* Generador de ProfilePage:
* Produce un JSON-LD como en el ejemplo de Google Search Central.
*/
function profilePage(opts) {
return {
type: "ProfilePage",
data: {
"@context": "https://schema.org",
"@type": "ProfilePage",
mainEntity: opts.mainEntity,
},
};
}
function product(opts) {
return {
type: "Product",
data: {
"@context": "https://schema.org",
"@type": "Product",
...opts,
},
};
}
/**
* Generador de JSON-LD para Practice problems (Quiz) :contentReference[oaicite:0]{index=0}
*/
function quiz(opts) {
return {
type: "Quiz",
data: {
"@context": "https://schema.org",
"@type": "Quiz",
...opts,
},
};
}
/**
* Generador de Organization basado en el ejemplo oficial :contentReference[oaicite:0]{index=0}
*/
function organization(opts) {
return {
type: "Organization",
data: {
"@context": "https://schema.org",
"@type": "Organization",
...opts,
},
};
}
/**
* Generador de esquema Movie según Google Search Central.
*/
function movie(opts) {
return {
type: "Movie",
data: {
"@context": "https://schema.org",
"@type": "Movie",
...opts,
},
};
}
/**
* Generador de JSON-LD para Math Solvers basado en Google Search Central.
* https://developers.google.com/search/docs/appearance/structured-data/math-solvers
*/
function mathSolver(opts) {
var _a;
const workType = (_a = opts.type) !== null && _a !== void 0 ? _a : "WebPage";
const { type: _ignored, stepByStep, mathExpression, ...rest } = opts;
return {
type: workType,
data: {
"@context": "https://schema.org",
"@type": workType,
...rest,
solverType: "MathSolver", // etiquetamos como MathSolver
mathExpression,
stepByStep,
},
};
}
/**
* Generador de LocalBusiness según Google Search Central.
* Ejemplo de Restaurant con LocalBusiness:
* :contentReference[oaicite:0]{index=0}
*/
function localBusiness(opts) {
return {
type: "LocalBusiness",
data: {
"@context": "https://schema.org",
"@type": "LocalBusiness",
...opts,
},
};
}
/**
* Generador de JobPosting según Google Search Central .
*/
function jobPosting(opts) {
return {
type: "JobPosting",
data: {
"@context": "https://schema.org",
"@type": "JobPosting",
...opts
},
};
}
/**
* Genera un JSON-LD para ImageObject con licencias.
* - `license` y `acquireLicensePage` son obligatorios :contentReference[oaicite:0]{index=0}.
* - `creditText`, `creator` y `copyrightNotice` opcionales :contentReference[oaicite:1]{index=1}.
*/
function imageObject(opts) {
return {
type: "ImageObject",
data: {
"@context": "https://schema.org",
"@type": "ImageObject",
...opts,
},
};
}
/** Generador de JSON-LD para FAQPage según Google Search Central */
function faqPage(opts) {
return {
type: "FAQPage",
data: {
"@context": "https://schema.org",
"@type": "FAQPage",
mainEntity: opts.mainEntity,
},
};
}
/**
* Generador de JSON-LD para Event, basado en el ejemplo oficial :contentReference[oaicite:0]{index=0}.
*/
function event(opts) {
return {
type: "Event",
data: {
"@context": "https://schema.org",
"@type": "Event",
...opts,
},
};
}
/**
* Genera un JSON-LD tipo EmployerAggregateRating:
*/
function employerAggregateRating(opts) {
return {
type: "EmployeeRole", // o puedes usar "Organization" según contexto
data: {
"@context": "https://schema.org",
"@type": "EmployerAggregateRating",
...opts,
},
};
}
/**
* Education Q&A usa el mismo marcado que QAPage,
* pero exportamos un helper con nombre más explícito.
*/
function educationQAPage(opts) {
return {
type: "QAPage",
data: {
"@context": "https://schema.org",
"@type": "QAPage",
mainEntity: opts.mainEntity,
},
};
}
/**
* Generador de JSON-LD para un post de foro (DiscussionForumPosting).
*/
function discussionForumPosting(opts) {
return {
type: "DiscussionForumPosting",
data: {
"@context": "https://schema.org",
"@type": "DiscussionForumPosting",
...opts,
},
};
}
/**
* Generador de JSON-LD para Dataset.
*/
function dataset(opts) {
return {
type: "Dataset",
data: {
"@context": "https://schema.org",
"@type": "Dataset",
...opts,
},
};
}
/**
* Generador de JSON-LD para Course.
*/
function course(opts) {
return {
type: "Course",
data: {
"@context": "https://schema.org",
"@type": "Course",
...opts,
},
};
}
function carousel(opts) {
return {
type: "ItemList",
data: {
"@context": "https://schema.org",
"@type": "ItemList",
itemListElement: opts.items.map(({ position, url, item }) => {
const base = { "@type": "ListItem", position };
if (item) {
return { ...base, item };
}
else {
return { ...base, url };
}
}),
},
};
}
/**
* Generador de JSON-LD para BreadcrumbList.
*
* Google espera un objeto @type=BreadcrumbList con un array
* itemListElement de ListItem :contentReference[oaicite:0]{index=0}.
*/
function breadcrumbList(opts) {
return {
type: "BreadcrumbList",
data: {
"@context": "https://schema.org",
"@type": "BreadcrumbList",
itemListElement: opts.itemListElement.map(({ position, name, item }) => ({
"@type": "ListItem",
position,
name,
item
}))
}
};
}
/**
* Generador de JSON-LD para Article.
*/
function article(opts) {
var _a;
const type = (_a = opts.articleType) !== null && _a !== void 0 ? _a : "Article";
const { articleType, ...rest } = opts;
return {
type,
data: {
"@context": "https://schema.org",
"@type": type,
...rest,
},
};
}
export { DynamicHead, article, breadcrumbList, carousel, course, dataset, discussionForumPosting, educationQAPage, employerAggregateRating, event, faqPage, imageObject, jobPosting, localBusiness, mathSolver, movie, organization, paywalledContent, product, profilePage, qaPage, quiz, recipe, reviewSnippet, softwareApplication, speakable, vacationRental, videoObject };