diff --git a/front/package.json b/front/package.json index 0b3d42f..7cf824b 100644 --- a/front/package.json +++ b/front/package.json @@ -22,7 +22,9 @@ "marked": "^15.0.11", "react": "^19.1.1", "react-dom": "^19.1.1", - "serve": "^14.2.4" + "satori": "^0.18.3", + "serve": "^14.2.4", + "sharp": "^0.34.4" }, "devDependencies": { "sass-embedded": "^1.90.0" diff --git a/front/public/fonts/sohne/soehne-dreiviertelfett.otf b/front/public/fonts/sohne/soehne-dreiviertelfett.otf new file mode 100644 index 0000000..dccb543 Binary files /dev/null and b/front/public/fonts/sohne/soehne-dreiviertelfett.otf differ diff --git a/front/src/components/OgImage/OgImage.module.scss b/front/src/components/OgImage/OgImage.module.scss new file mode 100644 index 0000000..f2b75ef --- /dev/null +++ b/front/src/components/OgImage/OgImage.module.scss @@ -0,0 +1,7 @@ +.wrapper { + display: flex; + flex-direction: column; + + background-color: #ff3e3e; // gorria + color: #ffffff; +} diff --git a/front/src/components/OgImage/OgImage.tsx b/front/src/components/OgImage/OgImage.tsx new file mode 100644 index 0000000..9955ed2 --- /dev/null +++ b/front/src/components/OgImage/OgImage.tsx @@ -0,0 +1,59 @@ +import { type CollectionEntry } from "astro:content"; +import type { Ekintza } from "models/Ekintza"; + +import dayjs from "dayjs"; +import "dayjs/locale/eu"; + +dayjs.locale("eu"); + +export function OgImage(props: CollectionEntry<"ekintzak">) { + const ekintza: Ekintza = props as unknown as Ekintza; + + return ( +
+
+

{ekintza.izenburua}

+

+ {dayjs(ekintza.hitzordua).format("MMMM[k] D. H:mm")} +

+ +

{ekintza.titularra}

+
+ +
+ + + + + + + + +
+
+ ); +} diff --git a/front/src/components/OgImage/index.ts b/front/src/components/OgImage/index.ts new file mode 100644 index 0000000..3d42fb2 --- /dev/null +++ b/front/src/components/OgImage/index.ts @@ -0,0 +1 @@ +export { OgImage } from "./OgImage"; diff --git a/front/src/layouts/Layout.astro b/front/src/layouts/Layout.astro index 23a583c..aaa3b07 100644 --- a/front/src/layouts/Layout.astro +++ b/front/src/layouts/Layout.astro @@ -5,9 +5,14 @@ import "../style/vars/index.scss"; interface Props { title: string; description?: string; + ogImage?: string; } -const { title, description = "" } = Astro.props; +const { + title, + description = "", + ogImage = "https://laba.eus/og-image.jpg", +} = Astro.props; --- @@ -31,11 +36,8 @@ const { title, description = "" } = Astro.props; - - + + diff --git a/front/src/pages/ekintzak/[ekintza].astro b/front/src/pages/ekintzak/[ekintza]/index.astro similarity index 100% rename from front/src/pages/ekintzak/[ekintza].astro rename to front/src/pages/ekintzak/[ekintza]/index.astro diff --git a/front/src/pages/ekintzak/[ekintza]/og.png.ts b/front/src/pages/ekintzak/[ekintza]/og.png.ts new file mode 100644 index 0000000..61dd97e --- /dev/null +++ b/front/src/pages/ekintzak/[ekintza]/og.png.ts @@ -0,0 +1,53 @@ +import fs from "fs/promises"; +import satori from "satori"; +import sharp from "sharp"; +import { getCollection } from "astro:content"; +import type { InferGetStaticParamsType } from "astro"; +import { OgImage } from "../../../components/OgImage"; + +const posts = await getCollection("ekintzak"); +type Params = InferGetStaticParamsType; + +export async function GET({ params }: { params: Params }) { + const postEntry = posts.find((p) => p.data?.slug === params.ekintza); + if (!postEntry) { + return new Response("Post not found", { status: 404 }); + } + + const element = OgImage(postEntry.data); + const png = await PNG(element); + return new Response(png as any, { + headers: { + "Content-Type": "image/png", + }, + }); +} + +export async function getStaticPaths() { + return posts.map((p) => ({ + params: { ekintza: p.data?.slug }, + props: p.data, + })); +} + +export async function SVG(component: JSX.Element) { + return await satori(component as any, { + width: 1200, + height: 630, + fonts: [ + { + name: "Sohne", + data: await fs.readFile( + "./public/fonts/sohne/soehne-dreiviertelfett.otf" + ), + weight: 400, + }, + ], + }); +} + +export async function PNG(component: JSX.Element) { + return await sharp(Buffer.from(await SVG(component))) + .png() + .toBuffer(); +} diff --git a/front/src/views/ekintza/Ekintza.astro b/front/src/views/ekintza/Ekintza.astro index 91d53ca..b821b3a 100644 --- a/front/src/views/ekintza/Ekintza.astro +++ b/front/src/views/ekintza/Ekintza.astro @@ -23,7 +23,11 @@ const { ekintza } = Astro.props; const irudia = ekintza.mainMedia ? getImageData(ekintza.mainMedia) : null; --- - +
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d9717f..6f27845 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -115,9 +115,15 @@ importers: react-dom: specifier: ^19.1.1 version: 19.1.1(react@19.1.1) + satori: + specifier: ^0.18.3 + version: 0.18.3 serve: specifier: ^14.2.4 version: 14.2.4 + sharp: + specifier: ^0.34.4 + version: 0.34.4 devDependencies: sass-embedded: specifier: ^1.90.0 @@ -457,9 +463,6 @@ packages: peerDependencies: react: '>=16.8.0' - '@emnapi/runtime@1.5.0': - resolution: {integrity: sha512-97/BJ3iXHww3djw6hYIfErCZFee7qCtrneuLa20UXFCOTCfBM2cvQHjWJ2EG0s0MtdNwInarqCTz35i4wWXHsQ==} - '@emnapi/runtime@1.6.0': resolution: {integrity: sha512-obtUmAHTMjll499P+D9A3axeJFlhdjOWdKUNs/U6QIGT7V5RjcUW1xToAzjvmgTSQhDbYn/NwfTRoJcQ2rNBxA==} @@ -2250,6 +2253,11 @@ packages: '@shikijs/vscode-textmate@10.0.2': resolution: {integrity: sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==} + '@shuding/opentype.js@1.4.0-beta.0': + resolution: {integrity: sha512-3NgmNyH3l/Hv6EvsWJbsvpcpUba6R8IREQ83nH83cyakCw7uM1arZKNfHwv1Wz6jgqrF/j4x5ELvR6PnK9nTcA==} + engines: {node: '>= 8.0.0'} + hasBin: true + '@simov/deep-extend@1.0.0': resolution: {integrity: sha512-Arv8/ZPcdKAMJnNF8cks35mPq1y3JnwH1lWpfWDKlJoj+Vw2xmA4+oL7m9GVHTgdX0mGFR7bCPTBTGbxhnfJJw==} engines: {node: '>=4.0.0'} @@ -3197,6 +3205,10 @@ packages: base-64@1.0.0: resolution: {integrity: sha512-kwDPIFCGx0NZHog36dj+tHiwP4QMzsZ3AgMViUBKI0+V5n4U0ufTCUMhnQ04diaRI8EX/QcPfql7zlhZ7j4zgg==} + base64-js@0.0.8: + resolution: {integrity: sha512-3XSA2cR/h/73EzlXXdU6YNycmYI7+kicTxks4eJg2g39biHR84slg2+des+p7iHYhbRg/udIS4TD53WabcOUkw==} + engines: {node: '>= 0.4'} + base64-js@1.5.1: resolution: {integrity: sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==} @@ -3779,10 +3791,20 @@ packages: resolution: {integrity: sha512-v1plID3y9r/lPhviJ1wrXpLeyUIGAZ2SHNYTEapm7/8A9nLPoyvVp3RK/EPFqn5kEznyWgYZNsRtYYIWbuG8KA==} engines: {node: '>=8'} + css-background-parser@0.1.0: + resolution: {integrity: sha512-2EZLisiZQ+7m4wwur/qiYJRniHX4K5Tc9w93MT3AS0WS1u5kaZ4FKXlOTBhOjc+CgEgPiGY+fX1yWD8UwpEqUA==} + + css-box-shadow@1.0.0-3: + resolution: {integrity: sha512-9jaqR6e7Ohds+aWwmhe6wILJ99xYQbfmK9QQB9CcMjDbTxPZjwEmUQpU91OG05Xgm8BahT5fW+svbsQGjS/zPg==} + css-color-keywords@1.0.0: resolution: {integrity: sha512-FyyrDHZKEjXDpNJYvVsV960FiqQyXc/LlYmsxl2BcdMb2WPx0OGRVgTg55rPSyLSNMqP52R9r8geSp7apN3Ofg==} engines: {node: '>=4'} + css-gradient-parser@0.0.17: + resolution: {integrity: sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==} + engines: {node: '>=16'} + css-loader@6.11.0: resolution: {integrity: sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==} engines: {node: '>= 12.13.0'} @@ -4126,6 +4148,10 @@ packages: resolution: {integrity: sha512-DeWwawk6r5yR9jFgnDKYt4sLS0LmHJJi3ZOnb5/JdbYwj3nW+FxQnHIjhBKz8YLC7oRNPVM9NQ47I3CVx34eqQ==} engines: {node: '>=12'} + emoji-regex-xs@2.0.1: + resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} + engines: {node: '>=10.0.0'} + emoji-regex@10.4.0: resolution: {integrity: sha512-EC+0oUMY1Rqm4O6LLrgjtYDvcVYTy7chDnM4Q7030tP4Kwj3u/pR6gP9ygnp2CJMK5Gq+9Q2oqmrFJAz01DXjw==} @@ -4393,6 +4419,9 @@ packages: fecha@4.2.3: resolution: {integrity: sha512-OP2IUU6HeYKJi3i0z4A19kHMQoLVs4Hc+DPqqxI2h/DPZHTm/vjsfC6P0b4jCMy14XizLBqvndQ+UilD7707Jw==} + fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + figures@3.2.0: resolution: {integrity: sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==} engines: {node: '>=8'} @@ -4833,6 +4862,10 @@ packages: resolution: {integrity: sha512-DWlwuXLLqbrIOltR6tFQXShj/+7Cyp0gLi6uAb8qMdFh/YBBFbKSgQ6nbXmScYd8emMctuthmgIa7tUfo9Rtyg==} engines: {node: '>=14.0.0'} + hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + highlight.js@10.7.3: resolution: {integrity: sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==} @@ -5525,6 +5558,9 @@ packages: limiter@1.1.5: resolution: {integrity: sha512-FWWMIEOxz3GwUI4Ts/IvgVy6LPvoMPgjMdQ185nN6psJyBJ4yOpzqm695/h5umdLJg2vW3GR5iG11MAkR2AzJA==} + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -6335,6 +6371,9 @@ packages: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} + parse-css-color@0.2.1: + resolution: {integrity: sha512-bwS/GGIFV3b6KS4uwpzCFj4w297Yl3uqnSgIPsoQkx7GMLROXfMnWvxfNkL0oh8HVhZA4hvJoEoEIqonfJ3BWg==} + parse-entities@4.0.2: resolution: {integrity: sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==} @@ -7272,6 +7311,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + satori@0.18.3: + resolution: {integrity: sha512-T3DzWNmnrfVmk2gCIlAxLRLbGkfp3K7TyRva+Byyojqu83BNvnMeqVeYRdmUw4TKCsyH4RiQ/KuF/I4yEzgR5A==} + engines: {node: '>=16'} + sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} @@ -7608,6 +7651,9 @@ packages: resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} engines: {node: '>=18'} + string.prototype.codepointat@0.2.1: + resolution: {integrity: sha512-2cBVCj6I4IOvEnjgO/hWqXjqBGsY+zwPmHl12Srk9IXSZ56Jwwmy+66XO5Iut/oQVR7t5ihYdLB0GMa4alEUcg==} + string_decoder@0.10.31: resolution: {integrity: sha512-ev2QzSzWPYmy9GuqfIVildA4OdcGLeFZQrq5ys6RtiuF+RQQiZWr8TZNyAcuVXyQRYfEO+MsoB/1BuQVhOJuoQ==} @@ -8583,6 +8629,9 @@ packages: resolution: {integrity: sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==} engines: {node: '>=18'} + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + yup@0.32.9: resolution: {integrity: sha512-Ci1qN+i2H0XpY7syDQ0k5zKQ/DoxO0LzPg8PAR/X4Mpj6DqaeCoIYEEjDJwhArh3Fa7GWbQQVDZKeXYlSH4JMg==} engines: {node: '>=10'} @@ -9100,11 +9149,6 @@ snapshots: react: 18.3.1 tslib: 2.8.1 - '@emnapi/runtime@1.5.0': - dependencies: - tslib: 2.8.1 - optional: true - '@emnapi/runtime@1.6.0': dependencies: tslib: 2.8.1 @@ -9693,7 +9737,7 @@ snapshots: '@img/sharp-wasm32@0.34.4': dependencies: - '@emnapi/runtime': 1.5.0 + '@emnapi/runtime': 1.6.0 optional: true '@img/sharp-win32-arm64@0.34.4': @@ -10680,6 +10724,11 @@ snapshots: '@shikijs/vscode-textmate@10.0.2': {} + '@shuding/opentype.js@1.4.0-beta.0': + dependencies: + fflate: 0.7.4 + string.prototype.codepointat: 0.2.1 + '@simov/deep-extend@1.0.0': {} '@sindresorhus/is@4.6.0': {} @@ -12673,6 +12722,8 @@ snapshots: base-64@1.0.0: {} + base64-js@0.0.8: {} + base64-js@1.5.1: {} baseline-browser-mapping@2.8.19: {} @@ -13266,8 +13317,14 @@ snapshots: crypto-random-string@2.0.0: {} + css-background-parser@0.1.0: {} + + css-box-shadow@1.0.0-3: {} + css-color-keywords@1.0.0: {} + css-gradient-parser@0.0.17: {} + css-loader@6.11.0(webpack@5.102.1(esbuild@0.25.11)): dependencies: icss-utils: 5.1.0(postcss@8.5.6) @@ -13614,6 +13671,8 @@ snapshots: emittery@0.13.1: {} + emoji-regex-xs@2.0.1: {} + emoji-regex@10.4.0: {} emoji-regex@8.0.0: {} @@ -13972,6 +14031,8 @@ snapshots: fecha@4.2.3: {} + fflate@0.7.4: {} + figures@3.2.0: dependencies: escape-string-regexp: 1.0.5 @@ -14589,6 +14650,8 @@ snapshots: helmet@6.2.0: {} + hex-rgb@4.3.0: {} + highlight.js@10.7.3: {} hls.js@1.5.20: {} @@ -15325,6 +15388,11 @@ snapshots: limiter@1.1.5: {} + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + lines-and-columns@1.2.4: {} linkify-it@4.0.1: @@ -16321,6 +16389,11 @@ snapshots: dependencies: callsites: 3.1.0 + parse-css-color@0.2.1: + dependencies: + color-name: 1.1.4 + hex-rgb: 4.3.0 + parse-entities@4.0.2: dependencies: '@types/unist': 2.0.11 @@ -17348,6 +17421,20 @@ snapshots: '@parcel/watcher': 2.5.1 optional: true + satori@0.18.3: + 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.2.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 + sax@1.4.1: {} scheduler@0.23.0: @@ -17822,6 +17909,8 @@ snapshots: get-east-asian-width: 1.3.0 strip-ansi: 7.1.2 + string.prototype.codepointat@0.2.1: {} + string_decoder@0.10.31: {} string_decoder@1.1.1: @@ -18676,6 +18765,8 @@ snapshots: yoctocolors@2.1.2: {} + yoga-layout@3.2.1: {} + yup@0.32.9: dependencies: '@babel/runtime': 7.28.4