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