From d4efbb4d740bbe7789d1742383d27bae449d41a9 Mon Sep 17 00:00:00 2001 From: nixvy-13 Date: Tue, 16 Sep 2025 16:42:23 +0200 Subject: [PATCH 1/4] feat: Add Open Graph image generation and metadata handling - Introduced `OgImageTemplate` component for rendering OG images with dynamic content. - Integrated `satori` library for SVG rendering of OG images. - Created unified route handler for OG images at `/og/[...route].png`. - Updated layout to include dynamic description and OG image metadata. - Added new JSON files for page content and metadata. - Implemented error handling for OG image generation. - Added utility functions for font loading and page data management. --- package-lock.json | 179 ++++++++++++++++++++ package.json | 1 + src/components/OgImageTemplate.tsx | 93 ++++++++++ src/content/config.ts | 0 src/content/pages/contacto.json | 0 src/content/pages/index.json | 0 src/content/pages/redes-sociales.json | 0 src/content/pages/sobre-nosotros.json | 0 src/layouts/Layout.astro | 39 ++--- src/pages/[...og].png.ts | 233 ++++++++++++++++++++++++++ src/pages/contacto.astro | 5 +- src/pages/index.astro | 5 +- src/pages/og.png.ts | 0 src/pages/og/[...route].png.ts | 0 src/pages/redes-sociales.astro | 5 +- src/pages/sobre-nosotros.astro | 4 +- src/utils/fontLoader.ts | 0 src/utils/ogErrorHandler.ts | 0 src/utils/pageDataLoader.ts | 0 src/utils/pageMetadata.ts | 0 20 files changed, 539 insertions(+), 25 deletions(-) create mode 100644 src/components/OgImageTemplate.tsx create mode 100644 src/content/config.ts create mode 100644 src/content/pages/contacto.json create mode 100644 src/content/pages/index.json create mode 100644 src/content/pages/redes-sociales.json create mode 100644 src/content/pages/sobre-nosotros.json create mode 100644 src/pages/[...og].png.ts create mode 100644 src/pages/og.png.ts create mode 100644 src/pages/og/[...route].png.ts create mode 100644 src/utils/fontLoader.ts create mode 100644 src/utils/ogErrorHandler.ts create mode 100644 src/utils/pageDataLoader.ts create mode 100644 src/utils/pageMetadata.ts diff --git a/package-lock.json b/package-lock.json index 3268047..77023e8 100644 --- a/package-lock.json +++ b/package-lock.json @@ -22,6 +22,7 @@ "react-hook-form": "^7.62.0", "react-p5": "^1.4.1", "react-phone-number-input": "^3.4.12", + "satori": "^0.18.2", "sharp": "^0.34.3", "tailwindcss": "^3.3.6" }, @@ -2568,6 +2569,22 @@ "resolved": "https://registry.npmjs.org/@shikijs/vscode-textmate/-/vscode-textmate-9.3.0.tgz", "integrity": "sha512-jn7/7ky30idSkd/O5yDBfAnVt+JJpepofP/POZ1iMOxK59cOfqIgg/Dj0eFsjOTMw+4ycJN0uhZH/Eb0bs/EUA==" }, + "node_modules/@shuding/opentype.js": { + "version": "1.4.0-beta.0", + "resolved": "https://registry.npmjs.org/@shuding/opentype.js/-/opentype.js-1.4.0-beta.0.tgz", + "integrity": "sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==", + "license": "MIT", + "dependencies": { + "fflate": "^0.7.3", + "string.prototype.codepointat": "^0.2.1" + }, + "bin": { + "ot": "bin/ot" + }, + "engines": { + "node": ">= 8.0.0" + } + }, "node_modules/@sindresorhus/is": { "version": "7.0.2", "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-7.0.2.tgz", @@ -3578,6 +3595,15 @@ "resolved": "https://registry.npmjs.org/base-64/-/base-64-1.0.0.tgz", "integrity": "sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==" }, + "node_modules/base64-js": { + "version": "0.0.8", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-0.0.8.tgz", + "integrity": "sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, "node_modules/binary-extensions": { "version": "2.2.0", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz", @@ -3753,6 +3779,15 @@ "node": ">= 6" } }, + "node_modules/camelize": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/camelize/-/camelize-1.0.1.tgz", + "integrity": "sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001672", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001672.tgz", @@ -4004,6 +4039,47 @@ "node": ">= 8" } }, + "node_modules/css-background-parser": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/css-background-parser/-/css-background-parser-0.1.0.tgz", + "integrity": "sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==", + "license": "MIT" + }, + "node_modules/css-box-shadow": { + "version": "1.0.0-3", + "resolved": "https://registry.npmjs.org/css-box-shadow/-/css-box-shadow-1.0.0-3.tgz", + "integrity": "sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==", + "license": "MIT" + }, + "node_modules/css-color-keywords": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/css-color-keywords/-/css-color-keywords-1.0.0.tgz", + "integrity": "sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==", + "license": "ISC", + "engines": { + "node": ">=4" + } + }, + "node_modules/css-gradient-parser": { + "version": "0.0.17", + "resolved": "https://registry.npmjs.org/css-gradient-parser/-/css-gradient-parser-0.0.17.tgz", + "integrity": "sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/css-to-react-native": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/css-to-react-native/-/css-to-react-native-3.2.0.tgz", + "integrity": "sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==", + "license": "MIT", + "dependencies": { + "camelize": "^1.0.0", + "css-color-keywords": "^1.0.0", + "postcss-value-parser": "^4.0.2" + } + }, "node_modules/cssesc": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", @@ -4301,6 +4377,15 @@ "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.4.0.tgz", "integrity": "sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==" }, + "node_modules/emoji-regex-xs": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/emoji-regex-xs/-/emoji-regex-xs-2.0.1.tgz", + "integrity": "sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/encodeurl": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", @@ -5220,6 +5305,12 @@ "reusify": "^1.0.4" } }, + "node_modules/fflate": { + "version": "0.7.4", + "resolved": "https://registry.npmjs.org/fflate/-/fflate-0.7.4.tgz", + "integrity": "sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==", + "license": "MIT" + }, "node_modules/file-entry-cache": { "version": "6.0.1", "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-6.0.1.tgz", @@ -5897,6 +5988,18 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/hex-rgb": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/hex-rgb/-/hex-rgb-4.3.0.tgz", + "integrity": "sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==", + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/html-escaper": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-3.0.3.tgz", @@ -6695,6 +6798,16 @@ "node": ">=14" } }, + "node_modules/linebreak": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/linebreak/-/linebreak-1.1.0.tgz", + "integrity": "sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==", + "license": "MIT", + "dependencies": { + "base64-js": "0.0.8", + "unicode-trie": "^2.0.0" + } + }, "node_modules/lines-and-columns": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", @@ -8086,6 +8199,12 @@ "resolved": "https://registry.npmjs.org/p5/-/p5-1.7.0.tgz", "integrity": "sha512-qrbT/44Dwm63ZtOKX/mp61pw+5yj6ijYLOmRv7p6zcfjbo83Vb0gVFEvW0kTLFu7hceWCig0HONo9F1bSlqbsQ==" }, + "node_modules/pako": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/pako/-/pako-0.2.9.tgz", + "integrity": "sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==", + "license": "MIT" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -8098,6 +8217,16 @@ "node": ">=6" } }, + "node_modules/parse-css-color": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/parse-css-color/-/parse-css-color-0.2.1.tgz", + "integrity": "sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==", + "license": "MIT", + "dependencies": { + "color-name": "^1.1.4", + "hex-rgb": "^4.1.0" + } + }, "node_modules/parse-latin": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/parse-latin/-/parse-latin-7.0.0.tgz", @@ -9111,6 +9240,28 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/satori": { + "version": "0.18.2", + "resolved": "https://registry.npmjs.org/satori/-/satori-0.18.2.tgz", + "integrity": "sha512-Y9fOzHuaslMX+3otoULyvUBOxXN6a0CJL+MPeFrHgGSPDwdSxkZdhY9W8U4MvDm0aT/+EIr5g18dvAQV+UplDA==", + "license": "MPL-2.0", + "dependencies": { + "@shuding/opentype.js": "1.4.0-beta.0", + "css-background-parser": "^0.1.0", + "css-box-shadow": "1.0.0-3", + "css-gradient-parser": "^0.0.17", + "css-to-react-native": "^3.0.0", + "emoji-regex-xs": "^2.0.1", + "escape-html": "^1.0.3", + "linebreak": "^1.1.0", + "parse-css-color": "^0.2.1", + "postcss-value-parser": "^4.2.0", + "yoga-layout": "^3.2.1" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/scheduler": { "version": "0.23.0", "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.0.tgz", @@ -9926,6 +10077,12 @@ "node": ">=8" } }, + "node_modules/string.prototype.codepointat": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/string.prototype.codepointat/-/string.prototype.codepointat-0.2.1.tgz", + "integrity": "sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==", + "license": "MIT" + }, "node_modules/string.prototype.includes": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/string.prototype.includes/-/string.prototype.includes-2.0.1.tgz", @@ -10245,6 +10402,12 @@ "globrex": "^0.1.2" } }, + "node_modules/tiny-inflate": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", + "integrity": "sha512-pkY1fj1cKHb2seWDy0B16HeWyczlJA9/WW3u3c4z/NiWDsO3DOU5D7nhTLE9CF0yXv/QZFY7sEJmj24dK+Rrqw==", + "license": "MIT" + }, "node_modules/tiny-invariant": { "version": "1.3.3", "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", @@ -10508,6 +10671,16 @@ "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", "dev": true }, + "node_modules/unicode-trie": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-trie/-/unicode-trie-2.0.0.tgz", + "integrity": "sha512-x7bc76x0bm4prf1VLg79uhAzKw8DVboClSN5VxJuQ+LKDOVEW9CdH+VY7SP+vX7xCYQqzzgQpFqz15zeLvAtZQ==", + "license": "MIT", + "dependencies": { + "pako": "^0.2.5", + "tiny-inflate": "^1.0.0" + } + }, "node_modules/unified": { "version": "11.0.5", "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", @@ -11796,6 +11969,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/yoga-layout": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/yoga-layout/-/yoga-layout-3.2.1.tgz", + "integrity": "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==", + "license": "MIT" + }, "node_modules/youch": { "version": "3.3.4", "resolved": "https://registry.npmjs.org/youch/-/youch-3.3.4.tgz", diff --git a/package.json b/package.json index a77f08b..f47479f 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "react-hook-form": "^7.62.0", "react-p5": "^1.4.1", "react-phone-number-input": "^3.4.12", + "satori": "^0.18.2", "sharp": "^0.34.3", "tailwindcss": "^3.3.6" }, diff --git a/src/components/OgImageTemplate.tsx b/src/components/OgImageTemplate.tsx new file mode 100644 index 0000000..0544da3 --- /dev/null +++ b/src/components/OgImageTemplate.tsx @@ -0,0 +1,93 @@ +// src/components/OgImageTemplate.tsx +import React from 'react'; + +// SVG del umbrella como string base64 - compatible con Satori +const umbrellaLogoBase64 = "data:image/svg+xml;base64," + Buffer.from(` + + + + + + + + + + + + + +`).toString('base64'); + +interface OgImageTemplateProps { + title: string; + description: string; + fontFamily?: string; +} + +export function OgImageTemplate({ title, description, fontFamily = 'Arial, sans-serif' }: OgImageTemplateProps) { + return ( +
+
+ {/* Logo Yellow Umbrella - Usando imagen base64 como recomienda Satori */} + + + {/* Título */} +

+ {title} +

+ + {/* Descripción */} +

+ {description} +

+
+
+ ); +} \ No newline at end of file diff --git a/src/content/config.ts b/src/content/config.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/content/pages/contacto.json b/src/content/pages/contacto.json new file mode 100644 index 0000000..e69de29 diff --git a/src/content/pages/index.json b/src/content/pages/index.json new file mode 100644 index 0000000..e69de29 diff --git a/src/content/pages/redes-sociales.json b/src/content/pages/redes-sociales.json new file mode 100644 index 0000000..e69de29 diff --git a/src/content/pages/sobre-nosotros.json b/src/content/pages/sobre-nosotros.json new file mode 100644 index 0000000..e69de29 diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index a1d9543..5b3a01f 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -1,13 +1,15 @@ --- interface Props { title: string; + description: string; } - -const { title } = Astro.props; +const { title, description } = Astro.props; import { ViewTransitions } from 'astro:transitions'; import { InteractiveRain } from "../components/Rain.jsx"; import { fade } from 'astro:transitions'; +const path = Astro.url.pathname; +const ogImageUrl = new URL(`/og${path === '/' ? '' : path}.png`, Astro.url); --- @@ -24,27 +26,22 @@ import { fade } from 'astro:transitions'; - - - - - - + + + + + + + + + + - - - - + + + + diff --git a/src/pages/[...og].png.ts b/src/pages/[...og].png.ts new file mode 100644 index 0000000..cc3eb40 --- /dev/null +++ b/src/pages/[...og].png.ts @@ -0,0 +1,233 @@ +// src/pages/[...og].png.ts - Manejo unificado de todas las rutas OG +import satori from 'satori'; +import type { APIRoute } from 'astro'; +import React from 'react'; +import { OgImageTemplate } from '../components/OgImageTemplate'; + +export const prerender = false; + +// Páginas con metadatos +const pagesData = [ + { + url: '/', + title: 'Yellow Umbrella', + description: "It's raining outside, take this" + }, + { + url: '/contacto', + title: 'Contacto', + description: 'Ponte en contacto con nosotros' + }, + { + url: '/redes-sociales', + title: 'Redes Sociales', + description: 'Síguenos en nuestras redes sociales' + }, + { + url: '/sobre-nosotros', + title: 'Sobre Nosotros', + description: 'Conoce más sobre nuestro equipo y misión' + } +]; + +export const GET: APIRoute = async ({ params, request }) => { + try { + // Cargar fuente MonaspaceKrypton con fallback más robusto + let fontData; + let fontName = 'MonaspaceKrypton'; + + try { + // Construir URL absoluta para la fuente local + const url = new URL(request.url); + const fontUrl = new URL('/fonts/MonaspaceKrypton-Regular.woff', url.origin); + + const fontResponse = await fetch(fontUrl.href); + if (!fontResponse.ok) { + throw new Error(`MonaspaceKrypton font error: ${fontResponse.status}`); + } + fontData = await fontResponse.arrayBuffer(); + console.log('MonaspaceKrypton font loaded successfully (unified og)'); + } catch (localError) { + console.error('Error cargando MonaspaceKrypton en unified og:', localError); + + // Fallback a una fuente WOFF (no WOFF2) que Satori puede manejar + try { + const fallbackResponse = await fetch( + 'https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff' + ); + if (!fallbackResponse.ok) { + throw new Error(`Inter fallback font error: ${fallbackResponse.status}`); + } + fontData = await fallbackResponse.arrayBuffer(); + fontName = 'Inter'; + console.log('Fallback to Inter font (WOFF) for unified og'); + } catch (fallbackError) { + console.error('Error cargando fuente fallback en unified og:', fallbackError); + + // Último fallback: usar sin fuentes personalizadas + console.log('Using system fonts as last fallback for unified og'); + fontData = null; + fontName = 'Arial, sans-serif'; + } + } + + const ogParam = params.og || ''; + + // Debugging - vamos a ver qué estamos recibiendo + console.log('Raw og param:', ogParam); + console.log('Type of og:', typeof ogParam); + console.log('Is array:', Array.isArray(ogParam)); + + // Manejar la ruta correctamente para todas las rutas OG unificadas + let requestedPath; + + if (!ogParam || ogParam === '' || ogParam === 'og.png') { + // Para /og.png (página principal) + requestedPath = '/'; + } else { + // Para rutas como /og/contacto.png + let cleanRoute = ''; + + if (Array.isArray(ogParam)) { + // Para rutas como og/contacto.png -> ["og", "contacto.png"] + if (ogParam.length >= 2) { + cleanRoute = ogParam[1]; // Tomar "contacto.png" + } else if (ogParam.length === 1 && ogParam[0] !== 'og') { + cleanRoute = ogParam[0]; // Caso edge donde solo hay un segmento + } + } else { + // Si es string, podría ser "og" (para /og.png) o "og/contacto.png" + if (ogParam === 'og') { + requestedPath = '/'; + } else { + cleanRoute = ogParam.replace(/^og\//, ''); // Remover prefijo "og/" + } + } + + if (cleanRoute) { + // Removemos la extensión .png si existe + cleanRoute = cleanRoute.replace(/\.png$/, ''); + // Construimos la ruta final + requestedPath = `/${cleanRoute}`; + } else if (!requestedPath) { + requestedPath = '/'; // Fallback a página principal + } + } + + console.log('OG param:', ogParam, 'Requested path:', requestedPath); + + const page = pagesData.find(p => p.url === requestedPath); + + if (!page) { + console.error(`Page not found for path: ${requestedPath}`); + console.log('Available pages:', pagesData.map(p => p.url)); + + // Devolver una imagen de error básica en SVG + const errorSvg = ` + + + + Page Not Found + + + ${requestedPath} + + + `; + + return new Response(errorSvg, { + status: 404, + headers: { + 'Content-Type': 'image/svg+xml; charset=utf-8', + 'Cache-Control': 'no-cache', + }, + }); + } + + const { title, description } = page; + console.log(`Generating OG image for: ${title} - ${description}`); + console.log(`Using font: ${fontName}`); + + // Crear el componente con propiedades adicionales para debugging + const element = React.createElement(OgImageTemplate, { + title, + description, + fontFamily: fontData ? fontName : 'Arial, sans-serif', // Usar Arial si no hay fuente personalizada + }); + + // Configuración robusta de Satori + const satoriConfig = { + width: 1200, + height: 630, + fonts: fontData ? [ + { + name: fontName, + data: fontData, + weight: 400 as const, + style: 'normal' as const, + }, + ] : [], // Array vacío si no hay datos de fuente + debug: false, // Cambiar a true para debugging visual + }; + + console.log('Satori config (unified og):', JSON.stringify({ + width: satoriConfig.width, + height: satoriConfig.height, + fontsCount: satoriConfig.fonts.length, + fontNames: satoriConfig.fonts.map(f => f.name), + })); + + const svg = await satori(element, satoriConfig); + + // Devolver SVG con headers apropiados para mejor compatibilidad + // Muchas plataformas de redes sociales aceptan SVG para Open Graph + return new Response(svg, { + headers: { + 'Content-Type': 'image/svg+xml; charset=utf-8', + 'Cache-Control': 'public, max-age=3600', // Cache por 1 hora + 'Access-Control-Allow-Origin': '*', + 'Access-Control-Allow-Methods': 'GET', + 'Access-Control-Allow-Headers': 'Content-Type', + }, + }); + } catch (error) { + console.error('Error generando imagen OG (unified):', error); + + // Crear una imagen de error más informativa + let errorMessage = 'Unknown error'; + if (error instanceof Error) { + errorMessage = error.message; + if (error.message.includes('Unsupported OpenType signature')) { + errorMessage = 'Font format not supported (try WOFF instead of WOFF2)'; + } else if (error.message.includes('Failed to parse URL')) { + errorMessage = 'Invalid font URL'; + } + } + + // Devolver una imagen de error en SVG simple y funcional + const errorSvg = ` + + + + YU + + Error Generating Image + + + ${errorMessage} + + + Route: ${params.og || 'unified-og'} + + + `; + + return new Response(errorSvg, { + status: 500, + headers: { + 'Content-Type': 'image/svg+xml; charset=utf-8', + 'Cache-Control': 'no-cache', + }, + }); + } +}; diff --git a/src/pages/contacto.astro b/src/pages/contacto.astro index d017388..42160c7 100644 --- a/src/pages/contacto.astro +++ b/src/pages/contacto.astro @@ -2,9 +2,12 @@ import Layout from "../layouts/Layout.astro"; import ContactForm from "../components/ContactForm.tsx"; import Head from "../components/Head.astro"; + +const title = "Contacto"; +const description = "Ponte en contacto con nosotros."; --- - +
diff --git a/src/pages/index.astro b/src/pages/index.astro index 9732403..0137782 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,9 +1,12 @@ --- import Layout from "../layouts/Layout.astro"; import Head from "../components/Head.astro"; + +const title = "Yellow Umbrella"; +const description = "It's raining outside, take this"; --- - +
diff --git a/src/pages/og.png.ts b/src/pages/og.png.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/og/[...route].png.ts b/src/pages/og/[...route].png.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/pages/redes-sociales.astro b/src/pages/redes-sociales.astro index 71a60ed..d2209c4 100644 --- a/src/pages/redes-sociales.astro +++ b/src/pages/redes-sociales.astro @@ -15,8 +15,11 @@ const iconMap = { }; export const prerender = false + +const title = "Redes sociales"; +const description = "Nuestras redes sociales."; --- - +
diff --git a/src/pages/sobre-nosotros.astro b/src/pages/sobre-nosotros.astro index 2dedf93..7776010 100644 --- a/src/pages/sobre-nosotros.astro +++ b/src/pages/sobre-nosotros.astro @@ -3,10 +3,12 @@ import Head from "../components/Head.astro"; import Layout from "../layouts/Layout.astro"; +const title = "Sobre nosotros"; +const description = "Conoce más sobre nuestro equipo y misión."; --- - +
diff --git a/src/utils/fontLoader.ts b/src/utils/fontLoader.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/ogErrorHandler.ts b/src/utils/ogErrorHandler.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/pageDataLoader.ts b/src/utils/pageDataLoader.ts new file mode 100644 index 0000000..e69de29 diff --git a/src/utils/pageMetadata.ts b/src/utils/pageMetadata.ts new file mode 100644 index 0000000..e69de29 From 34da4a8f1101fe257c2b0350936b4b5ac9f1ba60 Mon Sep 17 00:00:00 2001 From: nixvy-13 Date: Tue, 16 Sep 2025 16:51:54 +0200 Subject: [PATCH 2/4] borrados archivos no usados --- src/content/config.ts | 0 src/content/pages/contacto.json | 0 src/content/pages/index.json | 0 src/content/pages/redes-sociales.json | 0 src/content/pages/sobre-nosotros.json | 0 src/pages/og.png.ts | 0 src/pages/og/[...route].png.ts | 0 src/utils/fontLoader.ts | 0 src/utils/ogErrorHandler.ts | 0 src/utils/pageDataLoader.ts | 0 src/utils/pageMetadata.ts | 0 11 files changed, 0 insertions(+), 0 deletions(-) delete mode 100644 src/content/config.ts delete mode 100644 src/content/pages/contacto.json delete mode 100644 src/content/pages/index.json delete mode 100644 src/content/pages/redes-sociales.json delete mode 100644 src/content/pages/sobre-nosotros.json delete mode 100644 src/pages/og.png.ts delete mode 100644 src/pages/og/[...route].png.ts delete mode 100644 src/utils/fontLoader.ts delete mode 100644 src/utils/ogErrorHandler.ts delete mode 100644 src/utils/pageDataLoader.ts delete mode 100644 src/utils/pageMetadata.ts diff --git a/src/content/config.ts b/src/content/config.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/content/pages/contacto.json b/src/content/pages/contacto.json deleted file mode 100644 index e69de29..0000000 diff --git a/src/content/pages/index.json b/src/content/pages/index.json deleted file mode 100644 index e69de29..0000000 diff --git a/src/content/pages/redes-sociales.json b/src/content/pages/redes-sociales.json deleted file mode 100644 index e69de29..0000000 diff --git a/src/content/pages/sobre-nosotros.json b/src/content/pages/sobre-nosotros.json deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/og.png.ts b/src/pages/og.png.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/pages/og/[...route].png.ts b/src/pages/og/[...route].png.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/fontLoader.ts b/src/utils/fontLoader.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/ogErrorHandler.ts b/src/utils/ogErrorHandler.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/pageDataLoader.ts b/src/utils/pageDataLoader.ts deleted file mode 100644 index e69de29..0000000 diff --git a/src/utils/pageMetadata.ts b/src/utils/pageMetadata.ts deleted file mode 100644 index e69de29..0000000 From c9765d59b3bd660a5ffa9853940013f2fb2942b5 Mon Sep 17 00:00:00 2001 From: nixvy-13 Date: Thu, 18 Sep 2025 10:55:22 +0200 Subject: [PATCH 3/4] centralizada la informacion usada en las variables para el og dinamico en un json y limpiado el codigo de debug --- src/api/pages.json | 24 ++++++ src/pages/[...og].png.ts | 131 +-------------------------------- src/pages/contacto.astro | 6 +- src/pages/index.astro | 16 ++-- src/pages/redes-sociales.astro | 6 +- src/pages/sobre-nosotros.astro | 8 +- 6 files changed, 49 insertions(+), 142 deletions(-) create mode 100644 src/api/pages.json diff --git a/src/api/pages.json b/src/api/pages.json new file mode 100644 index 0000000..0c424f4 --- /dev/null +++ b/src/api/pages.json @@ -0,0 +1,24 @@ +{ + "pages": [ + { + "url": "/", + "title": "Yellow Umbrella", + "description": "It's raining outside, take this" + }, + { + "url": "/contacto", + "title": "Contacto", + "description": "Ponte en contacto con nosotros" + }, + { + "url": "/redes-sociales", + "title": "Redes sociales", + "description": "Siguenos en nuestras redes sociales" + }, + { + "url": "/sobre-nosotros", + "title": "Sobre nosotros", + "description": "Conoce más sobre nuestro equipo" + } + ] +} \ No newline at end of file diff --git a/src/pages/[...og].png.ts b/src/pages/[...og].png.ts index cc3eb40..dddb4a9 100644 --- a/src/pages/[...og].png.ts +++ b/src/pages/[...og].png.ts @@ -3,37 +3,14 @@ import satori from 'satori'; import type { APIRoute } from 'astro'; import React from 'react'; import { OgImageTemplate } from '../components/OgImageTemplate'; +import pagesData from '../api/pages.json'; export const prerender = false; -// Páginas con metadatos -const pagesData = [ - { - url: '/', - title: 'Yellow Umbrella', - description: "It's raining outside, take this" - }, - { - url: '/contacto', - title: 'Contacto', - description: 'Ponte en contacto con nosotros' - }, - { - url: '/redes-sociales', - title: 'Redes Sociales', - description: 'Síguenos en nuestras redes sociales' - }, - { - url: '/sobre-nosotros', - title: 'Sobre Nosotros', - description: 'Conoce más sobre nuestro equipo y misión' - } -]; - export const GET: APIRoute = async ({ params, request }) => { try { // Cargar fuente MonaspaceKrypton con fallback más robusto - let fontData; + let fontData: ArrayBuffer | null = null; let fontName = 'MonaspaceKrypton'; try { @@ -46,40 +23,14 @@ export const GET: APIRoute = async ({ params, request }) => { throw new Error(`MonaspaceKrypton font error: ${fontResponse.status}`); } fontData = await fontResponse.arrayBuffer(); - console.log('MonaspaceKrypton font loaded successfully (unified og)'); } catch (localError) { console.error('Error cargando MonaspaceKrypton en unified og:', localError); - - // Fallback a una fuente WOFF (no WOFF2) que Satori puede manejar - try { - const fallbackResponse = await fetch( - 'https://fonts.gstatic.com/s/inter/v12/UcCO3FwrK3iLTeHuS_fvQtMwCp50KnMa2JL7SUc.woff' - ); - if (!fallbackResponse.ok) { - throw new Error(`Inter fallback font error: ${fallbackResponse.status}`); - } - fontData = await fallbackResponse.arrayBuffer(); - fontName = 'Inter'; - console.log('Fallback to Inter font (WOFF) for unified og'); - } catch (fallbackError) { - console.error('Error cargando fuente fallback en unified og:', fallbackError); - - // Último fallback: usar sin fuentes personalizadas - console.log('Using system fonts as last fallback for unified og'); - fontData = null; - fontName = 'Arial, sans-serif'; - } } const ogParam = params.og || ''; - // Debugging - vamos a ver qué estamos recibiendo - console.log('Raw og param:', ogParam); - console.log('Type of og:', typeof ogParam); - console.log('Is array:', Array.isArray(ogParam)); - // Manejar la ruta correctamente para todas las rutas OG unificadas - let requestedPath; + let requestedPath: string | undefined; if (!ogParam || ogParam === '' || ogParam === 'og.png') { // Para /og.png (página principal) @@ -114,39 +65,9 @@ export const GET: APIRoute = async ({ params, request }) => { } } - console.log('OG param:', ogParam, 'Requested path:', requestedPath); - - const page = pagesData.find(p => p.url === requestedPath); - - if (!page) { - console.error(`Page not found for path: ${requestedPath}`); - console.log('Available pages:', pagesData.map(p => p.url)); - - // Devolver una imagen de error básica en SVG - const errorSvg = ` - - - - Page Not Found - - - ${requestedPath} - - - `; - - return new Response(errorSvg, { - status: 404, - headers: { - 'Content-Type': 'image/svg+xml; charset=utf-8', - 'Cache-Control': 'no-cache', - }, - }); - } + const page = pagesData.pages.find(p => p.url === requestedPath); const { title, description } = page; - console.log(`Generating OG image for: ${title} - ${description}`); - console.log(`Using font: ${fontName}`); // Crear el componente con propiedades adicionales para debugging const element = React.createElement(OgImageTemplate, { @@ -170,13 +91,6 @@ export const GET: APIRoute = async ({ params, request }) => { debug: false, // Cambiar a true para debugging visual }; - console.log('Satori config (unified og):', JSON.stringify({ - width: satoriConfig.width, - height: satoriConfig.height, - fontsCount: satoriConfig.fonts.length, - fontNames: satoriConfig.fonts.map(f => f.name), - })); - const svg = await satori(element, satoriConfig); // Devolver SVG con headers apropiados para mejor compatibilidad @@ -192,42 +106,5 @@ export const GET: APIRoute = async ({ params, request }) => { }); } catch (error) { console.error('Error generando imagen OG (unified):', error); - - // Crear una imagen de error más informativa - let errorMessage = 'Unknown error'; - if (error instanceof Error) { - errorMessage = error.message; - if (error.message.includes('Unsupported OpenType signature')) { - errorMessage = 'Font format not supported (try WOFF instead of WOFF2)'; - } else if (error.message.includes('Failed to parse URL')) { - errorMessage = 'Invalid font URL'; - } - } - - // Devolver una imagen de error en SVG simple y funcional - const errorSvg = ` - - - - YU - - Error Generating Image - - - ${errorMessage} - - - Route: ${params.og || 'unified-og'} - - - `; - - return new Response(errorSvg, { - status: 500, - headers: { - 'Content-Type': 'image/svg+xml; charset=utf-8', - 'Cache-Control': 'no-cache', - }, - }); } }; diff --git a/src/pages/contacto.astro b/src/pages/contacto.astro index 42160c7..634cf00 100644 --- a/src/pages/contacto.astro +++ b/src/pages/contacto.astro @@ -2,9 +2,11 @@ import Layout from "../layouts/Layout.astro"; import ContactForm from "../components/ContactForm.tsx"; import Head from "../components/Head.astro"; +import pagesData from "../api/pages.json"; -const title = "Contacto"; -const description = "Ponte en contacto con nosotros."; +const currentPath = "/contacto"; +const pageData = pagesData.pages.find((page) => page.url === currentPath); +const { title, description } = pageData; --- diff --git a/src/pages/index.astro b/src/pages/index.astro index 0137782..6a701eb 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,15 +1,17 @@ --- import Layout from "../layouts/Layout.astro"; import Head from "../components/Head.astro"; +import pagesData from "../api/pages.json"; -const title = "Yellow Umbrella"; -const description = "It's raining outside, take this"; +const currentPath = "/"; +const pageData = pagesData.pages.find(page => page.url === currentPath); +const { title, description } = pageData; --- -
-
- -
-
+
+
+ +
+
diff --git a/src/pages/redes-sociales.astro b/src/pages/redes-sociales.astro index d2209c4..40e3ebc 100644 --- a/src/pages/redes-sociales.astro +++ b/src/pages/redes-sociales.astro @@ -3,6 +3,7 @@ import Layout from "../layouts/Layout.astro"; import SocialCard from "../components/SocialCard.astro"; import Head from "../components/Head.astro"; import data from "../api/socials.json"; +import pagesData from "../api/pages.json"; import twitter from "../images/twitter.svg"; import github from "../images/github-circle.svg"; import linkedin from "../images/linkedin.svg"; @@ -16,8 +17,9 @@ const iconMap = { export const prerender = false -const title = "Redes sociales"; -const description = "Nuestras redes sociales."; +const currentPath = "/redes-sociales"; +const pageData = pagesData.pages.find(page => page.url === currentPath); +const { title, description } = pageData; --- diff --git a/src/pages/sobre-nosotros.astro b/src/pages/sobre-nosotros.astro index 7776010..d7714a8 100644 --- a/src/pages/sobre-nosotros.astro +++ b/src/pages/sobre-nosotros.astro @@ -1,11 +1,11 @@ --- - import Head from "../components/Head.astro"; import Layout from "../layouts/Layout.astro"; +import pagesData from "../api/pages.json"; -const title = "Sobre nosotros"; -const description = "Conoce más sobre nuestro equipo y misión."; - +const currentPath = "/sobre-nosotros"; +const pageData = pagesData.pages.find(page => page.url === currentPath); +const { title, description } = pageData; --- From ff661282a5c769e85ff6fb765392abd0cd4a457b Mon Sep 17 00:00:00 2001 From: nixvy-13 Date: Tue, 23 Sep 2025 16:37:37 +0200 Subject: [PATCH 4/4] Ahora funciona el og dinamico en preview, falta ajustar los estilos. --- src/components/OgImageTemplate.ts | 118 +++++++++++++++++++++++++++++ src/components/OgImageTemplate.tsx | 93 ----------------------- src/pages/[...og].png.ts | 104 +++++++++---------------- 3 files changed, 154 insertions(+), 161 deletions(-) create mode 100644 src/components/OgImageTemplate.ts delete mode 100644 src/components/OgImageTemplate.tsx diff --git a/src/components/OgImageTemplate.ts b/src/components/OgImageTemplate.ts new file mode 100644 index 0000000..3c061e2 --- /dev/null +++ b/src/components/OgImageTemplate.ts @@ -0,0 +1,118 @@ +// src/components/OgImageTemplate.ts +interface OgImageTemplateProps { + title: string; + description: string; +} + +export function generateOgImageSvg({ title, description }: OgImageTemplateProps): string { + // Escapar caracteres especiales para SVG + const escapeXml = (text: string) => { + return text + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + }; + + // Función para dividir texto largo en múltiples líneas + const wrapText = (text: string, maxLength: number = 40) => { + if (text.length <= maxLength) return [text]; + + const words = text.split(' '); + const lines: string[] = []; + let currentLine = ''; + + words.forEach(word => { + if ((currentLine + word).length <= maxLength) { + currentLine += (currentLine ? ' ' : '') + word; + } else { + if (currentLine) lines.push(currentLine); + currentLine = word; + } + }); + + if (currentLine) lines.push(currentLine); + return lines; + }; + + const titleLines = wrapText(title, 35); + const descriptionLines = wrapText(description, 60); + + // Calcular posiciones dinámicamente + const titleStartY = 280; + const titleLineHeight = 70; + const descriptionStartY = titleStartY + (titleLines.length * titleLineHeight) + 40; + const descriptionLineHeight = 40; + + return ` + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ${titleLines.map((line, index) => + `${escapeXml(line)}` + ).join('\n ')} + + + ${descriptionLines.map((line, index) => + `${escapeXml(line)}` + ).join('\n ')} +`; +} diff --git a/src/components/OgImageTemplate.tsx b/src/components/OgImageTemplate.tsx deleted file mode 100644 index 0544da3..0000000 --- a/src/components/OgImageTemplate.tsx +++ /dev/null @@ -1,93 +0,0 @@ -// src/components/OgImageTemplate.tsx -import React from 'react'; - -// SVG del umbrella como string base64 - compatible con Satori -const umbrellaLogoBase64 = "data:image/svg+xml;base64," + Buffer.from(` - - - - - - - - - - - - - -`).toString('base64'); - -interface OgImageTemplateProps { - title: string; - description: string; - fontFamily?: string; -} - -export function OgImageTemplate({ title, description, fontFamily = 'Arial, sans-serif' }: OgImageTemplateProps) { - return ( -
-
- {/* Logo Yellow Umbrella - Usando imagen base64 como recomienda Satori */} - - - {/* Título */} -

- {title} -

- - {/* Descripción */} -

- {description} -

-
-
- ); -} \ No newline at end of file diff --git a/src/pages/[...og].png.ts b/src/pages/[...og].png.ts index dddb4a9..604870a 100644 --- a/src/pages/[...og].png.ts +++ b/src/pages/[...og].png.ts @@ -1,110 +1,78 @@ -// src/pages/[...og].png.ts - Manejo unificado de todas las rutas OG -import satori from 'satori'; +// src/pages/[...og].png.ts import type { APIRoute } from 'astro'; -import React from 'react'; -import { OgImageTemplate } from '../components/OgImageTemplate'; +import { generateOgImageSvg } from '../components/OgImageTemplate'; import pagesData from '../api/pages.json'; export const prerender = false; -export const GET: APIRoute = async ({ params, request }) => { +export const GET: APIRoute = async ({ params }) => { try { - // Cargar fuente MonaspaceKrypton con fallback más robusto - let fontData: ArrayBuffer | null = null; - let fontName = 'MonaspaceKrypton'; - - try { - // Construir URL absoluta para la fuente local - const url = new URL(request.url); - const fontUrl = new URL('/fonts/MonaspaceKrypton-Regular.woff', url.origin); - - const fontResponse = await fetch(fontUrl.href); - if (!fontResponse.ok) { - throw new Error(`MonaspaceKrypton font error: ${fontResponse.status}`); - } - fontData = await fontResponse.arrayBuffer(); - } catch (localError) { - console.error('Error cargando MonaspaceKrypton en unified og:', localError); - } - const ogParam = params.og || ''; - // Manejar la ruta correctamente para todas las rutas OG unificadas - let requestedPath: string | undefined; + // Determinar la ruta solicitada + let requestedPath: string; if (!ogParam || ogParam === '' || ogParam === 'og.png') { - // Para /og.png (página principal) requestedPath = '/'; } else { - // Para rutas como /og/contacto.png let cleanRoute = ''; if (Array.isArray(ogParam)) { - // Para rutas como og/contacto.png -> ["og", "contacto.png"] if (ogParam.length >= 2) { - cleanRoute = ogParam[1]; // Tomar "contacto.png" + cleanRoute = ogParam[1]; } else if (ogParam.length === 1 && ogParam[0] !== 'og') { - cleanRoute = ogParam[0]; // Caso edge donde solo hay un segmento + cleanRoute = ogParam[0]; } } else { - // Si es string, podría ser "og" (para /og.png) o "og/contacto.png" if (ogParam === 'og') { requestedPath = '/'; } else { - cleanRoute = ogParam.replace(/^og\//, ''); // Remover prefijo "og/" + cleanRoute = ogParam.replace(/^og\//, ''); } } if (cleanRoute) { - // Removemos la extensión .png si existe cleanRoute = cleanRoute.replace(/\.png$/, ''); - // Construimos la ruta final requestedPath = `/${cleanRoute}`; } else if (!requestedPath) { - requestedPath = '/'; // Fallback a página principal + requestedPath = '/'; } } - const page = pagesData.pages.find(p => p.url === requestedPath); - + // Buscar los datos de la página + const page = pagesData.pages.find(p => p.url === requestedPath) || pagesData.pages[0]; const { title, description } = page; - // Crear el componente con propiedades adicionales para debugging - const element = React.createElement(OgImageTemplate, { - title, - description, - fontFamily: fontData ? fontName : 'Arial, sans-serif', // Usar Arial si no hay fuente personalizada - }); - - // Configuración robusta de Satori - const satoriConfig = { - width: 1200, - height: 630, - fonts: fontData ? [ - { - name: fontName, - data: fontData, - weight: 400 as const, - style: 'normal' as const, - }, - ] : [], // Array vacío si no hay datos de fuente - debug: false, // Cambiar a true para debugging visual - }; - - const svg = await satori(element, satoriConfig); - - // Devolver SVG con headers apropiados para mejor compatibilidad - // Muchas plataformas de redes sociales aceptan SVG para Open Graph - return new Response(svg, { + // Generar el SVG + const svgContent = generateOgImageSvg({ title, description }); + + return new Response(svgContent, { headers: { 'Content-Type': 'image/svg+xml; charset=utf-8', - 'Cache-Control': 'public, max-age=3600', // Cache por 1 hora + 'Cache-Control': 'public, max-age=3600', 'Access-Control-Allow-Origin': '*', - 'Access-Control-Allow-Methods': 'GET', - 'Access-Control-Allow-Headers': 'Content-Type', }, }); + } catch (error) { - console.error('Error generando imagen OG (unified):', error); + console.error('Error generando imagen OG:', error); + + const fallbackSvg = ` + + + + Yellow Umbrella + + + Error generando imagen + +`; + + return new Response(fallbackSvg, { + headers: { + 'Content-Type': 'image/svg+xml; charset=utf-8', + 'Cache-Control': 'public, max-age=300', + }, + }); } };