From e4e7e992f5959039165e05eb3ca7d84d34589e4a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?T=C3=B4n=20Th=E1=BA=A5t=20Tr=E1=BB=8Dng?= <119159110+BooGu7@users.noreply.github.com> Date: Sat, 16 May 2026 10:18:41 +0700 Subject: [PATCH 1/2] fix local --- astro.config.mjs | 3 + package.json | 3 +- pnpm-lock.yaml | 346 ++++++++++++++++++++++++- scripts/run-pagefind.mjs | 9 + src/components/ArchivePanel.svelte | 11 +- src/components/Footer.astro | 4 +- src/components/LanguageSwitcher.astro | 18 ++ src/components/PostCard.astro | 2 +- src/components/PostMeta.astro | 22 +- src/components/misc/ImageWrapper.astro | 18 +- src/constants/locale-preference.ts | 4 + src/layouts/Layout.astro | 33 ++- src/layouts/MainGridLayout.astro | 6 +- src/middleware.ts | 90 +++++++ src/pages/[...page].astro | 23 -- src/pages/about.astro | 1 + src/pages/archive.astro | 1 + src/pages/en/[page].astro | 1 + src/pages/en/about.astro | 1 + src/pages/en/archive.astro | 1 + src/pages/en/index.astro | 1 + src/pages/en/login.astro | 1 + src/pages/en/posts/[...slug].astro | 14 +- src/pages/en/privacy-policy.astro | 1 + src/pages/en/terms-of-service.astro | 1 + src/pages/login.astro | 1 + src/pages/posts/[...slug].astro | 14 +- src/pages/privacy-policy.astro | 1 + src/pages/privacypolicy.astro | 1 + src/pages/robots.txt.ts | 2 + src/pages/rss.xml.ts | 2 + src/pages/sitemap-0.xml.ts | 2 + src/pages/sitemap-index.xml.ts | 2 + src/pages/terms-of-service.astro | 1 + src/pages/vn/[...page].astro | 1 + src/pages/vn/about.astro | 1 + src/pages/vn/archive.astro | 1 + src/pages/vn/login.astro | 1 + src/pages/vn/posts/[...slug].astro | 14 + src/pages/vn/privacy-policy.astro | 1 + src/pages/vn/terms-of-service.astro | 1 + src/utils/date-utils.ts | 20 +- src/utils/og-image.ts | 41 +++ src/utils/site-urls.ts | 1 - 44 files changed, 652 insertions(+), 71 deletions(-) create mode 100644 scripts/run-pagefind.mjs create mode 100644 src/constants/locale-preference.ts create mode 100644 src/middleware.ts delete mode 100644 src/pages/[...page].astro create mode 100644 src/utils/og-image.ts diff --git a/astro.config.mjs b/astro.config.mjs index b4c910c..7478d76 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -1,5 +1,6 @@ import svelte from "@astrojs/svelte"; import tailwind from "@astrojs/tailwind"; +import vercel from "@astrojs/vercel"; import { pluginCollapsibleSections } from "@expressive-code/plugin-collapsible-sections"; import { pluginLineNumbers } from "@expressive-code/plugin-line-numbers"; import swup from "@swup/astro"; @@ -27,6 +28,8 @@ import { remarkReadingTime } from "./src/plugins/remark-reading-time.mjs"; export default defineConfig({ site: "https://boospace.tech/", base: "/", + output: "server", + adapter: vercel(), trailingSlash: "always", integrations: [ tailwind({ diff --git a/package.json b/package.json index 5fdc092..8121ac0 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": "astro build && node scripts/run-pagefind.mjs", "preview": "astro preview", "astro": "astro", "type-check": "tsc --noEmit --isolatedDeclarations", @@ -22,6 +22,7 @@ "@astrojs/rss": "^4.0.12", "@astrojs/svelte": "7.1.0", "@astrojs/tailwind": "^6.0.2", + "@astrojs/vercel": "^8.2.11", "@expressive-code/core": "^0.41.3", "@expressive-code/plugin-collapsible-sections": "^0.41.3", "@expressive-code/plugin-line-numbers": "^0.41.3", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b41f0bb..dbfeeef 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -16,10 +16,13 @@ importers: version: 4.0.12 '@astrojs/svelte': specifier: 7.1.0 - version: 7.1.0(@types/node@24.3.0)(astro@5.13.3(@types/node@24.3.0)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1))(jiti@1.21.7)(stylus@0.64.0)(svelte@5.38.6)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1) + version: 7.1.0(@types/node@24.3.0)(astro@5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1))(jiti@1.21.7)(stylus@0.64.0)(svelte@5.38.6)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1) '@astrojs/tailwind': specifier: ^6.0.2 - version: 6.0.2(astro@5.13.3(@types/node@24.3.0)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1))(tailwindcss@3.4.17) + version: 6.0.2(astro@5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1))(tailwindcss@3.4.17) + '@astrojs/vercel': + specifier: ^8.2.11 + version: 8.2.11(astro@5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1))(rollup@2.79.2)(svelte@5.38.6) '@expressive-code/core': specifier: ^0.41.3 version: 0.41.3 @@ -70,10 +73,10 @@ importers: version: 1.2.0(svelte@5.38.6) astro: specifier: 5.13.3 - version: 5.13.3(@types/node@24.3.0)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1) + version: 5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1) astro-expressive-code: specifier: ^0.41.3 - version: 0.41.3(astro@5.13.3(@types/node@24.3.0)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1)) + version: 0.41.3(astro@5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1)) astro-icon: specifier: ^1.1.5 version: 1.1.5 @@ -209,6 +212,9 @@ packages: '@astrojs/internal-helpers@0.7.2': resolution: {integrity: sha512-KCkCqR3Goym79soqEtbtLzJfqhTWMyVaizUi35FLzgGSzBotSw8DB1qwsu7U96ihOJgYhDk2nVPz+3LnXPeX6g==} + '@astrojs/internal-helpers@0.7.4': + resolution: {integrity: sha512-lDA9MqE8WGi7T/t2BMi+EAXhs4Vcvr94Gqx3q15cFEz8oFZMO4/SFBqYr/UcmNlvW+35alowkVj+w9VhLvs5Cw==} + '@astrojs/language-server@2.15.4': resolution: {integrity: sha512-JivzASqTPR2bao9BWsSc/woPHH7OGSGc9aMxXL4U6egVTqBycB3ZHdBJPuOCVtcGLrzdWTosAqVPz1BVoxE0+A==} hasBin: true @@ -252,6 +258,11 @@ packages: '@astrojs/ts-plugin@1.10.4': resolution: {integrity: sha512-rapryQINgv5VLZF884R/wmgX3mM9eH1PC/I3kkPV9rP6lEWrRN1YClF3bGcDHFrf8EtTLc0Wqxne1Uetpevozg==} + '@astrojs/vercel@8.2.11': + resolution: {integrity: sha512-PGtWHvHYMkT8ftSR3yuR7oyf/oPvOv8AfhCFlSQg318hfpalSEPND9mjbdQGpMeZz3KtvvOnHyYwqmu5V8MSHg==} + peerDependencies: + astro: ^5.0.0 + '@astrojs/yaml2ts@0.2.2': resolution: {integrity: sha512-GOfvSr5Nqy2z5XiwqTouBBpy5FyI6DEe+/g/Mk5am9SjILN1S5fOEvYK0GuWHg98yS/dobP4m8qyqw/URW35fQ==} @@ -1354,6 +1365,10 @@ packages: resolution: {integrity: sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA==} engines: {node: '>=12'} + '@isaacs/fs-minipass@4.0.1': + resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==} + engines: {node: '>=18.0.0'} + '@jridgewell/gen-mapping@0.3.13': resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} @@ -1373,6 +1388,11 @@ packages: '@jridgewell/trace-mapping@0.3.30': resolution: {integrity: sha512-GQ7Nw5G2lTu/BtHTKfXhKHok2WGetd4XYcVKGx00SjAk8GMwgJM3zr6zORiPGuOE+/vkc90KtTosSSvaCjKb2Q==} + '@mapbox/node-pre-gyp@2.0.3': + resolution: {integrity: sha512-uwPAhccfFJlsfCxMYTwOdVfOz3xqyj8xYL3zJj8f0pb30tLohnnFPhLuqp4/qoEz8sNxe4SESZedcBojRefIzg==} + engines: {node: '>=18'} + hasBin: true + '@nodelib/fs.scandir@2.1.5': resolution: {integrity: sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==} engines: {node: '>= 8'} @@ -1833,6 +1853,53 @@ packages: vue-router: optional: true + '@vercel/analytics@1.6.1': + resolution: {integrity: sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==} + peerDependencies: + '@remix-run/react': ^2 + '@sveltejs/kit': ^1 || ^2 + next: '>= 13' + react: ^18 || ^19 || ^19.0.0-rc + svelte: '>= 4' + vue: ^3 + vue-router: ^4 + peerDependenciesMeta: + '@remix-run/react': + optional: true + '@sveltejs/kit': + optional: true + next: + optional: true + react: + optional: true + svelte: + optional: true + vue: + optional: true + vue-router: + optional: true + + '@vercel/functions@2.2.13': + resolution: {integrity: sha512-14ArBSIIcOBx9nrEgaJb4Bw+en1gl6eSoJWh8qjifLl5G3E4dRXCFOT8HP+w66vb9Wqyd1lAQBrmRhRwOj9X9A==} + engines: {node: '>= 18'} + peerDependencies: + '@aws-sdk/credential-provider-web-identity': '*' + peerDependenciesMeta: + '@aws-sdk/credential-provider-web-identity': + optional: true + + '@vercel/nft@0.30.3': + resolution: {integrity: sha512-UEq+eF0ocEf9WQCV1gktxKhha36KDs7jln5qii6UpPf5clMqDc0p3E7d9l2Smx0i9Pm1qpq4S4lLfNl97bbv6w==} + engines: {node: '>=18'} + hasBin: true + + '@vercel/oidc@2.0.2': + resolution: {integrity: sha512-59PBFx3T+k5hLTEWa3ggiMpGRz1OVvl9eN8SUai+A43IsqiOuAe7qPBf+cray/Fj6mkgnxm/D7IAtjc8zSHi7g==} + engines: {node: '>= 18'} + + '@vercel/routing-utils@5.3.3': + resolution: {integrity: sha512-KYm2sLNUD48gDScv8ob4ejc3Gww2jcJyW80hTdYlenAPz/5BQar1Gyh38xrUuZ532TUwSb5mV1uRbAuiykq0EQ==} + '@vercel/speed-insights@1.2.0': resolution: {integrity: sha512-y9GVzrUJ2xmgtQlzFP2KhVRoCglwfRQgjyfY607aU0hh0Un6d0OUyrJkjuAlsV18qR4zfoFPs/BiIj9YDS6Wzw==} peerDependencies: @@ -1882,11 +1949,27 @@ packages: '@vscode/l10n@0.0.18': resolution: {integrity: sha512-KYSIHVmslkaCDyw013pphY+d7x1qV8IZupYfeIfzNA+nsaWHbn5uPuQRvdRFsa9zFzGeudPuoGoZ1Op4jrJXIQ==} + abbrev@3.0.1: + resolution: {integrity: sha512-AO2ac6pjRB3SJmGJo+v5/aK6Omggp6fsLrs6wN9bd35ulu4cCwaAU9+7ZhXjeqHVkaHThLuzH0nZr0YpCDhygg==} + engines: {node: ^18.17.0 || >=20.5.0} + + acorn-import-attributes@1.9.5: + resolution: {integrity: sha512-n02Vykv5uA3eHGM/Z2dQrcD56kL8TyDb2p1+0P83PClMnC/nc+anbQRhIOWnSq4Ke/KvDPrY3C9hDtC/A3eHnQ==} + peerDependencies: + acorn: ^8 + acorn@8.15.0: resolution: {integrity: sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==} engines: {node: '>=0.4.0'} hasBin: true + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + + ajv@6.15.0: + resolution: {integrity: sha512-fgFx7Hfoq60ytK2c7DhnF8jIvzYgOMxfugjLOSMHjLIPgenqa7S7oaagATUq99mV6IYvN2tRmC0wnTYX6iPbMw==} + ajv@8.17.1: resolution: {integrity: sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==} @@ -1962,6 +2045,9 @@ packages: resolution: {integrity: sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA==} engines: {node: '>= 0.4'} + async-sema@3.1.1: + resolution: {integrity: sha512-tLRNUXati5MFePdAk8dw7Qt7DpxPB60ofAgn8WRhW6a2rcimZnYBP9oxHiv0OHy+Wz7kPMG+t4LGdt31+4EmGg==} + async@3.2.6: resolution: {integrity: sha512-htCUDlxyyCLMgaM3xXg0C0LW2xqfuQ6p05pCEIsXuyQ+a1koYKTuBMzRNwmybfLgvJDMd0r1LTn4+E0Ti6C2AA==} @@ -2035,6 +2121,9 @@ packages: resolution: {integrity: sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==} engines: {node: '>=8'} + bindings@1.5.0: + resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==} + blob-to-buffer@1.2.9: resolution: {integrity: sha512-BF033y5fN6OCofD3vgHmNtwZWRcq9NLyyxyILx9hfMy1sXYy4ojFl765hJ2lP0YaN2fuxPaLO2Vzzoxy0FLFFA==} @@ -2157,6 +2246,10 @@ packages: resolution: {integrity: sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==} engines: {node: '>=10'} + chownr@3.0.0: + resolution: {integrity: sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g==} + engines: {node: '>=18'} + ci-info@4.3.0: resolution: {integrity: sha512-l+2bNRMiQgcfILUi33labAZYIWlH1kWDp+ecNo5iisRKrbm0xcRyCww71/YU0Fkw0mAFpz9bJayXPjey6vkmaQ==} engines: {node: '>=8'} @@ -2234,6 +2327,10 @@ packages: confbox@0.2.2: resolution: {integrity: sha512-1NB+BKqhtNipMsov4xI/NnhCKp9XG9NamYp5PVm9klAT0fsrNPjaFICsCFhNhwZJKNh7zB/3q8qXz0E9oaMNtQ==} + consola@3.4.2: + resolution: {integrity: sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==} + engines: {node: ^14.18.0 || >=16.10.0} + convert-source-map@2.0.0: resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} @@ -2595,6 +2692,9 @@ packages: resolution: {integrity: sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==} engines: {node: '>=8.6.0'} + fast-json-stable-stringify@2.1.0: + resolution: {integrity: sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==} + fast-uri@3.1.0: resolution: {integrity: sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==} @@ -2621,6 +2721,9 @@ packages: resolution: {integrity: sha512-UxKlfCRuCBxSXU4C6t9scbDyWZ4VlaFFdojKtzJuSkuOBQ5CNFum+zZXFwHjo+CxBC1t6zlYPgHIgFjL8ggoEQ==} engines: {node: '>=0.10.0'} + file-uri-to-path@1.0.0: + resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==} + filelist@1.0.4: resolution: {integrity: sha512-w1cEuf3S+DrLCQL7ET6kz+gmlJdbq9J7yXCSjK/OZCPA+qEN1WyF4ZAf0YYJa4/shHJra2t/d/r8SV4Ji+x+8Q==} @@ -2885,6 +2988,10 @@ packages: http-cache-semantics@4.2.0: resolution: {integrity: sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + iconv-lite@0.6.3: resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} engines: {node: '>=0.10.0'} @@ -3139,6 +3246,9 @@ packages: json-parse-even-better-errors@2.3.1: resolution: {integrity: sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==} + json-schema-traverse@0.4.1: + resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema-traverse@1.0.0: resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} @@ -3468,10 +3578,18 @@ packages: resolution: {integrity: sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==} engines: {node: '>=16 || 14 >=14.17'} + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + minizlib@2.1.2: resolution: {integrity: sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==} engines: {node: '>= 8'} + minizlib@3.1.0: + resolution: {integrity: sha512-KZxYo1BUkWD2TVFLr0MQoM8vUUigWD3LlD83a/75BqC+4qE0Hb1Vo5v1FgcfaNXvfXzr+5EhQ6ing/CaBijTlw==} + engines: {node: '>= 18'} + mkdirp@1.0.4: resolution: {integrity: sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==} engines: {node: '>=10'} @@ -3527,12 +3645,21 @@ packages: encoding: optional: true + node-gyp-build@4.8.4: + resolution: {integrity: sha512-LA4ZjwlnUblHVgq0oBF3Jl/6h/Nvs5fzBLwdEF4nuxnFdsfajde4WfxtJr3CaiH+F6ewcIB/q4jQ4UzPyid+CQ==} + hasBin: true + node-mock-http@1.0.2: resolution: {integrity: sha512-zWaamgDUdo9SSLw47we78+zYw/bDr5gH8pH7oRRs8V3KmBtu8GLgGIbV2p/gRPd3LWpEOpjQj7X1FOU3VFMJ8g==} node-releases@2.0.19: resolution: {integrity: sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw==} + nopt@8.1.0: + resolution: {integrity: sha512-ieGu42u/Qsa4TFktmaKEwM6MQH0pOWnaB3htzh0JRtx84+Mebc0cbZYN5bC+6WTZ4+77xrL9Pn5m7CV6VIkV7A==} + engines: {node: ^18.17.0 || >=20.5.0} + hasBin: true + normalize-path@3.0.0: resolution: {integrity: sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==} engines: {node: '>=0.10.0'} @@ -3702,6 +3829,9 @@ packages: resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} engines: {node: '>=16 || 14 >=14.18'} + path-to-regexp@6.1.0: + resolution: {integrity: sha512-h9DqehX3zZZDCEm+xbfU0ZmwCGFCAAraPJWMXJ4+v32NjZJilVg3k1TcKsRgIb8IQ/izZSaydDc1OhJCZvs2Dw==} + path-to-regexp@6.3.0: resolution: {integrity: sha512-Yhpw4T9C6hPpgPeA28us07OJeqZ5EzQTkbfwuhsUg0c237RomFoETJgmp2sa3F/41gfLE6G5cqcYwznmeEeOlQ==} @@ -3733,6 +3863,10 @@ packages: resolution: {integrity: sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==} engines: {node: '>=12'} + picomatch@4.0.4: + resolution: {integrity: sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==} + engines: {node: '>=12'} + pify@2.3.0: resolution: {integrity: sha512-udgsAY+fTnvv7kI7aaxbqwWNb0AHiB0qBO89PZKPkoTmGOgdbrHDKD+0B2X4uTfJ/FT1R09r9gTsjUjNJotuog==} engines: {node: '>=0.10.0'} @@ -4068,6 +4202,10 @@ packages: resolution: {integrity: sha512-uxFIHU0YlHYhDQtV4R9J6a52SLx28BCjT+4ieh7IGbgwVJWO+km431c4yRlREUAsAmt/uMjQUyQHNEPf0M39CA==} engines: {node: '>=6'} + punycode@2.3.1: + resolution: {integrity: sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==} + engines: {node: '>=6'} + quansync@0.2.11: resolution: {integrity: sha512-AifT7QEbW9Nri4tAwR5M/uzpBuqfZf+zwaEM/QkzEjj7NBuFD2rBuy0K3dE+8wltbezDV7JMA0WfnCPYRSYbXA==} @@ -4556,6 +4694,10 @@ packages: resolution: {integrity: sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==} engines: {node: '>=10'} + tar@7.5.15: + resolution: {integrity: sha512-dzGK0boVlC4W5QFuQN1EFSl3bIDYsk7Tj40U6eIBnK2k/8ml7TZ5agbI5j5+qnoVcAA+rNtBml8SEiLxZpNqRQ==} + engines: {node: '>=18'} + terser@5.43.1: resolution: {integrity: sha512-+6erLbBm0+LROX2sPXlUYx/ux5PyE9K/a92Wrt6oA+WDAoFTdpHE5tCYCI5PNzq2y8df4rA+QgHLJuR4jNymsg==} engines: {node: '>=10'} @@ -4584,6 +4726,10 @@ packages: resolution: {integrity: sha512-tX5e7OM1HnYr2+a2C/4V0htOcSQcoSTH9KgJnVvNm5zm/cyEWKJ7j7YutsH9CxMdtOkkLFy2AHrMci9IM8IPZQ==} engines: {node: '>=12.0.0'} + tinyglobby@0.2.16: + resolution: {integrity: sha512-pn99VhoACYR8nFHhxqix+uvsbXineAasWm5ojXoN8xEwK5Kd3/TrhNn1wByuD52UxWRLy8pu+kRMniEi6Eq9Zg==} + engines: {node: '>=12.0.0'} + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -4821,6 +4967,9 @@ packages: peerDependencies: browserslist: '>= 4.21.0' + uri-js@4.4.1: + resolution: {integrity: sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==} + util-deprecate@1.0.2: resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} @@ -5061,6 +5210,10 @@ packages: yallist@4.0.0: resolution: {integrity: sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==} + yallist@5.0.0: + resolution: {integrity: sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw==} + engines: {node: '>=18'} + yaml-language-server@1.15.0: resolution: {integrity: sha512-N47AqBDCMQmh6mBLmI6oqxryHRzi33aPFPsJhYy3VTUGCdLHYjGh4FZzpUjRlphaADBBkDmnkM/++KNIOHi5Rw==} hasBin: true @@ -5154,6 +5307,8 @@ snapshots: '@astrojs/internal-helpers@0.7.2': {} + '@astrojs/internal-helpers@0.7.4': {} + '@astrojs/language-server@2.15.4(typescript@5.9.2)': dependencies: '@astrojs/compiler': 2.12.2 @@ -5212,10 +5367,10 @@ snapshots: fast-xml-parser: 5.2.5 kleur: 4.1.5 - '@astrojs/svelte@7.1.0(@types/node@24.3.0)(astro@5.13.3(@types/node@24.3.0)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1))(jiti@1.21.7)(stylus@0.64.0)(svelte@5.38.6)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1)': + '@astrojs/svelte@7.1.0(@types/node@24.3.0)(astro@5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1))(jiti@1.21.7)(stylus@0.64.0)(svelte@5.38.6)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1)': dependencies: '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.38.6)(vite@6.3.5(@types/node@24.3.0)(jiti@1.21.7)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.1)) - astro: 5.13.3(@types/node@24.3.0)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1) + astro: 5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1) svelte: 5.38.6 svelte2tsx: 0.7.42(svelte@5.38.6)(typescript@5.9.2) typescript: 5.9.2 @@ -5234,9 +5389,9 @@ snapshots: - tsx - yaml - '@astrojs/tailwind@6.0.2(astro@5.13.3(@types/node@24.3.0)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1))(tailwindcss@3.4.17)': + '@astrojs/tailwind@6.0.2(astro@5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1))(tailwindcss@3.4.17)': dependencies: - astro: 5.13.3(@types/node@24.3.0)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1) + astro: 5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1) autoprefixer: 10.4.21(postcss@8.5.6) postcss: 8.5.6 postcss-load-config: 4.0.2(postcss@8.5.6) @@ -5266,6 +5421,29 @@ snapshots: semver: 7.7.2 vscode-languageserver-textdocument: 1.0.12 + '@astrojs/vercel@8.2.11(astro@5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1))(rollup@2.79.2)(svelte@5.38.6)': + dependencies: + '@astrojs/internal-helpers': 0.7.4 + '@vercel/analytics': 1.6.1(svelte@5.38.6) + '@vercel/functions': 2.2.13 + '@vercel/nft': 0.30.3(rollup@2.79.2) + '@vercel/routing-utils': 5.3.3 + astro: 5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1) + esbuild: 0.25.9 + tinyglobby: 0.2.16 + transitivePeerDependencies: + - '@aws-sdk/credential-provider-web-identity' + - '@remix-run/react' + - '@sveltejs/kit' + - encoding + - next + - react + - rollup + - supports-color + - svelte + - vue + - vue-router + '@astrojs/yaml2ts@0.2.2': dependencies: yaml: 2.8.1 @@ -6428,6 +6606,10 @@ snapshots: wrap-ansi: 8.1.0 wrap-ansi-cjs: wrap-ansi@7.0.0 + '@isaacs/fs-minipass@4.0.1': + dependencies: + minipass: 7.1.3 + '@jridgewell/gen-mapping@0.3.13': dependencies: '@jridgewell/sourcemap-codec': 1.5.5 @@ -6452,6 +6634,19 @@ snapshots: '@jridgewell/resolve-uri': 3.1.2 '@jridgewell/sourcemap-codec': 1.5.5 + '@mapbox/node-pre-gyp@2.0.3': + dependencies: + consola: 3.4.2 + detect-libc: 2.0.4 + https-proxy-agent: 7.0.6 + node-fetch: 2.7.0 + nopt: 8.1.0 + semver: 7.7.2 + tar: 7.5.15 + transitivePeerDependencies: + - encoding + - supports-color + '@nodelib/fs.scandir@2.1.5': dependencies: '@nodelib/fs.stat': 2.0.5 @@ -6914,6 +7109,45 @@ snapshots: optionalDependencies: svelte: 5.38.6 + '@vercel/analytics@1.6.1(svelte@5.38.6)': + optionalDependencies: + svelte: 5.38.6 + + '@vercel/functions@2.2.13': + dependencies: + '@vercel/oidc': 2.0.2 + + '@vercel/nft@0.30.3(rollup@2.79.2)': + dependencies: + '@mapbox/node-pre-gyp': 2.0.3 + '@rollup/pluginutils': 5.2.0(rollup@2.79.2) + acorn: 8.15.0 + acorn-import-attributes: 1.9.5(acorn@8.15.0) + async-sema: 3.1.1 + bindings: 1.5.0 + estree-walker: 2.0.2 + glob: 10.4.5 + graceful-fs: 4.2.11 + node-gyp-build: 4.8.4 + picomatch: 4.0.4 + resolve-from: 5.0.0 + transitivePeerDependencies: + - encoding + - rollup + - supports-color + + '@vercel/oidc@2.0.2': + dependencies: + '@types/ms': 2.1.0 + ms: 2.1.3 + + '@vercel/routing-utils@5.3.3': + dependencies: + path-to-regexp: 6.1.0 + path-to-regexp-updated: path-to-regexp@6.3.0 + optionalDependencies: + ajv: 6.15.0 + '@vercel/speed-insights@1.2.0(svelte@5.38.6)': optionalDependencies: svelte: 5.38.6 @@ -6968,8 +7202,24 @@ snapshots: '@vscode/l10n@0.0.18': {} + abbrev@3.0.1: {} + + acorn-import-attributes@1.9.5(acorn@8.15.0): + dependencies: + acorn: 8.15.0 + acorn@8.15.0: {} + agent-base@7.1.4: {} + + ajv@6.15.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-json-stable-stringify: 2.1.0 + json-schema-traverse: 0.4.1 + uri-js: 4.4.1 + optional: true + ajv@8.17.1: dependencies: fast-deep-equal: 3.1.3 @@ -7025,9 +7275,9 @@ snapshots: get-intrinsic: 1.3.0 is-array-buffer: 3.0.5 - astro-expressive-code@0.41.3(astro@5.13.3(@types/node@24.3.0)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1)): + astro-expressive-code@0.41.3(astro@5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1)): dependencies: - astro: 5.13.3(@types/node@24.3.0)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1) + astro: 5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1) rehype-expressive-code: 0.41.3 astro-icon@1.1.5: @@ -7039,7 +7289,7 @@ snapshots: - debug - supports-color - astro@5.13.3(@types/node@24.3.0)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1): + astro@5.13.3(@types/node@24.3.0)(@vercel/functions@2.2.13)(jiti@1.21.7)(rollup@2.79.2)(stylus@0.64.0)(terser@5.43.1)(typescript@5.9.2)(yaml@2.8.1): dependencies: '@astrojs/compiler': 2.12.2 '@astrojs/internal-helpers': 0.7.2 @@ -7093,7 +7343,7 @@ snapshots: ultrahtml: 1.6.0 unifont: 0.5.2 unist-util-visit: 5.0.0 - unstorage: 1.17.0 + unstorage: 1.17.0(@vercel/functions@2.2.13) vfile: 6.0.3 vite: 6.3.5(@types/node@24.3.0)(jiti@1.21.7)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.1) vitefu: 1.1.1(vite@6.3.5(@types/node@24.3.0)(jiti@1.21.7)(stylus@0.64.0)(terser@5.43.1)(yaml@2.8.1)) @@ -7143,6 +7393,8 @@ snapshots: async-function@1.0.0: {} + async-sema@3.1.1: {} + async@3.2.6: {} asynckit@0.4.0: {} @@ -7222,6 +7474,10 @@ snapshots: binary-extensions@2.3.0: {} + bindings@1.5.0: + dependencies: + file-uri-to-path: 1.0.0 + blob-to-buffer@1.2.9: {} boolbase@1.0.0: {} @@ -7371,6 +7627,8 @@ snapshots: chownr@2.0.0: {} + chownr@3.0.0: {} + ci-info@4.3.0: {} cli-boxes@3.0.0: {} @@ -7431,6 +7689,8 @@ snapshots: confbox@0.2.2: {} + consola@3.4.2: {} + convert-source-map@2.0.0: {} cookie-es@1.2.2: {} @@ -7895,6 +8155,9 @@ snapshots: merge2: 1.4.1 micromatch: 4.0.8 + fast-json-stable-stringify@2.1.0: + optional: true + fast-uri@3.1.0: {} fast-xml-parser@5.2.5: @@ -7913,11 +8176,17 @@ snapshots: optionalDependencies: picomatch: 4.0.3 + fdir@6.5.0(picomatch@4.0.4): + optionalDependencies: + picomatch: 4.0.4 + figures@1.7.0: dependencies: escape-string-regexp: 1.0.5 object-assign: 4.1.1 + file-uri-to-path@1.0.0: {} + filelist@1.0.4: dependencies: minimatch: 5.1.6 @@ -8298,6 +8567,13 @@ snapshots: http-cache-semantics@4.2.0: {} + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + iconv-lite@0.6.3: dependencies: safer-buffer: 2.1.2 @@ -8534,6 +8810,9 @@ snapshots: json-parse-even-better-errors@2.3.1: {} + json-schema-traverse@0.4.1: + optional: true + json-schema-traverse@1.0.0: {} json5@2.2.3: {} @@ -9107,11 +9386,17 @@ snapshots: minipass@7.1.2: {} + minipass@7.1.3: {} + minizlib@2.1.2: dependencies: minipass: 3.3.6 yallist: 4.0.0 + minizlib@3.1.0: + dependencies: + minipass: 7.1.3 + mkdirp@1.0.4: {} mlly@1.8.0: @@ -9156,10 +9441,16 @@ snapshots: dependencies: whatwg-url: 5.0.0 + node-gyp-build@4.8.4: {} + node-mock-http@1.0.2: {} node-releases@2.0.19: {} + nopt@8.1.0: + dependencies: + abbrev: 3.0.1 + normalize-path@3.0.0: {} normalize-range@0.1.2: {} @@ -9337,6 +9628,8 @@ snapshots: lru-cache: 10.4.3 minipass: 7.1.2 + path-to-regexp@6.1.0: {} + path-to-regexp@6.3.0: {} path-type@4.0.0: {} @@ -9355,6 +9648,8 @@ snapshots: picomatch@4.0.3: {} + picomatch@4.0.4: {} + pify@2.3.0: {} pify@5.0.0: {} @@ -9661,6 +9956,9 @@ snapshots: punycode.js@2.3.1: {} + punycode@2.3.1: + optional: true + quansync@0.2.11: {} queue-microtask@1.2.3: {} @@ -10438,6 +10736,14 @@ snapshots: mkdirp: 1.0.4 yallist: 4.0.0 + tar@7.5.15: + dependencies: + '@isaacs/fs-minipass': 4.0.1 + chownr: 3.0.0 + minipass: 7.1.3 + minizlib: 3.1.0 + yallist: 5.0.0 + terser@5.43.1: dependencies: '@jridgewell/source-map': 0.3.11 @@ -10469,6 +10775,11 @@ snapshots: fdir: 6.5.0(picomatch@4.0.3) picomatch: 4.0.3 + tinyglobby@0.2.16: + dependencies: + fdir: 6.5.0(picomatch@4.0.4) + picomatch: 4.0.4 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -10660,7 +10971,7 @@ snapshots: universalify@2.0.1: {} - unstorage@1.17.0: + unstorage@1.17.0(@vercel/functions@2.2.13): dependencies: anymatch: 3.1.3 chokidar: 4.0.3 @@ -10670,6 +10981,8 @@ snapshots: node-fetch-native: 1.6.7 ofetch: 1.4.1 ufo: 1.6.1 + optionalDependencies: + '@vercel/functions': 2.2.13 update-browserslist-db@1.1.3(browserslist@4.25.4): dependencies: @@ -10677,6 +10990,11 @@ snapshots: escalade: 3.2.0 picocolors: 1.1.1 + uri-js@4.4.1: + dependencies: + punycode: 2.3.1 + optional: true + util-deprecate@1.0.2: {} vfile-location@5.0.3: @@ -10917,6 +11235,8 @@ snapshots: yallist@4.0.0: {} + yallist@5.0.0: {} + yaml-language-server@1.15.0: dependencies: ajv: 8.17.1 diff --git a/scripts/run-pagefind.mjs b/scripts/run-pagefind.mjs new file mode 100644 index 0000000..7b345aa --- /dev/null +++ b/scripts/run-pagefind.mjs @@ -0,0 +1,9 @@ +import { execSync } from "node:child_process"; +import { existsSync } from "node:fs"; + +/** + * SSR + Vercel adapter writes prerendered HTML under `dist/client/`. + * Pure static builds use `dist/`. + */ +const site = existsSync("dist/client") ? "dist/client" : "dist"; +execSync(`pagefind --site ${site}`, { stdio: "inherit" }); diff --git a/src/components/ArchivePanel.svelte b/src/components/ArchivePanel.svelte index 080ef94..dfe482d 100644 --- a/src/components/ArchivePanel.svelte +++ b/src/components/ArchivePanel.svelte @@ -3,6 +3,7 @@ import { onMount } from "svelte"; import I18nKey from "../i18n/i18nKey"; import { i18n } from "../i18n/translation"; +import { formatDateToYYYYMMDD, getCalendarYear } from "../utils/date-utils"; import type { SiteLocale } from "../utils/locale-utils"; import { getPostUrlBySlug } from "../utils/url-utils"; @@ -45,12 +46,6 @@ interface Group { let groups: Group[] = []; -function formatDate(date: Date) { - const month = (date.getMonth() + 1).toString().padStart(2, "0"); - const day = date.getDate().toString().padStart(2, "0"); - return `${month}-${day}`; -} - function formatTag(tagList: string[]) { return tagList.map((t) => `#${t}`).join(" "); } @@ -79,7 +74,7 @@ onMount(async () => { const grouped = filteredPosts.reduce( (acc, post) => { - const year = post.data.published.getFullYear(); + const year = getCalendarYear(post.data.published); if (!acc[year]) { acc[year] = []; } @@ -133,7 +128,7 @@ onMount(async () => {
- {formatDate(post.data.published)} + {formatDateToYYYYMMDD(post.data.published)}
diff --git a/src/components/Footer.astro b/src/components/Footer.astro index 7768c32..a330dd4 100644 --- a/src/components/Footer.astro +++ b/src/components/Footer.astro @@ -17,9 +17,7 @@ const locale = getLocaleFromPathname(Astro.url.pathname);
© {currentYear} {profileConfig.name}. {i18n(locale, I18nKey.allRightsReserved)} / {i18n(locale, I18nKey.privacyPolicy)} / - {i18n(locale, I18nKey.termsOfService)} / - RSS / - {i18n(locale, I18nKey.sitemap)}
+ {i18n(locale, I18nKey.termsOfService)}
diff --git a/src/components/LanguageSwitcher.astro b/src/components/LanguageSwitcher.astro index 12b8ca8..dddbc74 100644 --- a/src/components/LanguageSwitcher.astro +++ b/src/components/LanguageSwitcher.astro @@ -1,4 +1,5 @@ --- +import { LOCALE_COOKIE } from "../constants/locale-preference"; import I18nKey from "@i18n/i18nKey"; import { i18n } from "@i18n/translation"; import { getTranslatedPostSlug } from "@utils/content-utils"; @@ -74,6 +75,7 @@ const pillClass = >
+ + diff --git a/src/components/PostCard.astro b/src/components/PostCard.astro index ddfa977..5b9777c 100644 --- a/src/components/PostCard.astro +++ b/src/components/PostCard.astro @@ -61,7 +61,7 @@ const { remarkPluginFrontmatter } = await entry.render(); - +
diff --git a/src/components/PostMeta.astro b/src/components/PostMeta.astro index 38ace88..9dbea0a 100644 --- a/src/components/PostMeta.astro +++ b/src/components/PostMeta.astro @@ -14,6 +14,8 @@ interface Props { category: string | null; hideTagsForMobile?: boolean; hideUpdateDate?: boolean; + /** If set, only this many tags are shown; remainder as +N. */ + maxTags?: number; } const { published, @@ -22,9 +24,16 @@ const { category, hideTagsForMobile = false, hideUpdateDate = false, + maxTags, } = Astro.props; const className = Astro.props.class; const locale = getLocaleFromPathname(Astro.url.pathname); + +const tagList = Array.isArray(tags) ? tags : []; +const visibleTags = + typeof maxTags === "number" && maxTags > 0 ? tagList.slice(0, maxTags) : tagList; +const tagOverflow = + typeof maxTags === "number" && maxTags > 0 ? Math.max(0, tagList.length - maxTags) : 0; ---
@@ -69,16 +78,19 @@ const locale = getLocaleFromPathname(Astro.url.pathname); >
-
- {(tags && tags.length > 0) && tags.map((tag, i) => ( -
/
+
+ {visibleTags.length > 0 && visibleTags.map((tag, i) => ( +
/
+ hover:text-[var(--primary)] dark:hover:text-[var(--primary)] whitespace-nowrap flex-shrink-0"> {tag.trim()} ))} - {!(tags && tags.length > 0) &&
{i18n(locale, I18nKey.noTags)}
} + {tagOverflow > 0 && ( + +{tagOverflow} + )} + {visibleTags.length === 0 &&
{i18n(locale, I18nKey.noTags)}
}
diff --git a/src/components/misc/ImageWrapper.astro b/src/components/misc/ImageWrapper.astro index 472a96d..d05113f 100644 --- a/src/components/misc/ImageWrapper.astro +++ b/src/components/misc/ImageWrapper.astro @@ -13,7 +13,7 @@ interface Props { import { Image } from "astro:assets"; import { url } from "../../utils/url-utils"; -const { id, src, alt, position = "center", basePath = "/" } = Astro.props; +const { id, src, alt, position = "center", basePath = "" } = Astro.props; const className = Astro.props.class; const isLocal = !( @@ -24,6 +24,11 @@ const isLocal = !( ); const isPublic = src.startsWith("/"); +const safeBase = + basePath === "/" || basePath === undefined || basePath === null + ? "" + : basePath; + // TODO temporary workaround for images dynamic import // https://github.com/withastro/astro/issues/3373 // biome-ignore lint/suspicious/noImplicitAnyLet: @@ -33,14 +38,19 @@ if (isLocal) { import: "default", }); let normalizedPath = path - .normalize(path.join("../../", basePath, src)) + .normalize(path.join("../../", safeBase, src)) + .replace(/\\/g, "/"); + const fallbackPath = path + .normalize(path.join("../../", "assets/images/banner_default.png")) .replace(/\\/g, "/"); - const file = files[normalizedPath]; + let file = files[normalizedPath]; if (!file) { console.error( `\n[ERROR] Image file not found: ${normalizedPath.replace("../../", "src/")}`, ); - } else { + file = files[fallbackPath]; + } + if (file) { img = await file(); } } diff --git a/src/constants/locale-preference.ts b/src/constants/locale-preference.ts new file mode 100644 index 0000000..6f0d592 --- /dev/null +++ b/src/constants/locale-preference.ts @@ -0,0 +1,4 @@ +/** Cookie set when user picks VI/EN or when `/` infers locale (middleware). */ +export const LOCALE_COOKIE = "boospace_locale"; + +export type StoredLocale = "vi" | "en"; diff --git a/src/layouts/Layout.astro b/src/layouts/Layout.astro index 5652fe1..22cffcd 100644 --- a/src/layouts/Layout.astro +++ b/src/layouts/Layout.astro @@ -6,6 +6,7 @@ import "@fontsource/roboto/700.css"; import ConfigCarrier from "@components/ConfigCarrier.astro"; import { getSiteSubtitle } from "@i18n/site-copy"; import { profileConfig, siteConfig } from "@/config"; +import { resolveSiteAssetOgImageUrl } from "../utils/og-image"; import { AUTO_MODE, BANNER_HEIGHT, @@ -39,6 +40,10 @@ interface Props { setOGTypeArticle?: boolean; canonicalPath?: string; alternates?: Partial>; + /** Open Graph / Twitter image (absolute URL). */ + ogImage?: string; + /** Prefer raw article title for `og:title` (e.g. without site suffix). */ + socialTitle?: string; } let { @@ -49,6 +54,8 @@ let { setOGTypeArticle, canonicalPath, alternates, + ogImage, + socialTitle, } = Astro.props; const locale = getLocaleFromPathname(Astro.url.pathname); @@ -75,6 +82,7 @@ if (title) { } else { pageTitle = `${siteConfig.title} - ${getSiteSubtitle(locale)}`; } +const ogTitle = socialTitle?.trim() ? socialTitle.trim() : pageTitle; const favicons: Favicon[] = siteConfig.favicon.length > 0 ? siteConfig.favicon : defaultFavicons; @@ -107,6 +115,13 @@ const bannerOffsetByPosition = { const bannerOffset = bannerOffsetByPosition[siteConfig.banner.position || "center"]; +let resolvedOgImage = ogImage; +if (!resolvedOgImage && Astro.site) { + resolvedOgImage = + (await resolveSiteAssetOgImageUrl(Astro.site, siteConfig.banner.src)) ?? + undefined; +} + // Inject Vercel scripts for Astro inject(); injectSpeedInsights(); @@ -204,8 +219,9 @@ injectSpeedInsights(); - + + {resolvedOgImage && } {setOGTypeArticle ? ( ) : ( @@ -214,8 +230,9 @@ injectSpeedInsights(); - + + {resolvedOgImage && } @@ -266,7 +283,7 @@ injectSpeedInsights(); - {/* Google Tag Manager (noscript) */} @@ -492,10 +509,10 @@ function showBanner() { function init() { // disableAnimation()() // TODO + showBanner(); loadTheme(); loadHue(); initCustomScrollbar(); - showBanner(); } /* Load settings when entering the site */ @@ -524,7 +541,7 @@ const setup = () => { } let threshold = window.innerHeight * (BANNER_HEIGHT / 100) - 72 - 16 let navbar = document.getElementById('navbar-wrapper') - if (!navbar || !document.body.classList.contains('lg:is-home')) { + if (!navbar || !document.body.classList.contains('is-home')) { return } if (document.body.scrollTop >= threshold || document.documentElement.scrollTop >= threshold) { @@ -536,9 +553,9 @@ const setup = () => { // change banner height immediately when a link is clicked const bodyElement = document.querySelector('body') if (stripLocaleFromPathname(new URL(visit.to.url, window.location.origin).pathname) === '/') { - bodyElement!.classList.add('lg:is-home'); + bodyElement!.classList.add('is-home'); } else { - bodyElement!.classList.remove('lg:is-home'); + bodyElement!.classList.remove('is-home'); } // increase the page height during page transition to prevent the scrolling animation from jumping @@ -609,7 +626,7 @@ function scrollFunction() { const MAIN_PANEL_EXCESS_HEIGHT = MAIN_PANEL_OVERLAPS_BANNER_HEIGHT * 16 // The height the main panel overlaps the banner let bannerHeight = BANNER_HEIGHT - if (document.body.classList.contains('lg:is-home') && window.innerWidth >= 1024) { + if (document.body.classList.contains('is-home') && window.innerWidth >= 1024) { bannerHeight = BANNER_HEIGHT_HOME } let threshold = window.innerHeight * (bannerHeight / 100) - NAVBAR_HEIGHT - MAIN_PANEL_EXCESS_HEIGHT - 16 diff --git a/src/layouts/MainGridLayout.astro b/src/layouts/MainGridLayout.astro index 0b20edf..5d1759e 100644 --- a/src/layouts/MainGridLayout.astro +++ b/src/layouts/MainGridLayout.astro @@ -26,6 +26,8 @@ interface Props { headings?: MarkdownHeading[]; canonicalPath?: string; alternates?: Partial>; + ogImage?: string; + socialTitle?: string; } const { @@ -37,6 +39,8 @@ const { headings = [], canonicalPath, alternates, + ogImage, + socialTitle, } = Astro.props; const hasBannerCredit = siteConfig.banner.enable && siteConfig.banner.credit.enable; @@ -48,7 +52,7 @@ const mainPanelTop = siteConfig.banner.enable : "5.5rem"; --- - + diff --git a/src/middleware.ts b/src/middleware.ts new file mode 100644 index 0000000..12ef95d --- /dev/null +++ b/src/middleware.ts @@ -0,0 +1,90 @@ +import { defineMiddleware } from "astro:middleware"; + +import { + LOCALE_COOKIE, + type StoredLocale, +} from "./constants/locale-preference"; + +const ONE_YEAR_SEC = 60 * 60 * 24 * 365; + +function isSiteRoot(pathname: string): boolean { + const normalized = pathname.replace(/\/+$/, ""); + return normalized === "" || normalized === "/"; +} + +function parseCookie(header: string | null, name: string): string | null { + if (!header) return null; + for (const part of header.split(";")) { + const idx = part.indexOf("="); + if (idx === -1) continue; + const k = part.slice(0, idx).trim(); + if (k !== name) continue; + return decodeURIComponent(part.slice(idx + 1).trim()); + } + return null; +} + +function localeFromAcceptLanguage(header: string | null): StoredLocale | null { + if (!header) return null; + const items = header.split(",").map((part) => { + const [tag, ...params] = part.trim().split(";"); + const qParam = params.find((x) => + x.trim().toLowerCase().startsWith("q="), + ); + const q = qParam ? Number.parseFloat(qParam.split("=")[1] ?? "") : 1; + return { tag: tag.trim().toLowerCase(), q: Number.isFinite(q) ? q : 1 }; + }); + items.sort((a, b) => b.q - a.q); + for (const { tag } of items) { + const base = tag.split("-")[0] ?? ""; + if (base === "vi") return "vi"; + if (base === "en") return "en"; + } + return null; +} + +function inferLocale(request: Request): StoredLocale { + const fromCookie = parseCookie(request.headers.get("cookie"), LOCALE_COOKIE); + if (fromCookie === "vi" || fromCookie === "en") return fromCookie; + + const fromAl = localeFromAcceptLanguage( + request.headers.get("accept-language"), + ); + if (fromAl) return fromAl; + + const country = + request.headers.get("x-vercel-ip-country")?.trim() || + request.headers.get("cf-ipcountry")?.trim() || + ""; + if (country === "VN") return "vi"; + + return "en"; +} + +function redirectToLocale( + url: URL, + locale: StoredLocale, +): Response { + const path = locale === "vi" ? "/vn/" : "/en/"; + const location = new URL(path, url.origin).href; + const secure = url.protocol === "https:"; + const cookie = `${LOCALE_COOKIE}=${locale}; Path=/; Max-Age=${ONE_YEAR_SEC}; SameSite=Lax${secure ? "; Secure" : ""}`; + + return new Response(null, { + status: 302, + headers: { + Location: location, + "Set-Cookie": cookie, + "Cache-Control": "private, no-store", + Vary: "Cookie, Accept-Language", + }, + }); +} + +export const onRequest = defineMiddleware((context, next) => { + if (!isSiteRoot(context.url.pathname)) { + return next(); + } + const locale = inferLocale(context.request); + return redirectToLocale(context.url, locale); +}); diff --git a/src/pages/[...page].astro b/src/pages/[...page].astro deleted file mode 100644 index f5a5de4..0000000 --- a/src/pages/[...page].astro +++ /dev/null @@ -1,23 +0,0 @@ ---- -import type { GetStaticPaths } from "astro"; -import Pagination from "../components/control/Pagination.astro"; -import PostPage from "../components/PostPage.astro"; -import { PAGE_SIZE } from "../constants/constants"; -import MainGridLayout from "../layouts/MainGridLayout.astro"; -import { getSortedPosts } from "../utils/content-utils"; - -export const getStaticPaths = (async ({ paginate }) => { - const allBlogPosts = await getSortedPosts(); - return paginate(allBlogPosts, { pageSize: PAGE_SIZE }); -}) satisfies GetStaticPaths; -// https://github.com/withastro/astro/issues/6507#issuecomment-1489916992 - -const { page } = Astro.props; - -const len = page.data.length; ---- - - - - - \ No newline at end of file diff --git a/src/pages/about.astro b/src/pages/about.astro index 7d8c442..a3cf115 100644 --- a/src/pages/about.astro +++ b/src/pages/about.astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import { render } from "astro:content"; import Markdown from "@components/misc/Markdown.astro"; diff --git a/src/pages/archive.astro b/src/pages/archive.astro index 798148c..34841f7 100644 --- a/src/pages/archive.astro +++ b/src/pages/archive.astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import ArchivePanel from "@components/ArchivePanel.svelte"; import I18nKey from "@i18n/i18nKey"; import { i18n } from "@i18n/translation"; diff --git a/src/pages/en/[page].astro b/src/pages/en/[page].astro index 59b3108..8eeb24c 100644 --- a/src/pages/en/[page].astro +++ b/src/pages/en/[page].astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import Pagination from "@components/control/Pagination.astro"; import PostPage from "@components/PostPage.astro"; import { PAGE_SIZE } from "@constants/constants"; diff --git a/src/pages/en/about.astro b/src/pages/en/about.astro index 69f965e..2d5e568 100644 --- a/src/pages/en/about.astro +++ b/src/pages/en/about.astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import { render } from "astro:content"; import Markdown from "@components/misc/Markdown.astro"; import I18nKey from "@i18n/i18nKey"; diff --git a/src/pages/en/archive.astro b/src/pages/en/archive.astro index ef99bc5..fbba27c 100644 --- a/src/pages/en/archive.astro +++ b/src/pages/en/archive.astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import ArchivePanel from "@components/ArchivePanel.svelte"; import I18nKey from "@i18n/i18nKey"; import { i18n } from "@i18n/translation"; diff --git a/src/pages/en/index.astro b/src/pages/en/index.astro index 670de97..921254c 100644 --- a/src/pages/en/index.astro +++ b/src/pages/en/index.astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import Pagination from "@components/control/Pagination.astro"; import PostPage from "@components/PostPage.astro"; import { PAGE_SIZE } from "@constants/constants"; diff --git a/src/pages/en/login.astro b/src/pages/en/login.astro index 4eecd59..c1aa5cc 100644 --- a/src/pages/en/login.astro +++ b/src/pages/en/login.astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import TikTokLoginConsole from "@components/TikTokLoginConsole.svelte"; import MainGridLayout from "@layouts/MainGridLayout.astro"; --- diff --git a/src/pages/en/posts/[...slug].astro b/src/pages/en/posts/[...slug].astro index 3a0c883..954009e 100644 --- a/src/pages/en/posts/[...slug].astro +++ b/src/pages/en/posts/[...slug].astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import path from "node:path"; import License from "@components/misc/License.astro"; import Markdown from "@components/misc/Markdown.astro"; @@ -13,6 +14,7 @@ import { } from "@utils/content-utils"; import type { SiteLocale } from "@utils/locale-utils"; import { getDir, getPostUrlBySlug } from "@utils/url-utils"; +import { resolvePostOgImageUrl } from "@utils/og-image"; import { Icon } from "astro-icon/components"; import { licenseConfig } from "@/config"; import ImageWrapper from "../../../components/misc/ImageWrapper.astro"; @@ -38,6 +40,12 @@ const { remarkPluginFrontmatter } = await entry.render(); const hasCustomCover = typeof entry.data.image === "string" && entry.data.image.trim() !== ""; const fallbackCoverImage = "assets/images/banner_default.png"; +const postAssetBase = path.join("content/posts/", getDir(entry.id)); +const ogImage = await resolvePostOgImageUrl( + Astro.site, + hasCustomCover ? entry.data.image : undefined, + postAssetBase, +); const jsonLd = { "@context": "https://schema.org", @@ -51,6 +59,10 @@ const jsonLd = { url: Astro.site, }, datePublished: formatDateToYYYYMMDD(entry.data.published), + ...(entry.data.updated && + entry.data.updated.getTime() !== entry.data.published.getTime() + ? { dateModified: formatDateToYYYYMMDD(entry.data.updated) } + : {}), inLanguage: getEntryLocale(entry).replace("_", "-"), }; @@ -62,7 +74,7 @@ if (translatedSlug) { alternates.vi = getPostUrlBySlug(translatedSlug, "vi"); } --- - +
diff --git a/src/pages/en/privacy-policy.astro b/src/pages/en/privacy-policy.astro index 3bb1de8..7e353d3 100644 --- a/src/pages/en/privacy-policy.astro +++ b/src/pages/en/privacy-policy.astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import SpecDocumentPage from "@components/SpecDocumentPage.astro"; --- diff --git a/src/pages/en/terms-of-service.astro b/src/pages/en/terms-of-service.astro index c5baeef..818974e 100644 --- a/src/pages/en/terms-of-service.astro +++ b/src/pages/en/terms-of-service.astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import SpecDocumentPage from "@components/SpecDocumentPage.astro"; --- diff --git a/src/pages/login.astro b/src/pages/login.astro index c132312..622e711 100644 --- a/src/pages/login.astro +++ b/src/pages/login.astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import TikTokLoginConsole from "@components/TikTokLoginConsole.svelte"; import MainGridLayout from "@layouts/MainGridLayout.astro"; --- diff --git a/src/pages/posts/[...slug].astro b/src/pages/posts/[...slug].astro index 0b19875..7b22d31 100644 --- a/src/pages/posts/[...slug].astro +++ b/src/pages/posts/[...slug].astro @@ -1,4 +1,5 @@ --- +export const prerender = true; import path from "node:path"; import License from "@components/misc/License.astro"; import Markdown from "@components/misc/Markdown.astro"; @@ -13,6 +14,7 @@ import { } from "@utils/content-utils"; import { getLocaleFromPathname, type SiteLocale } from "@utils/locale-utils"; import { getDir, getPostUrlBySlug } from "@utils/url-utils"; +import { resolvePostOgImageUrl } from "@utils/og-image"; import { Icon } from "astro-icon/components"; import { licenseConfig } from "@/config"; import ImageWrapper from "../../components/misc/ImageWrapper.astro"; @@ -40,6 +42,12 @@ const { remarkPluginFrontmatter } = await entry.render(); const hasCustomCover = typeof entry.data.image === "string" && entry.data.image.trim() !== ""; const fallbackCoverImage = "assets/images/banner_default.png"; +const postAssetBase = path.join("content/posts/", getDir(entry.id)); +const ogImage = await resolvePostOgImageUrl( + Astro.site, + hasCustomCover ? entry.data.image : undefined, + postAssetBase, +); const jsonLd = { "@context": "https://schema.org", @@ -53,6 +61,10 @@ const jsonLd = { url: Astro.site, }, datePublished: formatDateToYYYYMMDD(entry.data.published), + ...(entry.data.updated && + entry.data.updated.getTime() !== entry.data.published.getTime() + ? { dateModified: formatDateToYYYYMMDD(entry.data.updated) } + : {}), inLanguage: getEntryLocale(entry).replace("_", "-"), // TODO include cover image here }; @@ -67,7 +79,7 @@ if (translatedSlug) { alternates.en = getPostUrlBySlug(translatedSlug, "en"); } --- - +