From 1e3264c07e847784a76a96c26ef12b3725730507 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=B0=8F=E6=A9=98?= Date: Mon, 6 Apr 2026 13:14:15 +0000 Subject: [PATCH] feat: auto-generate OG images for social sharing (WeChat/XHS) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - satori + sharp generates 1200ร—630 PNG per post at build time - Blue gradient background matching sky-blue brand theme - Chinese font: Noto Sans SC (static, downloaded at build time) - Layout.astro now emits og:image + twitter:image meta tags - scripts/download-fonts.sh for CI font provisioning Closes the social sharing card gap โ€” links shared to WeChat/XHS now display a proper card with title, description, and branding. --- .gitignore | 3 + package.json | 3 +- pnpm-lock.yaml | 121 +++++++++++++++ scripts/download-fonts.sh | 18 +++ src/layouts/Layout.astro | 5 +- src/layouts/MainGridLayout.astro | 4 +- src/pages/og/[...slug].png.ts | 246 +++++++++++++++++++++++++++++++ src/pages/posts/[...slug].astro | 2 +- 8 files changed, 398 insertions(+), 4 deletions(-) create mode 100755 scripts/download-fonts.sh create mode 100644 src/pages/og/[...slug].png.ts diff --git a/.gitignore b/.gitignore index b2b378e..12fb8b2 100644 --- a/.gitignore +++ b/.gitignore @@ -29,3 +29,6 @@ yarn.lock # ide .idea *.iml + +# OG image fonts (downloaded at build time) +src/assets/fonts/*.ttf diff --git a/package.json b/package.json index 79c0edf..a9fd40c 100644 --- a/package.json +++ b/package.json @@ -6,7 +6,7 @@ "dev": "astro dev", "start": "astro dev", "check": "astro check", - "build": "astro build && pagefind --site dist", + "build": "bash scripts/download-fonts.sh && astro build && pagefind --site dist", "preview": "astro preview", "astro": "astro", "type-check": "tsc --noEmit --isolatedDeclarations", @@ -54,6 +54,7 @@ "remark-math": "^6.0.0", "remark-sectionize": "^2.1.0", "sanitize-html": "^2.17.0", + "satori": "^0.26.0", "sharp": "^0.34.5", "stylus": "^0.64.0", "svelte": "^5.39.8", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index a958251..057523f 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -122,6 +122,9 @@ importers: sanitize-html: specifier: ^2.17.0 version: 2.17.0 + satori: + specifier: ^0.26.0 + version: 0.26.0 sharp: specifier: ^0.34.5 version: 0.34.5 @@ -1901,6 +1904,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 + '@surma/rollup-plugin-off-main-thread@2.2.3': resolution: {integrity: sha512-lR8q/9W7hZpMWweNiAKU7NQerBnzQQLvi8qnTDU/fxItPhtZVMbPV3lbCwjhIlNBe9Bbr5V+KHshvWmVSG9cxQ==} @@ -2296,6 +2304,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==} @@ -2384,6 +2396,9 @@ packages: resolution: {integrity: sha512-8WB3Jcas3swSvjIeA2yvCJ+Miyz5l1ZmB6HFb9R1317dt9LCQoswg/BGrmAmkWVEszSrrg4RwmO46qIm2OEnSA==} engines: {node: '>=16'} + camelize@1.0.1: + resolution: {integrity: sha512-dU+Tx2fsypxTgtLoE36npi3UqcjSSMNYfkqgmoEhtZrraP5VWq0K7FkWVTYa8eMPtnU/G2txVsfdCJTn9uzpuQ==} + caniuse-api@3.0.0: resolution: {integrity: sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==} @@ -2536,12 +2551,26 @@ packages: crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + 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-declaration-sorter@6.4.1: resolution: {integrity: sha512-rtdthzxKuyq6IzqX6jEcIzQF/YqccluefyCYheovBOLhFT/drQA9zj/UbRAa9J7C0o6EG6u3E6g+vKkay7/k3g==} engines: {node: ^10 || ^12 || >=14} peerDependencies: postcss: ^8.0.9 + css-gradient-parser@0.0.17: + resolution: {integrity: sha512-w2Xy9UMMwlKtou0vlRnXvWglPAceXCTtcmVSo8ZBUvqCV5aXEFP/PC6d+I464810I9FT++UACwTD5511bmGPUg==} + engines: {node: '>=16'} + css-select@4.3.0: resolution: {integrity: sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==} @@ -2551,6 +2580,9 @@ packages: css-selector-parser@3.2.0: resolution: {integrity: sha512-L1bdkNKUP5WYxiW5dW6vA2hd3sL8BdRNLy2FCX0rLVise4eNw9nBdeBuJHxlELieSE2H1f6bYQFfwVUwWCV9rQ==} + css-to-react-native@3.2.0: + resolution: {integrity: sha512-e8RKaLXMOFii+02mOlqwjbD00KSEKqblnpO9e++1aXS1fPQOpS1YoqdVHBqPjHNoxeF2mimzVqawm2KCbEdtHQ==} + css-tree@1.1.3: resolution: {integrity: sha512-tRpdppF7TRazZrjJ6v3stzv93qxRcSsFmW6cX0Zm2NVKpxE1WV1HblnghVv9TreireHkqI/VDEsfolRF1p6y7Q==} engines: {node: '>=8.0.0'} @@ -2765,6 +2797,10 @@ packages: emmet@2.4.11: resolution: {integrity: sha512-23QPJB3moh/U9sT4rQzGgeyyGIrcM+GH5uVYg2C6wZIxAIJq7Ng3QLT79tl8FUwDXhyq9SusfknOrofAKqvgyQ==} + emoji-regex-xs@2.0.1: + resolution: {integrity: sha512-1QFuh8l7LqUcKe24LsPUNzjrzJQ7pgRwp1QMcZ5MX6mFplk2zQ08NVCM84++1cveaUUYtcCYHmeFEuNg16sU4g==} + engines: {node: '>=10.0.0'} + emoji-regex@10.5.0: resolution: {integrity: sha512-lb49vf1Xzfx080OKA0o6l8DQQpV+6Vg95zyCJX9VB/BqKYlhG7N4wgROUUHRA+ZPUefLnteQOad7z1kT2bV7bg==} @@ -2835,6 +2871,9 @@ packages: resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} engines: {node: '>=6'} + escape-html@1.0.3: + resolution: {integrity: sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==} + escape-string-regexp@1.0.5: resolution: {integrity: sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==} engines: {node: '>=0.8.0'} @@ -2918,6 +2957,9 @@ packages: picomatch: optional: true + fflate@0.7.4: + resolution: {integrity: sha512-5u2V/CDW15QM1XbbgS+0DfPxVB+jUKhWEKuuFuHncbk3tEEqzmoXL+2KyOFuKGqOnmdIy0/davWF1CkuwtibCw==} + figures@1.7.0: resolution: {integrity: sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==} engines: {node: '>=0.10.0'} @@ -3171,6 +3213,10 @@ packages: hastscript@9.0.1: resolution: {integrity: sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==} + hex-rgb@4.3.0: + resolution: {integrity: sha512-Ox1pJVrDCyGHMG9CFg1tmrRUMRPRsAWYc/PinY0XzJU4K7y7vjNoLKIQ7BR5UJMCxNN8EM1MNDmHWA/B3aZUuw==} + engines: {node: '>=6'} + html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -3544,6 +3590,9 @@ packages: resolution: {integrity: sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==} engines: {node: '>=14'} + linebreak@1.1.0: + resolution: {integrity: sha512-MHp03UImeVhB7XZtjd0E4n6+3xr5Dq/9xI/5FptGk5FrbDR3zagPa2DS6U8ks/3HjbKWG9Q1M2ufOzxV2qLYSQ==} + lines-and-columns@1.2.4: resolution: {integrity: sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==} @@ -4020,6 +4069,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==} @@ -4731,6 +4783,10 @@ packages: engines: {node: '>=14.0.0'} hasBin: true + satori@0.26.0: + resolution: {integrity: sha512-tkMFrfIs3l2mQ2JEcyW0ADTy3zGggFRFzi6Ef8YozQSFsFKEqaSO1Y8F9wJg4//PJGQauMalHGTUEkPrFwhVPA==} + engines: {node: '>=16'} + sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} @@ -4882,6 +4938,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.prototype.matchall@4.0.12: resolution: {integrity: sha512-6CC9uyBL+/48dYizRf7H7VAYCMCNTBeM78x/VTUe9bFEaxBepPJDa1Ow99LqI/1yF7kuy7Q3cQsYMrcjGUcskA==} engines: {node: '>= 0.4'} @@ -5564,6 +5623,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==} + zimmerframe@1.1.4: resolution: {integrity: sha512-B58NGBEoc8Y9MWWCQGl/gq9xBCe4IiKM0a2x7GZdQKOW5Exr8S1W24J6OgM1njK8xCRGvAJIL/MxXHf6SkmQKQ==} @@ -7312,6 +7374,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 + '@surma/rollup-plugin-off-main-thread@2.2.3': dependencies: ejs: 3.1.10 @@ -7915,6 +7982,8 @@ snapshots: base-64@1.0.0: {} + base64-js@0.0.8: {} + base64-js@1.5.1: {} bcp-47-match@2.0.3: {} @@ -8006,6 +8075,8 @@ snapshots: camelcase@8.0.0: {} + camelize@1.0.1: {} + caniuse-api@3.0.0: dependencies: browserslist: 4.25.1 @@ -8167,10 +8238,18 @@ snapshots: dependencies: uncrypto: 0.1.3 + css-background-parser@0.1.0: {} + + css-box-shadow@1.0.0-3: {} + + css-color-keywords@1.0.0: {} + css-declaration-sorter@6.4.1(postcss@8.5.6): dependencies: postcss: 8.5.6 + css-gradient-parser@0.0.17: {} + css-select@4.3.0: dependencies: boolbase: 1.0.0 @@ -8189,6 +8268,12 @@ snapshots: css-selector-parser@3.2.0: {} + css-to-react-native@3.2.0: + dependencies: + camelize: 1.0.1 + css-color-keywords: 1.0.0 + postcss-value-parser: 4.2.0 + css-tree@1.1.3: dependencies: mdn-data: 2.0.14 @@ -8413,6 +8498,8 @@ snapshots: '@emmetio/abbreviation': 2.3.3 '@emmetio/css-abbreviation': 2.1.8 + emoji-regex-xs@2.0.1: {} + emoji-regex@10.5.0: {} emoji-regex@8.0.0: {} @@ -8578,6 +8665,8 @@ snapshots: escalade@3.2.0: {} + escape-html@1.0.3: {} + escape-string-regexp@1.0.5: {} escape-string-regexp@4.0.0: {} @@ -8655,6 +8744,8 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fflate@0.7.4: {} + figures@1.7.0: dependencies: escape-string-regexp: 1.0.5 @@ -9019,6 +9110,8 @@ snapshots: property-information: 7.0.0 space-separated-tokens: 2.0.2 + hex-rgb@4.3.0: {} + html-escaper@3.0.3: {} html-void-elements@3.0.0: {} @@ -9351,6 +9444,11 @@ snapshots: lilconfig@3.1.3: {} + linebreak@1.1.0: + dependencies: + base64-js: 0.0.8 + unicode-trie: 2.0.0 + lines-and-columns@1.2.4: {} linkify-it@5.0.0: @@ -10069,6 +10167,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 @@ -10913,6 +11016,20 @@ snapshots: source-map-js: 1.2.1 optional: true + satori@0.26.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.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: {} scrl@2.0.0: {} @@ -11108,6 +11225,8 @@ snapshots: get-east-asian-width: 1.4.0 strip-ansi: 7.1.2 + string.prototype.codepointat@0.2.1: {} + string.prototype.matchall@4.0.12: dependencies: call-bind: 1.0.8 @@ -11847,6 +11966,8 @@ snapshots: yoctocolors@2.1.2: {} + yoga-layout@3.2.1: {} + zimmerframe@1.1.4: {} zod-to-json-schema@3.24.6(zod@3.25.76): diff --git a/scripts/download-fonts.sh b/scripts/download-fonts.sh new file mode 100755 index 0000000..bb395d5 --- /dev/null +++ b/scripts/download-fonts.sh @@ -0,0 +1,18 @@ +#!/usr/bin/env bash +# Download fonts needed for OG image generation +set -e + +FONT_DIR="src/assets/fonts" +FONT_FILE="$FONT_DIR/NotoSansSC-Regular.ttf" + +if [ -f "$FONT_FILE" ] && [ "$(wc -c < "$FONT_FILE")" -gt 10000 ]; then + echo "โœ… Font already exists: $FONT_FILE ($(du -h "$FONT_FILE" | cut -f1))" + exit 0 +fi + +echo "๐Ÿ“ฅ Downloading Noto Sans SC (static, ~2.5MB)..." +mkdir -p "$FONT_DIR" +curl -L -o "$FONT_FILE" \ + "https://cdn.jsdelivr.net/fontsource/fonts/noto-sans-sc@latest/chinese-simplified-400-normal.ttf" + +echo "โœ… Font downloaded: $(du -h "$FONT_FILE" | cut -f1)" diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index c180522..41e95e6 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -26,9 +26,10 @@ interface Props { description?: string; lang?: string; setOGTypeArticle?: boolean; + ogImage?: string; } -let { title, banner, description, lang, setOGTypeArticle } = Astro.props; +let { title, banner, description, lang, setOGTypeArticle, ogImage } = Astro.props; // apply a class to the body element to decide the height of the banner, only used for initial page load // Swup can update the body for each page visit, but it's after the page transition, causing a delay for banner height change @@ -93,11 +94,13 @@ const bannerOffset = ) : ( )} + {ogImage && } + {ogImage && } diff --git a/src/layouts/MainGridLayout.astro b/src/layouts/MainGridLayout.astro index e1f8e5c..1a480a5 100644 --- a/src/layouts/MainGridLayout.astro +++ b/src/layouts/MainGridLayout.astro @@ -22,6 +22,7 @@ interface Props { lang?: string; setOGTypeArticle?: boolean; headings?: MarkdownHeading[]; + ogImage?: string; } const { @@ -31,6 +32,7 @@ const { lang, setOGTypeArticle, headings = [], + ogImage, } = Astro.props; const hasBannerCredit = siteConfig.banner.enable && siteConfig.banner.credit.enable; @@ -41,7 +43,7 @@ const mainPanelTop = siteConfig.banner.enable : "5.5rem"; --- - + diff --git a/src/pages/og/[...slug].png.ts b/src/pages/og/[...slug].png.ts new file mode 100644 index 0000000..c57c35c --- /dev/null +++ b/src/pages/og/[...slug].png.ts @@ -0,0 +1,246 @@ +import type { APIRoute, GetStaticPaths } from "astro"; +import { getSortedPosts } from "@utils/content-utils"; +import satori from "satori"; +import sharp from "sharp"; +import fs from "node:fs"; +import path from "node:path"; +import { formatDateToYYYYMMDD } from "../../utils/date-utils"; + +// Load font at module level (cached across calls during build) +const fontPath = path.resolve("src/assets/fonts/NotoSansSC-Regular.ttf"); +let fontData: ArrayBuffer; +try { + fontData = fs.readFileSync(fontPath).buffer as ArrayBuffer; +} catch { + // Font will be downloaded by build script; fail gracefully if missing + fontData = new ArrayBuffer(0); +} + +export const getStaticPaths: GetStaticPaths = async () => { + const posts = await getSortedPosts(); + return posts.map((post) => ({ + params: { slug: post.slug }, + props: { + title: post.data.title, + description: post.data.description || "", + date: formatDateToYYYYMMDD(post.data.published), + tags: post.data.tags || [], + }, + })); +}; + +export const GET: APIRoute = async ({ props }) => { + const { title, description, date, tags } = props as { + title: string; + description: string; + date: string; + tags: string[]; + }; + + // Truncate title to ~40 chars for display + const displayTitle = + title.length > 42 ? title.slice(0, 40) + "โ€ฆ" : title; + const displayDesc = + description.length > 70 ? description.slice(0, 68) + "โ€ฆ" : description; + const displayTags = tags.slice(0, 3).join(" ยท "); + + const svg = await satori( + { + type: "div", + props: { + style: { + width: "100%", + height: "100%", + display: "flex", + flexDirection: "column", + justifyContent: "space-between", + padding: "60px 64px", + background: "linear-gradient(135deg, #0f172a 0%, #1e3a5f 40%, #3b82f6 100%)", + color: "#ffffff", + fontFamily: "Noto Sans SC", + }, + children: [ + // Top: logo + branding + { + type: "div", + props: { + style: { + display: "flex", + alignItems: "center", + gap: "12px", + }, + children: [ + { + type: "span", + props: { + style: { fontSize: "36px" }, + children: "๐ŸŠ", + }, + }, + { + type: "span", + props: { + style: { + fontSize: "22px", + color: "rgba(255,255,255,0.7)", + letterSpacing: "0.05em", + }, + children: "ๅฐๆฉ˜็š„ๆ—ฅ่ฎฐ", + }, + }, + ], + }, + }, + // Middle: title + description + { + type: "div", + props: { + style: { + display: "flex", + flexDirection: "column", + gap: "16px", + flex: "1", + justifyContent: "center", + }, + children: [ + { + type: "div", + props: { + style: { + fontSize: "48px", + fontWeight: 700, + lineHeight: 1.3, + letterSpacing: "-0.02em", + textShadow: "0 2px 10px rgba(0,0,0,0.3)", + }, + children: displayTitle, + }, + }, + description + ? { + type: "div", + props: { + style: { + fontSize: "22px", + color: "rgba(255,255,255,0.65)", + lineHeight: 1.5, + }, + children: displayDesc, + }, + } + : null, + ].filter(Boolean), + }, + }, + // Bottom: date + tags + site + { + type: "div", + props: { + style: { + display: "flex", + justifyContent: "space-between", + alignItems: "flex-end", + }, + children: [ + { + type: "div", + props: { + style: { + display: "flex", + flexDirection: "column", + gap: "6px", + }, + children: [ + displayTags + ? { + type: "div", + props: { + style: { + fontSize: "16px", + color: "rgba(255,255,255,0.5)", + }, + children: displayTags, + }, + } + : null, + { + type: "div", + props: { + style: { + fontSize: "18px", + color: "rgba(255,255,255,0.6)", + }, + children: date, + }, + }, + ].filter(Boolean), + }, + }, + { + type: "div", + props: { + style: { + display: "flex", + alignItems: "center", + gap: "8px", + }, + children: [ + { + type: "span", + props: { + style: { + fontSize: "24px", + }, + children: "โœจ ๐ŸŒ™ โ˜๏ธ", + }, + }, + { + type: "span", + props: { + style: { + fontSize: "16px", + color: "rgba(255,255,255,0.4)", + }, + children: "oc-xiaoju.github.io", + }, + }, + ], + }, + }, + ], + }, + }, + ], + }, + }, + { + width: 1200, + height: 630, + fonts: fontData.byteLength > 0 + ? [ + { + name: "Noto Sans SC", + data: fontData, + weight: 400 as const, + style: "normal" as const, + }, + { + name: "Noto Sans SC", + data: fontData, // variable font covers all weights + weight: 700 as const, + style: "normal" as const, + }, + ] + : [], + } + ); + + const png = await sharp(Buffer.from(svg)).png().toBuffer(); + + return new Response(png, { + headers: { + "Content-Type": "image/png", + "Cache-Control": "public, max-age=31536000, immutable", + }, + }); +}; diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro index 7fdfcb8..eea30cc 100644 --- a/src/pages/posts/[...slug].astro +++ b/src/pages/posts/[...slug].astro @@ -45,7 +45,7 @@ const jsonLd = { // TODO include cover image here }; --- - +