diff --git a/package.json b/package.json
index b39f859..bb197cd 100644
--- a/package.json
+++ b/package.json
@@ -45,6 +45,7 @@
"mdast-util-to-string": "^4.0.0",
"overlayscrollbars": "^2.16.0",
"pagefind": "^1.5.2",
+ "qrcode": "^1.5.4",
"reading-time": "^1.5.0",
"rehype-autolink-headings": "^7.1.0",
"rehype-components": "^0.3.0",
@@ -68,6 +69,7 @@
"@rollup/plugin-yaml": "^5.0.0",
"@types/hast": "^3.0.5",
"@types/mdast": "^4.0.4",
+ "@types/qrcode": "^1.5.6",
"@types/sanitize-html": "^2.16.1"
},
"packageManager": "pnpm@9.14.4"
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8789fc3..eb47823 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -95,6 +95,9 @@ importers:
pagefind:
specifier: ^1.5.2
version: 1.5.2
+ qrcode:
+ specifier: ^1.5.4
+ version: 1.5.4
reading-time:
specifier: ^1.5.0
version: 1.5.0
@@ -159,6 +162,9 @@ importers:
'@types/mdast':
specifier: ^4.0.4
version: 4.0.4
+ '@types/qrcode':
+ specifier: ^1.5.6
+ version: 1.5.6
'@types/sanitize-html':
specifier: ^2.16.1
version: 2.16.1
@@ -1954,6 +1960,9 @@ packages:
'@types/parse-json@4.0.2':
resolution: {integrity: sha512-dISoDXWWQwUquiKsyZ4Ng+HX2KsPL7LyHKHQwgGFEA3IaKac4Obd+h2a/a6waisAoepJlBcx9paWqjA8/HVjCw==}
+ '@types/qrcode@1.5.6':
+ resolution: {integrity: sha512-te7NQcV2BOvdj2b1hCAHzAoMNuj65kNBMz0KBaxM6c3VGBOhU0dURQKOtH8CFNI/dsKkwlv32p26qYQTWoB5bw==}
+
'@types/resolve@1.17.1':
resolution: {integrity: sha512-yy7HuzQhj0dhGpD8RLXSZWEkLsV9ibvxvi6EiJ3bkqLAO1RGo0WbkWQiwpRlSFymTJRz0d3k5LM3kkx8ArDbLw==}
@@ -2208,6 +2217,10 @@ packages:
resolution: {integrity: sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==}
engines: {node: '>=6'}
+ camelcase@5.3.1:
+ resolution: {integrity: sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==}
+ engines: {node: '>=6'}
+
camelcase@6.3.0:
resolution: {integrity: sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==}
engines: {node: '>=10'}
@@ -2257,6 +2270,9 @@ packages:
resolution: {integrity: sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg==}
engines: {node: '>=8'}
+ cliui@6.0.0:
+ resolution: {integrity: sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==}
+
cliui@8.0.1:
resolution: {integrity: sha512-BSeNnyus75C4//NQ9gQt1/csTXyo/8Sb+afLAkzAptFuMsod9HFokGNudZpi/oQV73hnVK+sR+5PVRMd+Dr7YQ==}
engines: {node: '>=12'}
@@ -2424,6 +2440,10 @@ packages:
supports-color:
optional: true
+ decamelize@1.2.0:
+ resolution: {integrity: sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==}
+ engines: {node: '>=0.10.0'}
+
decode-named-character-reference@1.3.0:
resolution: {integrity: sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==}
@@ -2473,6 +2493,9 @@ packages:
resolution: {integrity: sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==}
engines: {node: '>=0.3.1'}
+ dijkstrajs@1.0.3:
+ resolution: {integrity: sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==}
+
direction@2.0.1:
resolution: {integrity: sha512-9S6m9Sukh1cZNknO1CWAr2QAWsbKLafQiyM5gZ7VgXHeuaoUwffKN4q6NC4A/Mf9iiPlOXQEKW/Mv/mh9/3YFA==}
hasBin: true
@@ -3818,6 +3841,10 @@ packages:
resolution: {integrity: sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==}
engines: {node: '>=8'}
+ pngjs@5.0.0:
+ resolution: {integrity: sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==}
+ engines: {node: '>=10.13.0'}
+
possible-typed-array-names@1.1.0:
resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==}
engines: {node: '>= 0.4'}
@@ -4078,6 +4105,11 @@ packages:
property-information@7.2.0:
resolution: {integrity: sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==}
+ qrcode@1.5.4:
+ resolution: {integrity: sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==}
+ engines: {node: '>=10.13.0'}
+ hasBin: true
+
radix3@1.1.2:
resolution: {integrity: sha512-b484I/7b8rDEdSDKckSSBA8knMpcdsXudlE/LNL639wFoHKwLbEkQFZHWEYwDC0wa0FKUcCY+GAF73Z7wxNVFA==}
@@ -4224,6 +4256,9 @@ packages:
resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==}
engines: {node: '>=0.10.0'}
+ require-main-filename@2.0.0:
+ resolution: {integrity: sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==}
+
resolve-from@4.0.0:
resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==}
engines: {node: '>=4'}
@@ -4351,6 +4386,9 @@ packages:
serialize-javascript@4.0.0:
resolution: {integrity: sha512-GaNA54380uFefWghODBWEGisLZFj00nS5ACs6yHa9nLqlLpVLO8ChDGeKRjZnV4Nh4n0Qi7nhYZD/9fCPzEqkw==}
+ set-blocking@2.0.0:
+ resolution: {integrity: sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==}
+
set-function-length@1.2.2:
resolution: {integrity: sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==}
engines: {node: '>= 0.4'}
@@ -4992,6 +5030,9 @@ packages:
resolution: {integrity: sha512-K4jVyjnBdgvc86Y6BkaLZEN933SwYOuBFkdmBu9ZfkcAbdVbpITnDmjvZ/aQjRXQrv5EPkTnD1s39GiiqbngCw==}
engines: {node: '>= 0.4'}
+ which-module@2.0.1:
+ resolution: {integrity: sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==}
+
which-pm-runs@1.1.0:
resolution: {integrity: sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==}
engines: {node: '>=4'}
@@ -5005,6 +5046,10 @@ packages:
engines: {node: '>= 8'}
hasBin: true
+ wrap-ansi@6.2.0:
+ resolution: {integrity: sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==}
+ engines: {node: '>=8'}
+
wrap-ansi@7.0.0:
resolution: {integrity: sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==}
engines: {node: '>=10'}
@@ -5027,6 +5072,9 @@ packages:
xxhash-wasm@1.1.0:
resolution: {integrity: sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==}
+ y18n@4.0.3:
+ resolution: {integrity: sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==}
+
y18n@5.0.8:
resolution: {integrity: sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==}
engines: {node: '>=10'}
@@ -5052,6 +5100,10 @@ packages:
engines: {node: '>= 14.6'}
hasBin: true
+ yargs-parser@18.1.3:
+ resolution: {integrity: sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==}
+ engines: {node: '>=6'}
+
yargs-parser@21.1.1:
resolution: {integrity: sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==}
engines: {node: '>=12'}
@@ -5060,6 +5112,10 @@ packages:
resolution: {integrity: sha512-rwu/ClNdSMpkSrUb+d6BRsSkLUq1fmfsY6TOpYzTwvwkg1/NRG85KBy3kq++A8LKQwX6lsu+aWad+2khvuXrqw==}
engines: {node: ^20.19.0 || ^22.12.0 || >=23}
+ yargs@15.4.1:
+ resolution: {integrity: sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==}
+ engines: {node: '>=8'}
+
yargs@17.7.3:
resolution: {integrity: sha512-GZtjxm/J/4TSxuL3FNYjCmLktBTnIw/rVmKSIyKeYAZpmJB2ig9VauCC5xsa82GNKVKDAqpOn3KVzNt0zmrU0g==}
engines: {node: '>=12'}
@@ -6964,6 +7020,10 @@ snapshots:
'@types/parse-json@4.0.2': {}
+ '@types/qrcode@1.5.6':
+ dependencies:
+ '@types/node': 26.2.0
+
'@types/resolve@1.17.1':
dependencies:
'@types/node': 26.2.0
@@ -7320,6 +7380,8 @@ snapshots:
callsites@3.1.0: {}
+ camelcase@5.3.1: {}
+
camelcase@6.3.0: {}
caniuse-api@3.0.0:
@@ -7366,6 +7428,12 @@ snapshots:
ci-info@4.4.0: {}
+ cliui@6.0.0:
+ dependencies:
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+ wrap-ansi: 6.2.0
+
cliui@8.0.1:
dependencies:
string-width: 4.2.3
@@ -7555,6 +7623,8 @@ snapshots:
dependencies:
ms: 2.1.3
+ decamelize@1.2.0: {}
+
decode-named-character-reference@1.3.0:
dependencies:
character-entities: 2.0.2
@@ -7597,6 +7667,8 @@ snapshots:
diff@8.0.4: {}
+ dijkstrajs@1.0.3: {}
+
direction@2.0.1: {}
dom-serializer@1.4.1:
@@ -9473,6 +9545,8 @@ snapshots:
dependencies:
find-up: 4.1.0
+ pngjs@5.0.0: {}
+
possible-typed-array-names@1.1.0: {}
postcss-calc@8.2.4(postcss@8.5.26):
@@ -9707,6 +9781,12 @@ snapshots:
property-information@7.2.0: {}
+ qrcode@1.5.4:
+ dependencies:
+ dijkstrajs: 1.0.3
+ pngjs: 5.0.0
+ yargs: 15.4.1
+
radix3@1.1.2: {}
randombytes@2.1.0:
@@ -9960,6 +10040,8 @@ snapshots:
require-from-string@2.0.2: {}
+ require-main-filename@2.0.0: {}
+
resolve-from@4.0.0: {}
resolve-from@5.0.0: {}
@@ -10142,6 +10224,8 @@ snapshots:
dependencies:
randombytes: 2.1.0
+ set-blocking@2.0.0: {}
+
set-function-length@1.2.2:
dependencies:
define-data-property: 1.1.4
@@ -10861,6 +10945,8 @@ snapshots:
is-weakmap: 2.0.2
is-weakset: 2.0.4
+ which-module@2.0.1: {}
+
which-pm-runs@1.1.0: {}
which-typed-array@1.1.22:
@@ -10877,6 +10963,12 @@ snapshots:
dependencies:
isexe: 2.0.0
+ wrap-ansi@6.2.0:
+ dependencies:
+ ansi-styles: 4.3.0
+ string-width: 4.2.3
+ strip-ansi: 6.0.1
+
wrap-ansi@7.0.0:
dependencies:
ansi-styles: 4.3.0
@@ -10901,6 +10993,8 @@ snapshots:
xxhash-wasm@1.1.0: {}
+ y18n@4.0.3: {}
+
y18n@5.0.8: {}
yallist@3.1.1: {}
@@ -10926,10 +11020,29 @@ snapshots:
yaml@2.9.0: {}
+ yargs-parser@18.1.3:
+ dependencies:
+ camelcase: 5.3.1
+ decamelize: 1.2.0
+
yargs-parser@21.1.1: {}
yargs-parser@22.0.0: {}
+ yargs@15.4.1:
+ dependencies:
+ cliui: 6.0.0
+ decamelize: 1.2.0
+ find-up: 4.1.0
+ get-caller-file: 2.0.5
+ require-directory: 2.1.1
+ require-main-filename: 2.0.0
+ set-blocking: 2.0.0
+ string-width: 4.2.3
+ which-module: 2.0.1
+ y18n: 4.0.3
+ yargs-parser: 18.1.3
+
yargs@17.7.3:
dependencies:
cliui: 8.0.1
diff --git a/src/components/SocialShare.astro b/src/components/SocialShare.astro
deleted file mode 100644
index 59c9eb6..0000000
--- a/src/components/SocialShare.astro
+++ /dev/null
@@ -1,376 +0,0 @@
----
-import { Icon } from "astro-icon/components";
-import I18nKey from "../i18n/i18nKey";
-import { i18n } from "../i18n/translation";
-
-interface Props {
- title: string;
- url: string;
- description?: string;
- tags?: string[];
- image?: string;
-}
-
-const { title, url, description = "", tags = [], image = "" } = Astro.props;
-
-const pageUrl = new URL(url, Astro.site).href;
-const pageImage = image ? new URL(image, Astro.site).href : "";
-
-// 分享平台配置
-const sharePlatforms = [
- {
- key: "twitter",
- name: "Twitter/X",
- icon: "fa6-brands:x-twitter",
- color: "bg-[#1DA1F2] hover:bg-[#0d8bd9]",
- webUrl: `https://twitter.com/intent/tweet?text=${encodeURIComponent(title)}&url=${encodeURIComponent(pageUrl)}&hashtags=${encodeURIComponent(tags.join(","))}`,
- mobileUrl: `twitter://post?message=${encodeURIComponent(`${title} ${pageUrl}`)}`,
- },
- {
- key: "weibo",
- name: "微博",
- icon: "fa6-brands:weibo",
- color: "bg-[#E6162D] hover:bg-[#c11225]",
- webUrl: `https://service.weibo.com/share/share.php?title=${encodeURIComponent(`${title} - ${description}`)}&url=${encodeURIComponent(pageUrl)}&pic=${pageImage ? encodeURIComponent(pageImage) : ""}`,
- mobileUrl: `sinaweibo://share?title=${encodeURIComponent(title)}&content=${encodeURIComponent(`${description}\n${pageUrl}`)}`,
- },
- {
- key: "qq",
- name: "QQ",
- icon: "fa6-brands:qq",
- color: "bg-[#12B7F5] hover:bg-[#0e9dce]",
- webUrl: `https://connect.qq.com/widget/shareqq/index.html?url=${encodeURIComponent(pageUrl)}&title=${encodeURIComponent(title)}&desc=${encodeURIComponent(description)}&pics=${pageImage ? encodeURIComponent(pageImage) : ""}&summary=${encodeURIComponent(tags.join(","))}`,
- mobileUrl: `mqq://share/to_fri?url=${encodeURIComponent(pageUrl)}&title=${encodeURIComponent(title)}&desc=${encodeURIComponent(description)}&image_url=${pageImage ? encodeURIComponent(pageImage) : ""}&share_id=1101685683`,
- },
- {
- key: "wechat",
- name: "微信",
- icon: "fa6-brands:weixin",
- color: "bg-[#07C160] hover:bg-[#06a854]",
- webUrl: null, // 微信没有网页版分享
- mobileUrl: "weixin://", // 只是尝试唤起微信
- isWechat: true, // 特殊处理
- },
- {
- key: "qzone",
- name: "QQ空间",
- icon: "simple-icons:qzone",
- color: "bg-[#FECE00] hover:bg-[#e6b800]",
- webUrl: `https://sns.qzone.qq.com/cgi-bin/qzshare/cgi_qzshare_onekey?url=${encodeURIComponent(pageUrl)}&title=${encodeURIComponent(title)}&desc=${encodeURIComponent(description)}&pics=${pageImage ? encodeURIComponent(pageImage) : ""}&summary=${encodeURIComponent(tags.join(","))}&site=${encodeURIComponent(Astro.site?.hostname || "")}`,
- mobileUrl: `mqqzone://arise/share?url=${encodeURIComponent(pageUrl)}&title=${encodeURIComponent(title)}&desc=${encodeURIComponent(description)}&image_url=${pageImage ? encodeURIComponent(pageImage) : ""}`,
- },
- {
- key: "copy",
- name: i18n(I18nKey.copyLink),
- icon: "material-symbols:link",
- color:
- "bg-gray-500 hover:bg-gray-600 dark:bg-gray-600 dark:hover:bg-gray-700",
- isCopy: true,
- },
-];
----
-
-
-
-
-
- {i18n(I18nKey.share)}
-
-
-
-
-
-
-
-
-
-
diff --git a/src/components/misc/ShareCard.astro b/src/components/misc/ShareCard.astro
new file mode 100644
index 0000000..a0fe165
--- /dev/null
+++ b/src/components/misc/ShareCard.astro
@@ -0,0 +1,69 @@
+---
+import { Icon } from "astro-icon/components";
+import { profileConfig } from "../../config";
+import I18nKey from "../../i18n/i18nKey";
+import { i18n } from "../../i18n/translation";
+import SharePoster from "./SharePoster.svelte";
+
+interface Props {
+ title: string;
+ description?: string;
+ author?: string;
+ pubDate: string;
+ coverImage?: string | null;
+ url: string;
+ siteTitle: string;
+ avatar?: string | null;
+ class?: string;
+}
+
+const {
+ title,
+ description,
+ author = profileConfig.name,
+ pubDate,
+ coverImage,
+ url,
+ siteTitle,
+ avatar,
+ class: className,
+} = Astro.props;
+
+const finalAuthor = author || profileConfig.name;
+---
+
+
+
+
+
+
+
+
+
+ {i18n(I18nKey.shareOnSocial)}
+
+
+ {i18n(I18nKey.shareOnSocialDescription)}
+
+
+
+
+
+
+
+
diff --git a/src/components/misc/SharePoster.svelte b/src/components/misc/SharePoster.svelte
new file mode 100644
index 0000000..3008d5d
--- /dev/null
+++ b/src/components/misc/SharePoster.svelte
@@ -0,0 +1,576 @@
+
+
+
+
+{#if showModal}
+
+{/if}
diff --git a/src/components/misc/utils/poster-renderer.ts b/src/components/misc/utils/poster-renderer.ts
new file mode 100644
index 0000000..53d2bde
--- /dev/null
+++ b/src/components/misc/utils/poster-renderer.ts
@@ -0,0 +1,257 @@
+/**
+ * poster-renderer.ts - Canvas rendering utilities for SharePoster component
+ */
+
+export interface PosterConfig {
+ title: string;
+ author: string;
+ description: string;
+ pubDate: string;
+ coverImage: string | null;
+ url: string;
+ siteTitle: string;
+ avatar: string | null;
+ themeColor: string;
+}
+
+export interface SizeConfig {
+ scale: number;
+ width: number;
+ padding: number;
+ contentWidth: number;
+}
+
+/**
+ * Load image from URL with fallback to proxy
+ */
+export async function loadImage(src: string): Promise {
+ return new Promise((resolve) => {
+ const img = new Image();
+ img.crossOrigin = "anonymous";
+
+ img.onload = () => resolve(img);
+ img.onerror = () => {
+ if (src.includes("images.weserv.nl") || src.startsWith("data:")) {
+ resolve(null);
+ return;
+ }
+
+ const proxyImg = new Image();
+ proxyImg.crossOrigin = "anonymous";
+ proxyImg.onload = () => resolve(proxyImg);
+ proxyImg.onerror = () => resolve(null);
+ proxyImg.src = `https://images.weserv.nl/?url=${encodeURIComponent(src)}&output=png`;
+ };
+
+ img.src = src;
+ });
+}
+
+/**
+ * Get text lines wrapped to fit maxWidth
+ */
+export function getLines(
+ ctx: CanvasRenderingContext2D,
+ text: string,
+ maxWidth: number,
+): string[] {
+ const lines: string[] = [];
+ let currentLine = "";
+
+ for (const char of text) {
+ if (ctx.measureText(currentLine + char).width < maxWidth) {
+ currentLine += char;
+ } else {
+ if (currentLine) {
+ lines.push(currentLine);
+ }
+ currentLine = char;
+ }
+ }
+
+ if (currentLine) {
+ lines.push(currentLine);
+ }
+ return lines;
+}
+
+/**
+ * Draw rounded rectangle on canvas
+ */
+export function drawRoundedRect(
+ ctx: CanvasRenderingContext2D,
+ x: number,
+ y: number,
+ width: number,
+ height: number,
+ radius: number,
+): void {
+ ctx.beginPath();
+ ctx.moveTo(x + radius, y);
+ ctx.lineTo(x + width - radius, y);
+ ctx.quadraticCurveTo(x + width, y, x + width, y + radius);
+ ctx.lineTo(x + width, y + height - radius);
+ ctx.quadraticCurveTo(x + width, y + height, x + width - radius, y + height);
+ ctx.lineTo(x + radius, y + height);
+ ctx.quadraticCurveTo(x, y + height, x, y + height - radius);
+ ctx.lineTo(x, y + radius);
+ ctx.quadraticCurveTo(x, y, x + radius, y);
+ ctx.closePath();
+}
+
+/**
+ * Parse date string to object
+ */
+export function parseDate(
+ dateStr: string,
+): { day: string; month: string; year: string } | null {
+ const dateOnly = /^(\d{4})-(\d{2})-(\d{2})$/.exec(dateStr.trim());
+ if (dateOnly) {
+ return {
+ day: dateOnly[3],
+ month: dateOnly[2],
+ year: dateOnly[1],
+ };
+ }
+
+ try {
+ const d = new Date(dateStr);
+ if (Number.isNaN(d.getTime())) {
+ return null;
+ }
+
+ return {
+ day: d.getDate().toString().padStart(2, "0"),
+ month: (d.getMonth() + 1).toString().padStart(2, "0"),
+ year: d.getFullYear().toString(),
+ };
+ } catch {
+ return null;
+ }
+}
+
+/**
+ * Calculate poster dimensions
+ */
+export function calculateDimensions(
+ hasCover: boolean,
+ title: string,
+ description: string,
+ ctx: CanvasRenderingContext2D,
+ config: SizeConfig,
+): {
+ coverHeight: number;
+ titleHeight: number;
+ descHeight: number;
+ canvasHeight: number;
+} {
+ const { scale, padding, contentWidth } = config;
+ const FONT_FAMILY = "'Roboto', sans-serif";
+
+ const coverHeight = (hasCover ? 200 : 120) * scale;
+ const titleFontSize = 24 * scale;
+ const descFontSize = 14 * scale;
+ const titleLineHeight = 30 * scale;
+
+ // Calculate title height
+ ctx.font = `700 ${titleFontSize}px ${FONT_FAMILY}`;
+ const titleLines = getLines(ctx, title, contentWidth);
+ const titleHeight = titleLines.length * titleLineHeight;
+
+ // Calculate description height
+ let descHeight = 0;
+ if (description) {
+ ctx.font = `${descFontSize}px ${FONT_FAMILY}`;
+ const descLines = getLines(ctx, description, contentWidth - 16 * scale);
+ descHeight = Math.min(descLines.length, 6) * (25 * scale);
+ }
+
+ const canvasHeight =
+ coverHeight +
+ padding +
+ titleHeight +
+ 16 * scale +
+ descHeight +
+ (description ? 24 * scale : 8 * scale) +
+ 24 * scale +
+ 80 * scale + // QR size
+ padding;
+
+ return { coverHeight, titleHeight, descHeight, canvasHeight };
+}
+
+/**
+ * Draw decorative circles on poster
+ */
+export function drawDecorativeCircles(
+ ctx: CanvasRenderingContext2D,
+ canvasWidth: number,
+ canvasHeight: number,
+ themeColor: string,
+ scale: number,
+): void {
+ ctx.save();
+ ctx.globalAlpha = 0.1;
+ ctx.fillStyle = themeColor;
+ ctx.beginPath();
+ ctx.arc(canvasWidth - 25 * scale, 25 * scale, 75 * scale, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.beginPath();
+ ctx.arc(10 * scale, canvasHeight - 10 * scale, 50 * scale, 0, Math.PI * 2);
+ ctx.fill();
+ ctx.restore();
+}
+
+/**
+ * Draw date badge on cover image
+ */
+export function drawDateBadge(
+ ctx: CanvasRenderingContext2D,
+ dateObj: { day: string; month: string; year: string },
+ padding: number,
+ coverHeight: number,
+ scale: number,
+ FONT_FAMILY: string,
+ isDarkMode = false,
+): void {
+ const dateBoxW = 60 * scale;
+ const dateBoxH = 60 * scale;
+ const dateBoxX = padding;
+ const dateBoxY = coverHeight - dateBoxH;
+
+ const bgColor = isDarkMode
+ ? "rgba(255, 255, 255, 0.15)"
+ : "rgba(0, 0, 0, 0.3)";
+ const textColor = isDarkMode ? "#e5e5e5" : "#ffffff";
+ const separatorColor = isDarkMode
+ ? "rgba(255, 255, 255, 0.4)"
+ : "rgba(255, 255, 255, 0.6)";
+
+ // Background
+ ctx.fillStyle = bgColor;
+ drawRoundedRect(ctx, dateBoxX, dateBoxY, dateBoxW, dateBoxH, 4 * scale);
+ ctx.fill();
+
+ // Day number
+ ctx.fillStyle = textColor;
+ ctx.textAlign = "center";
+ ctx.textBaseline = "middle";
+ ctx.font = `700 ${30 * scale}px ${FONT_FAMILY}`;
+ ctx.fillText(dateObj.day, dateBoxX + dateBoxW / 2, dateBoxY + 24 * scale);
+
+ // Separator line
+ ctx.beginPath();
+ ctx.strokeStyle = separatorColor;
+ ctx.lineWidth = 1 * scale;
+ ctx.moveTo(dateBoxX + 10 * scale, dateBoxY + 42 * scale);
+ ctx.lineTo(dateBoxX + dateBoxW - 10 * scale, dateBoxY + 42 * scale);
+ ctx.stroke();
+
+ // Year month
+ ctx.font = `${10 * scale}px ${FONT_FAMILY}`;
+ ctx.fillText(
+ `${dateObj.year} ${dateObj.month}`,
+ dateBoxX + dateBoxW / 2,
+ dateBoxY + 51 * scale,
+ );
+}
diff --git a/src/i18n/i18nKey.ts b/src/i18n/i18nKey.ts
index 3cdf1c3..2426fb3 100644
--- a/src/i18n/i18nKey.ts
+++ b/src/i18n/i18nKey.ts
@@ -41,7 +41,16 @@ enum I18nKey {
share = "share",
copyLink = "copyLink",
copied = "copied",
+ copyLinkFailed = "copyLinkFailed",
emptyArchive = "emptyArchive",
+
+ shareArticle = "shareArticle",
+ generatingPoster = "generatingPoster",
+ posterFailed = "posterFailed",
+ savePoster = "savePoster",
+ shareOnSocial = "shareOnSocial",
+ shareOnSocialDescription = "shareOnSocialDescription",
+ scanToRead = "scanToRead",
}
export default I18nKey;
diff --git a/src/i18n/languages/en.ts b/src/i18n/languages/en.ts
index 308bdd3..694820d 100644
--- a/src/i18n/languages/en.ts
+++ b/src/i18n/languages/en.ts
@@ -44,5 +44,15 @@ export const en: Translation = {
[Key.share]: "Share to",
[Key.copyLink]: "Copy Link",
[Key.copied]: "Copied",
+ [Key.copyLinkFailed]: "Copy failed. Please try again.",
[Key.emptyArchive]: "No matching posts",
+
+ [Key.shareArticle]: "Share",
+ [Key.generatingPoster]: "Generating poster...",
+ [Key.posterFailed]: "Failed to generate poster. Please try again.",
+ [Key.savePoster]: "Save Poster",
+ [Key.shareOnSocial]: "Share",
+ [Key.shareOnSocialDescription]:
+ "If this article helped you, please share it with others!",
+ [Key.scanToRead]: "Source",
};
diff --git a/src/i18n/languages/es.ts b/src/i18n/languages/es.ts
index 635756f..562e6c4 100644
--- a/src/i18n/languages/es.ts
+++ b/src/i18n/languages/es.ts
@@ -44,5 +44,15 @@ export const es: Translation = {
[Key.share]: "Compartir en",
[Key.copyLink]: "Copiar enlace",
[Key.copied]: "Copiado",
+ [Key.copyLinkFailed]: "No se pudo copiar el enlace. Inténtalo de nuevo.",
[Key.emptyArchive]: "No hay publicaciones que coincidan",
+
+ [Key.shareArticle]: "Compartir",
+ [Key.generatingPoster]: "Generando póster...",
+ [Key.posterFailed]: "No se pudo generar el póster. Inténtalo de nuevo.",
+ [Key.savePoster]: "Guardar póster",
+ [Key.shareOnSocial]: "Compartir",
+ [Key.shareOnSocialDescription]:
+ "Si este artículo te ha ayudado, ¡compártelo con más gente!",
+ [Key.scanToRead]: "Fuente",
};
diff --git a/src/i18n/languages/id.ts b/src/i18n/languages/id.ts
index dfc3c94..c9ea442 100644
--- a/src/i18n/languages/id.ts
+++ b/src/i18n/languages/id.ts
@@ -44,5 +44,15 @@ export const id: Translation = {
[Key.share]: "Bagikan ke",
[Key.copyLink]: "Salin Tautan",
[Key.copied]: "Tersalin",
+ [Key.copyLinkFailed]: "Gagal menyalin tautan. Silakan coba lagi.",
[Key.emptyArchive]: "Tidak ada postingan yang cocok",
+
+ [Key.shareArticle]: "Bagikan",
+ [Key.generatingPoster]: "Membuat poster...",
+ [Key.posterFailed]: "Gagal membuat poster. Silakan coba lagi.",
+ [Key.savePoster]: "Simpan Poster",
+ [Key.shareOnSocial]: "Bagikan",
+ [Key.shareOnSocialDescription]:
+ "Jika artikel ini membantu Anda, silakan bagikan kepada orang lain!",
+ [Key.scanToRead]: "Sumber",
};
diff --git a/src/i18n/languages/ja.ts b/src/i18n/languages/ja.ts
index 9fdd1ef..0e72daa 100644
--- a/src/i18n/languages/ja.ts
+++ b/src/i18n/languages/ja.ts
@@ -44,5 +44,15 @@ export const ja: Translation = {
[Key.share]: "共有",
[Key.copyLink]: "リンクをコピー",
[Key.copied]: "コピーしました",
+ [Key.copyLinkFailed]: "コピーに失敗しました。もう一度お試しください。",
[Key.emptyArchive]: "一致する記事がありません",
+
+ [Key.shareArticle]: "共有",
+ [Key.generatingPoster]: "ポスターを生成中...",
+ [Key.posterFailed]: "ポスターの生成に失敗しました。もう一度お試しください。",
+ [Key.savePoster]: "ポスターを保存",
+ [Key.shareOnSocial]: "共有",
+ [Key.shareOnSocialDescription]:
+ "この記事が役に立ったら、ぜひシェアしてください!",
+ [Key.scanToRead]: "出典元",
};
diff --git a/src/i18n/languages/ko.ts b/src/i18n/languages/ko.ts
index ba3d835..fbe6dae 100644
--- a/src/i18n/languages/ko.ts
+++ b/src/i18n/languages/ko.ts
@@ -44,5 +44,15 @@ export const ko: Translation = {
[Key.share]: "공유하기",
[Key.copyLink]: "링크 복사",
[Key.copied]: "복사됨",
+ [Key.copyLinkFailed]: "링크 복사에 실패했습니다. 다시 시도해 주세요.",
[Key.emptyArchive]: "일치하는 게시글이 없습니다",
+
+ [Key.shareArticle]: "공유",
+ [Key.generatingPoster]: "포스터 생성 중...",
+ [Key.posterFailed]: "포스터 생성에 실패했습니다. 다시 시도해 주세요.",
+ [Key.savePoster]: "포스터 저장",
+ [Key.shareOnSocial]: "공유",
+ [Key.shareOnSocialDescription]:
+ "이 글이 도움이 되었다면 다른 사람들과 공유해 주세요!",
+ [Key.scanToRead]: "출처",
};
diff --git a/src/i18n/languages/th.ts b/src/i18n/languages/th.ts
index ab4227b..900f813 100644
--- a/src/i18n/languages/th.ts
+++ b/src/i18n/languages/th.ts
@@ -44,5 +44,15 @@ export const th: Translation = {
[Key.share]: "แชร์ไปยัง",
[Key.copyLink]: "คัดลอกลิงก์",
[Key.copied]: "คัดลอกแล้ว",
+ [Key.copyLinkFailed]: "คัดลอกลิงก์ไม่สำเร็จ กรุณาลองอีกครั้ง",
[Key.emptyArchive]: "ไม่พบโพสต์ที่ตรงกัน",
+
+ [Key.shareArticle]: "แชร์",
+ [Key.generatingPoster]: "กำลังสร้างโปสเตอร์...",
+ [Key.posterFailed]: "สร้างโปสเตอร์ไม่สำเร็จ กรุณาลองอีกครั้ง",
+ [Key.savePoster]: "บันทึกโปสเตอร์",
+ [Key.shareOnSocial]: "แชร์",
+ [Key.shareOnSocialDescription]:
+ "หากบทความนี้เป็นประโยชน์ต่อคุณ แบ่งปันให้คนอื่นๆ ได้เลย!",
+ [Key.scanToRead]: "แหล่งที่มา",
};
diff --git a/src/i18n/languages/tr.ts b/src/i18n/languages/tr.ts
index 89e22f5..3ef2bd6 100644
--- a/src/i18n/languages/tr.ts
+++ b/src/i18n/languages/tr.ts
@@ -44,5 +44,15 @@ export const tr: Translation = {
[Key.share]: "Paylaş",
[Key.copyLink]: "Bağlantıyı Kopyala",
[Key.copied]: "Kopyalandı",
+ [Key.copyLinkFailed]: "Bağlantı kopyalanamadı. Lütfen tekrar deneyin.",
[Key.emptyArchive]: "Eşleşen yazı bulunamadı",
+
+ [Key.shareArticle]: "Paylaş",
+ [Key.generatingPoster]: "Poster oluşturuluyor...",
+ [Key.posterFailed]: "Poster oluşturulamadı. Lütfen tekrar deneyin.",
+ [Key.savePoster]: "Posteri Kaydet",
+ [Key.shareOnSocial]: "Paylaş",
+ [Key.shareOnSocialDescription]:
+ "Bu yazı işinize yaradıysa başkalarıyla paylaşın!",
+ [Key.scanToRead]: "Kaynak",
};
diff --git a/src/i18n/languages/vi.ts b/src/i18n/languages/vi.ts
index 7fa66f3..d0356ec 100644
--- a/src/i18n/languages/vi.ts
+++ b/src/i18n/languages/vi.ts
@@ -44,5 +44,15 @@ export const vi: Translation = {
[Key.share]: "Chia sẻ",
[Key.copyLink]: "Sao chép liên kết",
[Key.copied]: "Đã sao chép",
+ [Key.copyLinkFailed]: "Không thể sao chép liên kết. Vui lòng thử lại.",
[Key.emptyArchive]: "Không có bài viết phù hợp",
+
+ [Key.shareArticle]: "Chia sẻ",
+ [Key.generatingPoster]: "Đang tạo poster...",
+ [Key.posterFailed]: "Không thể tạo poster. Vui lòng thử lại.",
+ [Key.savePoster]: "Lưu Poster",
+ [Key.shareOnSocial]: "Chia sẻ",
+ [Key.shareOnSocialDescription]:
+ "Nếu bài viết này hữu ích, hãy chia sẻ cho người khác!",
+ [Key.scanToRead]: "Nguồn",
};
diff --git a/src/i18n/languages/zh_CN.ts b/src/i18n/languages/zh_CN.ts
index 41f828f..f74c043 100644
--- a/src/i18n/languages/zh_CN.ts
+++ b/src/i18n/languages/zh_CN.ts
@@ -44,5 +44,14 @@ export const zh_CN: Translation = {
[Key.share]: "分享到",
[Key.copyLink]: "复制链接",
[Key.copied]: "已复制",
+ [Key.copyLinkFailed]: "复制失败,请重试",
[Key.emptyArchive]: "暂无匹配的文章",
+
+ [Key.shareArticle]: "分享",
+ [Key.generatingPoster]: "正在生成海报...",
+ [Key.posterFailed]: "生成失败,请重试",
+ [Key.savePoster]: "保存海报",
+ [Key.shareOnSocial]: "分享",
+ [Key.shareOnSocialDescription]: "如果这篇文章对你有帮助,欢迎分享给更多人!",
+ [Key.scanToRead]: "来源网站",
};
diff --git a/src/i18n/languages/zh_TW.ts b/src/i18n/languages/zh_TW.ts
index 2c00adc..8228489 100644
--- a/src/i18n/languages/zh_TW.ts
+++ b/src/i18n/languages/zh_TW.ts
@@ -44,5 +44,14 @@ export const zh_TW: Translation = {
[Key.share]: "分享到",
[Key.copyLink]: "複製連結",
[Key.copied]: "已複製",
+ [Key.copyLinkFailed]: "複製失敗,請重試",
[Key.emptyArchive]: "暫無匹配的文章",
+
+ [Key.shareArticle]: "分享",
+ [Key.generatingPoster]: "正在生成海報...",
+ [Key.posterFailed]: "生成失敗,請重試",
+ [Key.savePoster]: "儲存海報",
+ [Key.shareOnSocial]: "分享",
+ [Key.shareOnSocialDescription]: "如果這篇文章對你有幫助,歡迎分享給更多人!",
+ [Key.scanToRead]: "來源網站",
};
diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro
index 45033f0..caea769 100644
--- a/src/pages/posts/[...slug].astro
+++ b/src/pages/posts/[...slug].astro
@@ -1,17 +1,17 @@
---
import { render } from "astro:content";
+import path from "node:path";
import Giscus from "@components/Giscus.astro";
import License from "@components/misc/License.astro";
import Markdown from "@components/misc/Markdown.astro";
-import SocialShare from "@components/SocialShare.astro";
+import ShareCard from "@components/misc/ShareCard.astro";
import I18nKey from "@i18n/i18nKey";
import { i18n } from "@i18n/translation";
import MainGridLayout from "@layouts/MainGridLayout.astro";
-
import { getSortedPosts } from "@utils/content-utils";
-
import { getPostUrlById } from "@utils/url-utils";
+import type { ImageMetadata } from "astro";
import { Icon } from "astro-icon/components";
@@ -41,9 +41,34 @@ const postImageSrc = entry.data.image
? entry.data.image.src
: entry.data.image
: "";
-const shareImageUrl = postImageSrc
- ? new URL(postImageSrc, Astro.site).href
- : "";
+
+// Resolve the configured profile avatar into a served URL for the poster canvas.
+let posterAvatarUrl = profileConfig.avatar || "";
+const avatarSource = posterAvatarUrl;
+const isAvatarLocal =
+ avatarSource !== "" &&
+ !(
+ avatarSource.startsWith("/") ||
+ avatarSource.startsWith("http") ||
+ avatarSource.startsWith("https") ||
+ avatarSource.startsWith("data:")
+ );
+if (isAvatarLocal) {
+ const avatarFiles = import.meta.glob(
+ "../../**/*.{jpg,jpeg,png,gif,webp,avif,svg}",
+ { import: "default" },
+ );
+ const normalizedAvatarPath = path
+ .normalize(path.join("../../", avatarSource))
+ .replace(/\\/g, "/");
+ const avatarFile =
+ avatarFiles[normalizedAvatarPath] ||
+ avatarFiles[`./${normalizedAvatarPath}`];
+ if (avatarFile) {
+ const avatarImg = await avatarFile();
+ posterAvatarUrl = avatarImg.src;
+ }
+}
// The hero spans the full content column (~50rem on desktop). Requesting a
// responsive srcset keeps the decode small, and Astro emits
@@ -149,21 +174,27 @@ const giscusConfig = {
-
+
-
+>
diff --git a/src/styles/variables.styl b/src/styles/variables.styl
index ab9958b..f3ac918 100644
--- a/src/styles/variables.styl
+++ b/src/styles/variables.styl
@@ -51,6 +51,7 @@ define({
--line-color: black(0.1) white(0.1)
--meta-divider: black(0.2) white(0.2)
+ --content-meta: black(0.7) white(0.6)
--inline-code-bg: var(--btn-regular-bg)
--inline-code-color: var(--btn-content)