feat: auto-generate OG images for social sharing (WeChat/XHS)
- 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.
This commit is contained in:
parent
26afab1d42
commit
1e3264c07e
3
.gitignore
vendored
3
.gitignore
vendored
@ -29,3 +29,6 @@ yarn.lock
|
||||
# ide
|
||||
.idea
|
||||
*.iml
|
||||
|
||||
# OG image fonts (downloaded at build time)
|
||||
src/assets/fonts/*.ttf
|
||||
|
||||
@ -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",
|
||||
|
||||
121
pnpm-lock.yaml
generated
121
pnpm-lock.yaml
generated
@ -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):
|
||||
|
||||
18
scripts/download-fonts.sh
Executable file
18
scripts/download-fonts.sh
Executable file
@ -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)"
|
||||
@ -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 =
|
||||
) : (
|
||||
<meta property="og:type" content="website" />
|
||||
)}
|
||||
{ogImage && <meta property="og:image" content={new URL(ogImage, Astro.site || 'https://oc-xiaoju.github.io').href} />}
|
||||
|
||||
<meta name="twitter:card" content="summary_large_image">
|
||||
<meta property="twitter:url" content={Astro.url}>
|
||||
<meta name="twitter:title" content={pageTitle}>
|
||||
<meta name="twitter:description" content={description || pageTitle}>
|
||||
{ogImage && <meta name="twitter:image" content={new URL(ogImage, Astro.site || 'https://oc-xiaoju.github.io').href} />}
|
||||
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<meta name="generator" content={Astro.generator} />
|
||||
|
||||
@ -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";
|
||||
---
|
||||
|
||||
<Layout title={title} banner={banner} description={description} lang={lang} setOGTypeArticle={setOGTypeArticle}>
|
||||
<Layout title={title} banner={banner} description={description} lang={lang} setOGTypeArticle={setOGTypeArticle} ogImage={ogImage}>
|
||||
|
||||
<!-- Navbar -->
|
||||
<slot slot="head" name="head"></slot>
|
||||
|
||||
246
src/pages/og/[...slug].png.ts
Normal file
246
src/pages/og/[...slug].png.ts
Normal file
@ -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",
|
||||
},
|
||||
});
|
||||
};
|
||||
@ -45,7 +45,7 @@ const jsonLd = {
|
||||
// TODO include cover image here
|
||||
};
|
||||
---
|
||||
<MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description} lang={entry.data.lang} setOGTypeArticle={true} headings={headings}>
|
||||
<MainGridLayout banner={entry.data.image} title={entry.data.title} description={entry.data.description} lang={entry.data.lang} setOGTypeArticle={true} headings={headings} ogImage={`/og/${entry.slug}.png`}>
|
||||
<script is:inline slot="head" type="application/ld+json" set:html={JSON.stringify(jsonLd)}></script>
|
||||
<div class="flex w-full rounded-[var(--radius-large)] overflow-hidden relative mb-4">
|
||||
<div id="post-container" class:list={["card-base z-10 px-6 md:px-9 pt-6 pb-4 relative w-full ",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user