El patrón Builder permite construir objetos complejos paso a paso, separando el proceso de construcción de su representación final.
1. El Problema de Partida
En generateMetadata de las páginas de knowledge, cada ruta (categoría, post ES, post EN) construye a mano un objeto Metadata gigante: alternates, openGraph, twitter, canonical... Copiando y pegando el mismo bloque de 40 líneas y cambiando 3 campos, con alto riesgo de olvidar un campo (ej. images en twitter).
// ANTES — repetido y propenso a error en cada page.jsx
return {
title: category.title,
description: category.description,
alternates: { canonical: `.../${slugStr}`, languages: { 'es-ES': ..., 'en-US': ... } },
openGraph: { title: category.title, description: category.description, type: 'website', images: [...] },
twitter: { card: 'summary_large_image', title: category.title, description: category.description, images: [...] },
};2. La Solución Builder
Un MetadataBuilder fluido encadena solo los pasos necesarios y aplica valores por defecto sensatos (favicon, locale, tipo article vs website) sin obligar a repetirlos.
// src/lib/MetadataBuilder.js
export class MetadataBuilder {
constructor(baseUrl = 'https://manuprieto.com') {
this.baseUrl = baseUrl;
this.data = { alternates: { languages: {} }, openGraph: {}, twitter: { card: 'summary_large_image' } };
}
title(title) {
this.data.title = title;
this.data.openGraph.title = title;
this.data.twitter.title = title;
return this;
}
description(desc) {
this.data.description = desc;
this.data.openGraph.description = desc;
this.data.twitter.description = desc;
return this;
}
path(path, esPath, enPath) {
this.data.alternates.canonical = `${this.baseUrl}${path}`;
this.data.alternates.languages['es-ES'] = `${this.baseUrl}${esPath}`;
this.data.alternates.languages['en-US'] = `${this.baseUrl}${enPath}`;
this.data.openGraph.url = `${this.baseUrl}${path}`;
return this;
}
image(url, width = 1200, height = 630) {
const image = { url, width, height, alt: this.data.title };
this.data.openGraph.images = [image];
this.data.twitter.images = [url];
return this;
}
type(type = 'website', publishedTime) {
this.data.openGraph.type = type;
if (publishedTime) this.data.openGraph.publishedTime = publishedTime;
return this;
}
build() {
return this.data;
}
}3. Uso en generateMetadata
// app/knowledge/[...slug]/page.jsx — DESPUÉS
export async function generateMetadata({ params }) {
const post = findPost(params);
return new MetadataBuilder()
.title(post.meta.title)
.description(post.meta.description)
.path(`/knowledge/${post.slugPath}`, `/knowledge/${post.slugPath}`, `/en/knowledge/${post.enSlug}`)
.image(`https://manuprieto.com${post.meta.image}`)
.type('article', post.meta.date)
.build();
}4. Ventajas
- Sin duplicación: un solo lugar define cómo se sincronizan
openGraph/twitter/title. - Pasos opcionales: una categoría sin imagen simplemente omite
.image(); ninguna ramaifextra. - Legible como frase:
.title().description().path().image().build()documenta el propio proceso.