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/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/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/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..604870a --- /dev/null +++ b/src/pages/[...og].png.ts @@ -0,0 +1,78 @@ +// src/pages/[...og].png.ts +import type { APIRoute } from 'astro'; +import { generateOgImageSvg } from '../components/OgImageTemplate'; +import pagesData from '../api/pages.json'; + +export const prerender = false; + +export const GET: APIRoute = async ({ params }) => { + try { + const ogParam = params.og || ''; + + // Determinar la ruta solicitada + let requestedPath: string; + + if (!ogParam || ogParam === '' || ogParam === 'og.png') { + requestedPath = '/'; + } else { + let cleanRoute = ''; + + if (Array.isArray(ogParam)) { + if (ogParam.length >= 2) { + cleanRoute = ogParam[1]; + } else if (ogParam.length === 1 && ogParam[0] !== 'og') { + cleanRoute = ogParam[0]; + } + } else { + if (ogParam === 'og') { + requestedPath = '/'; + } else { + cleanRoute = ogParam.replace(/^og\//, ''); + } + } + + if (cleanRoute) { + cleanRoute = cleanRoute.replace(/\.png$/, ''); + requestedPath = `/${cleanRoute}`; + } else if (!requestedPath) { + requestedPath = '/'; + } + } + + // Buscar los datos de la página + const page = pagesData.pages.find(p => p.url === requestedPath) || pagesData.pages[0]; + const { title, description } = page; + + // 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', + 'Access-Control-Allow-Origin': '*', + }, + }); + + } catch (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', + }, + }); + } +}; diff --git a/src/pages/contacto.astro b/src/pages/contacto.astro index d017388..634cf00 100644 --- a/src/pages/contacto.astro +++ b/src/pages/contacto.astro @@ -2,9 +2,14 @@ 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 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 9732403..6a701eb 100644 --- a/src/pages/index.astro +++ b/src/pages/index.astro @@ -1,12 +1,17 @@ --- import Layout from "../layouts/Layout.astro"; import Head from "../components/Head.astro"; +import pagesData from "../api/pages.json"; + +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 71a60ed..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"; @@ -15,8 +16,12 @@ const iconMap = { }; export const prerender = false + +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 2dedf93..d7714a8 100644 --- a/src/pages/sobre-nosotros.astro +++ b/src/pages/sobre-nosotros.astro @@ -1,12 +1,14 @@ --- - import Head from "../components/Head.astro"; import Layout from "../layouts/Layout.astro"; +import pagesData from "../api/pages.json"; - +const currentPath = "/sobre-nosotros"; +const pageData = pagesData.pages.find(page => page.url === currentPath); +const { title, description } = pageData; --- - +