mirror of
https://github.com/timmypidashev/web.git
synced 2026-04-14 02:53:51 +00:00
Update blog metrics; add vercel imsights
This commit is contained in:
@@ -18,14 +18,17 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/mdx": "^5.0.3",
|
||||
"@astrojs/vercel": "^10.0.3",
|
||||
"@astrojs/rss": "^4.0.18",
|
||||
"@astrojs/sitemap": "^3.7.2",
|
||||
"@astrojs/vercel": "^10.0.3",
|
||||
"@giscus/react": "^3.1.0",
|
||||
"@pilcrowjs/object-parser": "^0.0.4",
|
||||
"@react-hook/intersection-observer": "^3.1.2",
|
||||
"@rehype-pretty/transformers": "^0.13.2",
|
||||
"@vercel/analytics": "^2.0.1",
|
||||
"@vercel/speed-insights": "^2.0.0",
|
||||
"arctic": "^3.7.0",
|
||||
"ioredis": "^5.10.1",
|
||||
"lucide-react": "^0.468.0",
|
||||
"marked": "^15.0.12",
|
||||
"react": "^18.3.1",
|
||||
|
||||
183
pnpm-lock.yaml
generated
183
pnpm-lock.yaml
generated
@@ -10,7 +10,7 @@ importers:
|
||||
dependencies:
|
||||
'@astrojs/mdx':
|
||||
specifier: ^5.0.3
|
||||
version: 5.0.3(astro@6.1.2(@types/node@24.12.0)(@vercel/functions@3.4.3)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))
|
||||
version: 5.0.3(astro@6.1.2(@types/node@24.12.0)(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))
|
||||
'@astrojs/rss':
|
||||
specifier: ^4.0.18
|
||||
version: 4.0.18
|
||||
@@ -19,7 +19,7 @@ importers:
|
||||
version: 3.7.2
|
||||
'@astrojs/vercel':
|
||||
specifier: ^10.0.3
|
||||
version: 10.0.3(astro@6.1.2(@types/node@24.12.0)(@vercel/functions@3.4.3)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))(react@18.3.1)(rollup@4.60.1)
|
||||
version: 10.0.3(astro@6.1.2(@types/node@24.12.0)(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))(react@18.3.1)(rollup@4.60.1)
|
||||
'@giscus/react':
|
||||
specifier: ^3.1.0
|
||||
version: 3.1.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
|
||||
@@ -32,9 +32,18 @@ importers:
|
||||
'@rehype-pretty/transformers':
|
||||
specifier: ^0.13.2
|
||||
version: 0.13.2
|
||||
'@vercel/analytics':
|
||||
specifier: ^2.0.1
|
||||
version: 2.0.1(react@18.3.1)
|
||||
'@vercel/speed-insights':
|
||||
specifier: ^2.0.0
|
||||
version: 2.0.0(react@18.3.1)
|
||||
arctic:
|
||||
specifier: ^3.7.0
|
||||
version: 3.7.0
|
||||
ioredis:
|
||||
specifier: ^5.10.1
|
||||
version: 5.10.1
|
||||
lucide-react:
|
||||
specifier: ^0.468.0
|
||||
version: 0.468.0(react@18.3.1)
|
||||
@@ -80,7 +89,7 @@ importers:
|
||||
version: 5.0.2(@types/node@24.12.0)(@types/react-dom@18.3.7(@types/react@18.3.28))(@types/react@18.3.28)(jiti@1.21.7)(react-dom@18.3.1(react@18.3.1))(react@18.3.1)(yaml@2.8.3)
|
||||
'@astrojs/tailwind':
|
||||
specifier: ^6.0.2
|
||||
version: 6.0.2(astro@6.1.2(@types/node@24.12.0)(@vercel/functions@3.4.3)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))(tailwindcss@3.4.19(yaml@2.8.3))
|
||||
version: 6.0.2(astro@6.1.2(@types/node@24.12.0)(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))(tailwindcss@3.4.19(yaml@2.8.3))
|
||||
'@tailwindcss/typography':
|
||||
specifier: ^0.5.19
|
||||
version: 0.5.19(tailwindcss@3.4.19(yaml@2.8.3))
|
||||
@@ -92,7 +101,7 @@ importers:
|
||||
version: 18.3.7(@types/react@18.3.28)
|
||||
astro:
|
||||
specifier: ^6.1.2
|
||||
version: 6.1.2(@types/node@24.12.0)(@vercel/functions@3.4.3)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3)
|
||||
version: 6.1.2(@types/node@24.12.0)(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3)
|
||||
tailwindcss:
|
||||
specifier: ^3.4.19
|
||||
version: 3.4.19(yaml@2.8.3)
|
||||
@@ -563,6 +572,9 @@ packages:
|
||||
cpu: [x64]
|
||||
os: [win32]
|
||||
|
||||
'@ioredis/commands@1.5.1':
|
||||
resolution: {integrity: sha512-JH8ZL/ywcJyR9MmJ5BNqZllXNZQqQbnVZOqpPQqE1vHiFgAw4NHbvE0FOduNU8IX9babitBT46571OnPTT0Zcw==}
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
resolution: {integrity: sha512-wgm9Ehl2jpeqP3zw/7mo3kRHFp5MEDhqAdwy1fTGkHAwnkGOVsgpvQhL8B5n1qlb01jV3n/bI0ZfZp5lWA1k4w==}
|
||||
engines: {node: '>=18.0.0'}
|
||||
@@ -913,6 +925,9 @@ packages:
|
||||
'@ungap/structured-clone@1.3.0':
|
||||
resolution: {integrity: sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==}
|
||||
|
||||
'@upstash/redis@1.37.0':
|
||||
resolution: {integrity: sha512-LqOJ3+XWPLSZ2rGSed5DYG3ixybxb8EhZu3yQqF7MdZX1wLBG/FRcI6xcUZXHy/SS7mmXWyadrud0HJHkOc+uw==}
|
||||
|
||||
'@vercel/analytics@1.6.1':
|
||||
resolution: {integrity: sha512-oH9He/bEM+6oKlv3chWuOOcp8Y6fo6/PSro8hEkgCW3pu9/OiCXiUpRUogDh3Fs3LH2sosDrx8CxeOLBEE+afg==}
|
||||
peerDependencies:
|
||||
@@ -939,6 +954,35 @@ packages:
|
||||
vue-router:
|
||||
optional: true
|
||||
|
||||
'@vercel/analytics@2.0.1':
|
||||
resolution: {integrity: sha512-MTQG6V9qQrt1tsDeF+2Uoo5aPjqbVPys1xvnIftXSJYG2SrwXRHnqEvVoYID7BTruDz4lCd2Z7rM1BdkUehk2g==}
|
||||
peerDependencies:
|
||||
'@remix-run/react': ^2
|
||||
'@sveltejs/kit': ^1 || ^2
|
||||
next: '>= 13'
|
||||
nuxt: '>= 3'
|
||||
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
|
||||
nuxt:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
svelte:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
vue-router:
|
||||
optional: true
|
||||
|
||||
'@vercel/functions@3.4.3':
|
||||
resolution: {integrity: sha512-kA14KIUVgAY6VXbhZ5jjY+s0883cV3cZqIU3WhrSRxuJ9KvxatMjtmzl0K23HK59oOUjYl7HaE/eYMmhmqpZzw==}
|
||||
engines: {node: '>= 20'}
|
||||
@@ -948,6 +992,11 @@ packages:
|
||||
'@aws-sdk/credential-provider-web-identity':
|
||||
optional: true
|
||||
|
||||
'@vercel/kv@3.0.0':
|
||||
resolution: {integrity: sha512-pKT8fRnfyYk2MgvyB6fn6ipJPCdfZwiKDdw7vB+HL50rjboEBHDVBEcnwfkEpVSp2AjNtoaOUH7zG+bVC/rvSg==}
|
||||
engines: {node: '>=14.6'}
|
||||
deprecated: 'Vercel KV is deprecated. If you had an existing KV store, it should have moved to Upstash Redis which you will see under Vercel Integrations. For new projects, install a Redis integration from Vercel Marketplace: https://vercel.com/marketplace?category=storage&search=redis'
|
||||
|
||||
'@vercel/nft@1.5.0':
|
||||
resolution: {integrity: sha512-IWTDeIoWhQ7ZtRO/JRKH+jhmeQvZYhtGPmzw/QGDY+wDCQqfm25P9yIdoAFagu4fWsK4IwZXDFIjrmp5rRm/sA==}
|
||||
engines: {node: '>=20'}
|
||||
@@ -960,6 +1009,32 @@ packages:
|
||||
'@vercel/routing-utils@5.3.3':
|
||||
resolution: {integrity: sha512-KYm2sLNUD48gDScv8ob4ejc3Gww2jcJyW80hTdYlenAPz/5BQar1Gyh38xrUuZ532TUwSb5mV1uRbAuiykq0EQ==}
|
||||
|
||||
'@vercel/speed-insights@2.0.0':
|
||||
resolution: {integrity: sha512-jwkNcrTeafWxjmWq4AHBaptSqZiJkYU5adLC9QBSqeim0GcqDMgN5Ievh8OG1rJ6W3A4l1oiP7qr9CWxGuzu3w==}
|
||||
peerDependencies:
|
||||
'@sveltejs/kit': ^1 || ^2
|
||||
next: '>= 13'
|
||||
nuxt: '>= 3'
|
||||
react: ^18 || ^19 || ^19.0.0-rc
|
||||
svelte: '>= 4'
|
||||
vue: ^3
|
||||
vue-router: ^4
|
||||
peerDependenciesMeta:
|
||||
'@sveltejs/kit':
|
||||
optional: true
|
||||
next:
|
||||
optional: true
|
||||
nuxt:
|
||||
optional: true
|
||||
react:
|
||||
optional: true
|
||||
svelte:
|
||||
optional: true
|
||||
vue:
|
||||
optional: true
|
||||
vue-router:
|
||||
optional: true
|
||||
|
||||
'@vitejs/plugin-react@5.2.0':
|
||||
resolution: {integrity: sha512-YmKkfhOAi3wsB1PhJq5Scj3GXMn3WvtQ/JC0xoopuHoXSdmtdStOpFrYaT1kie2YgFBcIe64ROzMYRjCrYOdYw==}
|
||||
engines: {node: ^20.19.0 || >=22.12.0}
|
||||
@@ -1115,6 +1190,10 @@ packages:
|
||||
resolution: {integrity: sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==}
|
||||
engines: {node: '>=6'}
|
||||
|
||||
cluster-key-slot@1.1.2:
|
||||
resolution: {integrity: sha512-RMr0FhtfXemyinomL4hrWcYJxmX6deFdCxpJzhDttxgO1+bcCnkk+9drydLVDmAMG7NE6aN/fl4F7ucU/90gAA==}
|
||||
engines: {node: '>=0.10.0'}
|
||||
|
||||
collapse-white-space@2.1.0:
|
||||
resolution: {integrity: sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==}
|
||||
|
||||
@@ -1195,6 +1274,10 @@ packages:
|
||||
defu@6.1.4:
|
||||
resolution: {integrity: sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==}
|
||||
|
||||
denque@2.1.0:
|
||||
resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==}
|
||||
engines: {node: '>=0.10'}
|
||||
|
||||
dequal@2.0.3:
|
||||
resolution: {integrity: sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -1457,6 +1540,10 @@ packages:
|
||||
resolution: {integrity: sha512-fn4bQ0Xq8FTej09YC/jqKZwtijpvARlRp6wxL5WTA6yPe2YWSJ5RJh7Nm79rK2qB0wr6iDQzH60XGq5V/7u8YQ==}
|
||||
deprecated: The Intersection Observer polyfill is no longer needed and can safely be removed. Intersection Observer has been Baseline since 2019.
|
||||
|
||||
ioredis@5.10.1:
|
||||
resolution: {integrity: sha512-HuEDBTI70aYdx1v6U97SbNx9F1+svQKBDo30o0b9fw055LMepzpOOd0Ccg9Q6tbqmBSJaMuY0fB7yw9/vjBYCA==}
|
||||
engines: {node: '>=12.22.0'}
|
||||
|
||||
iron-webcrypto@1.2.1:
|
||||
resolution: {integrity: sha512-feOM6FaSr6rEABp/eDfVseKyTMDt+KGpeB35SkVn9Tyn0CqvVsY3EwI0v5i8nMHyJnzCIQf7nsy3p41TPkJZhg==}
|
||||
|
||||
@@ -1550,6 +1637,12 @@ packages:
|
||||
lit@3.3.2:
|
||||
resolution: {integrity: sha512-NF9zbsP79l4ao2SNrH3NkfmFgN/hBYSQo90saIVI1o5GpjAdCPVstVzO1MrLOakHoEhYkrtRjPK6Ob521aoYWQ==}
|
||||
|
||||
lodash.defaults@4.2.0:
|
||||
resolution: {integrity: sha512-qjxPLHd3r5DnsdGacqOMU6pb/avJzdh9tFX2ymgoZE27BmjXrNy/y4LoaiTeAb+O3gL8AfpJGtqfX/ae2leYYQ==}
|
||||
|
||||
lodash.isarguments@3.1.0:
|
||||
resolution: {integrity: sha512-chi4NHZlZqZD18a0imDHnZPrDeBbTtVN7GXMwuGdRH9qotxAjYs3aVLKc7zNOG9eddR5Ksd8rvFEBc9SsggPpg==}
|
||||
|
||||
longest-streak@3.1.0:
|
||||
resolution: {integrity: sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==}
|
||||
|
||||
@@ -2061,6 +2154,14 @@ packages:
|
||||
recma-stringify@1.0.0:
|
||||
resolution: {integrity: sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==}
|
||||
|
||||
redis-errors@1.2.0:
|
||||
resolution: {integrity: sha512-1qny3OExCf0UvUV/5wpYKf2YwPcOqXzkwKKSmKHiE6ZMQs5heeE/c8eXK+PNllPvmjgAbfnsbpkGZWy8cBpn9w==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
redis-parser@3.0.0:
|
||||
resolution: {integrity: sha512-DJnGAeenTdpMEH6uAJRK/uiyEIH9WVsUmoLwzudwGJUwZPp80PDBWPHXSAGNPwNvIXAbe7MSUB1zQFugFml66A==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
regex-recursion@6.0.2:
|
||||
resolution: {integrity: sha512-0YCaSCq2VRIebiaUviZNs0cBz1kg5kVS2UKUfNIx8YVs1cN3AV7NTctO5FOKBA+UT2BPJIWZauYHPqJODG50cg==}
|
||||
|
||||
@@ -2202,6 +2303,9 @@ packages:
|
||||
space-separated-tokens@2.0.2:
|
||||
resolution: {integrity: sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==}
|
||||
|
||||
standard-as-callback@2.1.0:
|
||||
resolution: {integrity: sha512-qoRRSyROncaz1z0mvYqIE4lCd9p2R90i6GxW3uZv5ucSu8tU7B5HXUP1gG8pVZsYNVaXjk8ClXHPttLyxAL48A==}
|
||||
|
||||
stream-replace-string@2.0.0:
|
||||
resolution: {integrity: sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==}
|
||||
|
||||
@@ -2559,12 +2663,12 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@astrojs/mdx@5.0.3(astro@6.1.2(@types/node@24.12.0)(@vercel/functions@3.4.3)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))':
|
||||
'@astrojs/mdx@5.0.3(astro@6.1.2(@types/node@24.12.0)(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@astrojs/markdown-remark': 7.1.0
|
||||
'@mdx-js/mdx': 3.1.1
|
||||
acorn: 8.16.0
|
||||
astro: 6.1.2(@types/node@24.12.0)(@vercel/functions@3.4.3)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3)
|
||||
astro: 6.1.2(@types/node@24.12.0)(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3)
|
||||
es-module-lexer: 2.0.0
|
||||
estree-util-visit: 2.0.0
|
||||
hast-util-to-html: 9.0.5
|
||||
@@ -2619,9 +2723,9 @@ snapshots:
|
||||
stream-replace-string: 2.0.0
|
||||
zod: 4.3.6
|
||||
|
||||
'@astrojs/tailwind@6.0.2(astro@6.1.2(@types/node@24.12.0)(@vercel/functions@3.4.3)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))(tailwindcss@3.4.19(yaml@2.8.3))':
|
||||
'@astrojs/tailwind@6.0.2(astro@6.1.2(@types/node@24.12.0)(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))(tailwindcss@3.4.19(yaml@2.8.3))':
|
||||
dependencies:
|
||||
astro: 6.1.2(@types/node@24.12.0)(@vercel/functions@3.4.3)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3)
|
||||
astro: 6.1.2(@types/node@24.12.0)(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3)
|
||||
autoprefixer: 10.4.27(postcss@8.5.8)
|
||||
postcss: 8.5.8
|
||||
postcss-load-config: 4.0.2(postcss@8.5.8)
|
||||
@@ -2641,14 +2745,14 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
'@astrojs/vercel@10.0.3(astro@6.1.2(@types/node@24.12.0)(@vercel/functions@3.4.3)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))(react@18.3.1)(rollup@4.60.1)':
|
||||
'@astrojs/vercel@10.0.3(astro@6.1.2(@types/node@24.12.0)(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3))(react@18.3.1)(rollup@4.60.1)':
|
||||
dependencies:
|
||||
'@astrojs/internal-helpers': 0.8.0
|
||||
'@vercel/analytics': 1.6.1(react@18.3.1)
|
||||
'@vercel/functions': 3.4.3
|
||||
'@vercel/nft': 1.5.0(rollup@4.60.1)
|
||||
'@vercel/routing-utils': 5.3.3
|
||||
astro: 6.1.2(@types/node@24.12.0)(@vercel/functions@3.4.3)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3)
|
||||
astro: 6.1.2(@types/node@24.12.0)(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3)
|
||||
esbuild: 0.27.4
|
||||
tinyglobby: 0.2.15
|
||||
transitivePeerDependencies:
|
||||
@@ -2975,6 +3079,8 @@ snapshots:
|
||||
'@img/sharp-win32-x64@0.34.5':
|
||||
optional: true
|
||||
|
||||
'@ioredis/commands@1.5.1': {}
|
||||
|
||||
'@isaacs/fs-minipass@4.0.1':
|
||||
dependencies:
|
||||
minipass: 7.1.3
|
||||
@@ -3327,14 +3433,28 @@ snapshots:
|
||||
|
||||
'@ungap/structured-clone@1.3.0': {}
|
||||
|
||||
'@upstash/redis@1.37.0':
|
||||
dependencies:
|
||||
uncrypto: 0.1.3
|
||||
optional: true
|
||||
|
||||
'@vercel/analytics@1.6.1(react@18.3.1)':
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
|
||||
'@vercel/analytics@2.0.1(react@18.3.1)':
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
|
||||
'@vercel/functions@3.4.3':
|
||||
dependencies:
|
||||
'@vercel/oidc': 3.2.0
|
||||
|
||||
'@vercel/kv@3.0.0':
|
||||
dependencies:
|
||||
'@upstash/redis': 1.37.0
|
||||
optional: true
|
||||
|
||||
'@vercel/nft@1.5.0(rollup@4.60.1)':
|
||||
dependencies:
|
||||
'@mapbox/node-pre-gyp': 2.0.3
|
||||
@@ -3363,6 +3483,10 @@ snapshots:
|
||||
optionalDependencies:
|
||||
ajv: 6.14.0
|
||||
|
||||
'@vercel/speed-insights@2.0.0(react@18.3.1)':
|
||||
optionalDependencies:
|
||||
react: 18.3.1
|
||||
|
||||
'@vitejs/plugin-react@5.2.0(vite@7.3.1(@types/node@24.12.0)(jiti@1.21.7)(yaml@2.8.3))':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.0
|
||||
@@ -3420,7 +3544,7 @@ snapshots:
|
||||
|
||||
astring@1.9.0: {}
|
||||
|
||||
astro@6.1.2(@types/node@24.12.0)(@vercel/functions@3.4.3)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3):
|
||||
astro@6.1.2(@types/node@24.12.0)(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)(jiti@1.21.7)(rollup@4.60.1)(typescript@5.7.3)(yaml@2.8.3):
|
||||
dependencies:
|
||||
'@astrojs/compiler': 3.0.1
|
||||
'@astrojs/internal-helpers': 0.8.0
|
||||
@@ -3470,7 +3594,7 @@ snapshots:
|
||||
ultrahtml: 1.6.0
|
||||
unifont: 0.7.4
|
||||
unist-util-visit: 5.1.0
|
||||
unstorage: 1.17.5(@vercel/functions@3.4.3)
|
||||
unstorage: 1.17.5(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1)
|
||||
vfile: 6.0.3
|
||||
vite: 7.3.1(@types/node@24.12.0)(jiti@1.21.7)(yaml@2.8.3)
|
||||
vitefu: 1.1.2(vite@7.3.1(@types/node@24.12.0)(jiti@1.21.7)(yaml@2.8.3))
|
||||
@@ -3593,6 +3717,8 @@ snapshots:
|
||||
|
||||
clsx@2.1.1: {}
|
||||
|
||||
cluster-key-slot@1.1.2: {}
|
||||
|
||||
collapse-white-space@2.1.0: {}
|
||||
|
||||
comma-separated-tokens@2.0.3: {}
|
||||
@@ -3655,6 +3781,8 @@ snapshots:
|
||||
|
||||
defu@6.1.4: {}
|
||||
|
||||
denque@2.1.0: {}
|
||||
|
||||
dequal@2.0.3: {}
|
||||
|
||||
destr@2.0.5: {}
|
||||
@@ -4037,6 +4165,20 @@ snapshots:
|
||||
|
||||
intersection-observer@0.10.0: {}
|
||||
|
||||
ioredis@5.10.1:
|
||||
dependencies:
|
||||
'@ioredis/commands': 1.5.1
|
||||
cluster-key-slot: 1.1.2
|
||||
debug: 4.4.3
|
||||
denque: 2.1.0
|
||||
lodash.defaults: 4.2.0
|
||||
lodash.isarguments: 3.1.0
|
||||
redis-errors: 1.2.0
|
||||
redis-parser: 3.0.0
|
||||
standard-as-callback: 2.1.0
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
iron-webcrypto@1.2.1: {}
|
||||
|
||||
is-alphabetical@2.0.1: {}
|
||||
@@ -4113,6 +4255,10 @@ snapshots:
|
||||
lit-element: 4.2.2
|
||||
lit-html: 3.3.2
|
||||
|
||||
lodash.defaults@4.2.0: {}
|
||||
|
||||
lodash.isarguments@3.1.0: {}
|
||||
|
||||
longest-streak@3.1.0: {}
|
||||
|
||||
loose-envify@1.4.0:
|
||||
@@ -4868,6 +5014,12 @@ snapshots:
|
||||
unified: 11.0.5
|
||||
vfile: 6.0.3
|
||||
|
||||
redis-errors@1.2.0: {}
|
||||
|
||||
redis-parser@3.0.0:
|
||||
dependencies:
|
||||
redis-errors: 1.2.0
|
||||
|
||||
regex-recursion@6.0.2:
|
||||
dependencies:
|
||||
regex-utilities: 2.3.0
|
||||
@@ -5132,6 +5284,8 @@ snapshots:
|
||||
|
||||
space-separated-tokens@2.0.2: {}
|
||||
|
||||
standard-as-callback@2.1.0: {}
|
||||
|
||||
stream-replace-string@2.0.0: {}
|
||||
|
||||
stringify-entities@4.0.4:
|
||||
@@ -5325,7 +5479,7 @@ snapshots:
|
||||
unist-util-is: 6.0.1
|
||||
unist-util-visit-parents: 6.0.2
|
||||
|
||||
unstorage@1.17.5(@vercel/functions@3.4.3):
|
||||
unstorage@1.17.5(@upstash/redis@1.37.0)(@vercel/functions@3.4.3)(@vercel/kv@3.0.0)(ioredis@5.10.1):
|
||||
dependencies:
|
||||
anymatch: 3.1.3
|
||||
chokidar: 5.0.0
|
||||
@@ -5336,7 +5490,10 @@ snapshots:
|
||||
ofetch: 1.5.1
|
||||
ufo: 1.6.3
|
||||
optionalDependencies:
|
||||
'@upstash/redis': 1.37.0
|
||||
'@vercel/functions': 3.4.3
|
||||
'@vercel/kv': 3.0.0
|
||||
ioredis: 5.10.1
|
||||
|
||||
update-browserslist-db@1.2.3(browserslist@4.28.1):
|
||||
dependencies:
|
||||
|
||||
11
src/components/analytics.tsx
Normal file
11
src/components/analytics.tsx
Normal file
@@ -0,0 +1,11 @@
|
||||
import { Analytics } from "@vercel/analytics/react";
|
||||
import { SpeedInsights } from "@vercel/speed-insights/react";
|
||||
|
||||
export default function VercelAnalytics() {
|
||||
return (
|
||||
<>
|
||||
<Analytics />
|
||||
<SpeedInsights />
|
||||
</>
|
||||
);
|
||||
}
|
||||
@@ -77,7 +77,7 @@ export const BlogPostList = ({ posts }: BlogPostListProps) => {
|
||||
className="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.location.href = `/blog/tag/${tag}`;
|
||||
window.location.href = `/blog/tags/${encodeURIComponent(tag)}`;
|
||||
}}
|
||||
>
|
||||
#{tag}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import React, { useMemo } from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import { AnimateIn } from "@/components/animate-in";
|
||||
|
||||
interface BlogPost {
|
||||
title: string;
|
||||
@@ -11,47 +12,49 @@ interface TagListProps {
|
||||
posts: BlogPost[];
|
||||
}
|
||||
|
||||
const TagList: React.FC<TagListProps> = ({ posts }) => {
|
||||
const spectrumColors = [
|
||||
'text-red-bright',
|
||||
'text-orange-bright',
|
||||
'text-yellow-bright',
|
||||
'text-green-bright',
|
||||
'text-aqua-bright',
|
||||
'text-blue-bright',
|
||||
'text-purple-bright'
|
||||
];
|
||||
const spectrumColors = [
|
||||
'text-red-bright',
|
||||
'text-orange-bright',
|
||||
'text-yellow-bright',
|
||||
'text-green-bright',
|
||||
'text-aqua-bright',
|
||||
'text-blue-bright',
|
||||
'text-purple-bright'
|
||||
];
|
||||
|
||||
const sizeClasses = [
|
||||
'text-3xl sm:text-4xl',
|
||||
'text-2xl sm:text-3xl',
|
||||
'text-xl sm:text-2xl',
|
||||
'text-lg sm:text-xl',
|
||||
'text-base sm:text-lg',
|
||||
];
|
||||
|
||||
const TagList = ({ posts }: TagListProps) => {
|
||||
const tagData = useMemo(() => {
|
||||
if (!Array.isArray(posts)) return [];
|
||||
|
||||
const tagMap = new Map();
|
||||
const tagMap = new Map<string, number>();
|
||||
posts.forEach(post => {
|
||||
if (post?.data?.tags && Array.isArray(post.data.tags)) {
|
||||
post.data.tags.forEach(tag => {
|
||||
if (!tagMap.has(tag)) {
|
||||
tagMap.set(tag, {
|
||||
name: tag,
|
||||
count: 1
|
||||
});
|
||||
} else {
|
||||
const data = tagMap.get(tag);
|
||||
data.count++;
|
||||
}
|
||||
});
|
||||
}
|
||||
post?.data?.tags?.forEach(tag => {
|
||||
tagMap.set(tag, (tagMap.get(tag) || 0) + 1);
|
||||
});
|
||||
});
|
||||
|
||||
const tagArray = Array.from(tagMap.values());
|
||||
const maxCount = Math.max(...tagArray.map(t => t.count));
|
||||
const tags = Array.from(tagMap.entries())
|
||||
.sort((a, b) => b[1] - a[1]);
|
||||
const maxCount = tags[0]?.[1] || 1;
|
||||
|
||||
return tagArray
|
||||
.sort((a, b) => b.count - a.count)
|
||||
.map((tag, index) => ({
|
||||
...tag,
|
||||
color: spectrumColors[index % spectrumColors.length],
|
||||
frequency: tag.count / maxCount
|
||||
}));
|
||||
return tags.map(([name, count], i) => {
|
||||
const ratio = count / maxCount;
|
||||
const sizeIndex = ratio > 0.8 ? 0 : ratio > 0.6 ? 1 : ratio > 0.4 ? 2 : ratio > 0.2 ? 3 : 4;
|
||||
return {
|
||||
name,
|
||||
count,
|
||||
color: spectrumColors[i % spectrumColors.length],
|
||||
size: sizeClasses[sizeIndex],
|
||||
};
|
||||
});
|
||||
}, [posts]);
|
||||
|
||||
if (tagData.length === 0) {
|
||||
@@ -63,50 +66,25 @@ const TagList: React.FC<TagListProps> = ({ posts }) => {
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex-1 w-full bg-background p-4">
|
||||
<div className="grid grid-cols-1 sm:grid-cols-2 md:grid-cols-3 lg:grid-cols-4 gap-4">
|
||||
{tagData.map(({ name, count, color, frequency }) => (
|
||||
<div className="flex flex-wrap items-baseline justify-center gap-x-6 gap-y-4 sm:gap-x-8 sm:gap-y-5 px-4 py-8 max-w-4xl mx-auto">
|
||||
{tagData.map(({ name, count, color, size }, i) => (
|
||||
<AnimateIn key={name} delay={i * 50}>
|
||||
<a
|
||||
key={name}
|
||||
href={`/blog/tags/${encodeURIComponent(name)}`}
|
||||
className={`
|
||||
group relative
|
||||
flex flex-col items-center justify-center
|
||||
min-h-[5rem]
|
||||
px-6 py-4 rounded-lg
|
||||
text-xl
|
||||
transition-all duration-300 ease-in-out
|
||||
hover:scale-105
|
||||
hover:bg-foreground/5
|
||||
${color}
|
||||
${color} ${size}
|
||||
font-medium
|
||||
hover:opacity-70 transition-opacity duration-200
|
||||
cursor-pointer whitespace-nowrap
|
||||
`}
|
||||
>
|
||||
{/* Main tag display */}
|
||||
<div className="font-medium text-center">
|
||||
#{name}
|
||||
</div>
|
||||
|
||||
{/* Post count */}
|
||||
<div className="mt-2 text-base opacity-60">
|
||||
{count} post{count !== 1 ? 's' : ''}
|
||||
</div>
|
||||
|
||||
{/* Background gradient */}
|
||||
<div
|
||||
className="absolute inset-0 -z-10 rounded-lg opacity-10"
|
||||
style={{
|
||||
background: `
|
||||
linear-gradient(
|
||||
45deg,
|
||||
currentColor ${frequency * 100}%,
|
||||
transparent
|
||||
)
|
||||
`
|
||||
}}
|
||||
/>
|
||||
#{name}
|
||||
<span className="text-foreground/30 text-xs ml-1 align-super">
|
||||
{count}
|
||||
</span>
|
||||
</a>
|
||||
))}
|
||||
</div>
|
||||
</AnimateIn>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
123
src/components/blog/tagged-posts.tsx
Normal file
123
src/components/blog/tagged-posts.tsx
Normal file
@@ -0,0 +1,123 @@
|
||||
import { AnimateIn } from "@/components/animate-in";
|
||||
import { RssIcon, TagIcon, TrendingUpIcon } from "lucide-react";
|
||||
|
||||
type BlogPost = {
|
||||
id: string;
|
||||
data: {
|
||||
title: string;
|
||||
author: string;
|
||||
date: string;
|
||||
tags: string[];
|
||||
description: string;
|
||||
image?: string;
|
||||
imagePosition?: string;
|
||||
};
|
||||
};
|
||||
|
||||
interface TaggedPostsProps {
|
||||
tag: string;
|
||||
posts: BlogPost[];
|
||||
}
|
||||
|
||||
const formatDate = (dateString: string) => {
|
||||
return new Date(dateString).toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
});
|
||||
};
|
||||
|
||||
const TaggedPosts = ({ tag, posts }: TaggedPostsProps) => {
|
||||
return (
|
||||
<div className="w-full max-w-6xl mx-auto">
|
||||
<div className="w-full px-4 pt-24 sm:pt-24">
|
||||
<AnimateIn>
|
||||
<h1 className="text-2xl sm:text-3xl font-bold text-purple mb-3 text-center px-4 leading-relaxed">
|
||||
#{tag}
|
||||
</h1>
|
||||
</AnimateIn>
|
||||
<AnimateIn delay={100}>
|
||||
<div className="flex flex-wrap justify-center gap-4 mb-12 text-sm sm:text-base">
|
||||
<a
|
||||
href="/rss"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-orange hover:text-orange-bright hover:border-orange/50 transition-colors duration-200"
|
||||
>
|
||||
<RssIcon className="w-4 h-4" />
|
||||
<span>RSS Feed</span>
|
||||
</a>
|
||||
<a
|
||||
href="/blog/tags"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-aqua hover:text-aqua-bright hover:border-aqua/50 transition-colors duration-200"
|
||||
>
|
||||
<TagIcon className="w-4 h-4" />
|
||||
<span>Browse Tags</span>
|
||||
</a>
|
||||
<a
|
||||
href="/blog/popular"
|
||||
className="inline-flex items-center gap-2 px-4 py-2 rounded-lg bg-background border border-foreground/20 text-blue hover:text-blue-bright hover:border-blue/50 transition-colors duration-200"
|
||||
>
|
||||
<TrendingUpIcon className="w-4 h-4" />
|
||||
<span>Most Popular</span>
|
||||
</a>
|
||||
</div>
|
||||
</AnimateIn>
|
||||
</div>
|
||||
|
||||
<ul className="space-y-6 md:space-y-10">
|
||||
{posts.map((post, i) => (
|
||||
<AnimateIn key={post.id} delay={200 + i * 80}>
|
||||
<li className="group px-4 md:px-0">
|
||||
<a href={`/blog/${post.id}`} className="block">
|
||||
<article className="flex flex-col md:flex-row md:items-center gap-4 md:gap-8 p-2 md:p-4 border-b border-foreground/20 last:border-b-0 rounded-lg group-hover:outline group-hover:outline-2 group-hover:outline-purple transition-all duration-200">
|
||||
<div className="w-full md:w-1/3 aspect-[16/9] overflow-hidden rounded-lg bg-background flex-shrink-0">
|
||||
<img
|
||||
src={post.data.image || "/blog/placeholder.png"}
|
||||
alt={post.data.title}
|
||||
className="w-full h-full object-cover group-hover:scale-105 transition-transform duration-300"
|
||||
style={{ objectPosition: post.data.imagePosition || "center center" }}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full md:w-2/3 flex flex-col gap-2 md:gap-3">
|
||||
<div className="space-y-1.5 md:space-y-3">
|
||||
<h2 className="text-lg md:text-2xl font-semibold text-yellow group-hover:text-purple transition-colors duration-200 line-clamp-2">
|
||||
{post.data.title}
|
||||
</h2>
|
||||
<div className="flex flex-wrap items-center gap-2 md:gap-3 text-sm md:text-base text-foreground/80">
|
||||
<span className="text-orange">{post.data.author}</span>
|
||||
<span className="text-foreground/50">•</span>
|
||||
<time dateTime={post.data.date} className="text-blue">
|
||||
{formatDate(post.data.date)}
|
||||
</time>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-foreground/90 text-sm md:text-lg leading-relaxed line-clamp-2 mt-0.5 md:mt-0">
|
||||
{post.data.description}
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-1.5 md:gap-3 mt-1 md:mt-2">
|
||||
{post.data.tags.map((t) => (
|
||||
<span
|
||||
key={t}
|
||||
className={`text-xs md:text-base transition-colors duration-200 ${
|
||||
t === tag ? "text-aqua-bright" : "text-aqua hover:text-aqua-bright"
|
||||
}`}
|
||||
onClick={(e) => {
|
||||
e.preventDefault();
|
||||
window.location.href = `/blog/tags/${encodeURIComponent(t)}`;
|
||||
}}
|
||||
>
|
||||
#{t}
|
||||
</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
</a>
|
||||
</li>
|
||||
</AnimateIn>
|
||||
))}
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default TaggedPosts;
|
||||
@@ -7,6 +7,7 @@ import Footer from "@/components/footer";
|
||||
import Background from "@/components/background";
|
||||
import ThemeSwitcher from "@/components/theme-switcher";
|
||||
import AnimationSwitcher from "@/components/animation-switcher";
|
||||
import VercelAnalytics from "@/components/analytics";
|
||||
import { THEME_LOADER_SCRIPT, THEME_NAV_SCRIPT } from "@/lib/themes/loader";
|
||||
import { ANIMATION_LOADER_SCRIPT, ANIMATION_NAV_SCRIPT } from "@/lib/animations/loader";
|
||||
|
||||
@@ -68,6 +69,7 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
</div>
|
||||
<ThemeSwitcher client:only="react" transition:persist />
|
||||
<AnimationSwitcher client:only="react" transition:persist />
|
||||
<VercelAnalytics client:load />
|
||||
<script is:inline set:html={`window.scrollTo(0,0);` + THEME_NAV_SCRIPT} />
|
||||
<script is:inline set:html={ANIMATION_NAV_SCRIPT} />
|
||||
</body>
|
||||
|
||||
@@ -8,6 +8,7 @@ import Footer from "@/components/footer";
|
||||
import Background from "@/components/background";
|
||||
import ThemeSwitcher from "@/components/theme-switcher";
|
||||
import AnimationSwitcher from "@/components/animation-switcher";
|
||||
import VercelAnalytics from "@/components/analytics";
|
||||
import { THEME_LOADER_SCRIPT, THEME_NAV_SCRIPT } from "@/lib/themes/loader";
|
||||
import { ANIMATION_LOADER_SCRIPT, ANIMATION_NAV_SCRIPT } from "@/lib/animations/loader";
|
||||
|
||||
@@ -48,6 +49,7 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
<Footer client:load transition:persist fixed=true />
|
||||
<ThemeSwitcher client:only="react" transition:persist />
|
||||
<AnimationSwitcher client:only="react" transition:persist />
|
||||
<VercelAnalytics client:load />
|
||||
<script is:inline set:html={THEME_NAV_SCRIPT} />
|
||||
<script is:inline set:html={ANIMATION_NAV_SCRIPT} />
|
||||
</body>
|
||||
|
||||
@@ -7,6 +7,7 @@ import Footer from "@/components/footer";
|
||||
import Background from "@/components/background";
|
||||
import ThemeSwitcher from "@/components/theme-switcher";
|
||||
import AnimationSwitcher from "@/components/animation-switcher";
|
||||
import VercelAnalytics from "@/components/analytics";
|
||||
import { THEME_LOADER_SCRIPT, THEME_NAV_SCRIPT } from "@/lib/themes/loader";
|
||||
import { ANIMATION_LOADER_SCRIPT, ANIMATION_NAV_SCRIPT } from "@/lib/animations/loader";
|
||||
|
||||
@@ -64,6 +65,7 @@ const ogImage = "https://timmypidashev.dev/og-image.jpg";
|
||||
</main>
|
||||
<ThemeSwitcher client:only="react" transition:persist />
|
||||
<AnimationSwitcher client:only="react" transition:persist />
|
||||
<VercelAnalytics client:load />
|
||||
<script is:inline set:html={`window.scrollTo(0,0);` + THEME_NAV_SCRIPT} />
|
||||
<script is:inline set:html={ANIMATION_NAV_SCRIPT} />
|
||||
</body>
|
||||
|
||||
55
src/lib/views.ts
Normal file
55
src/lib/views.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import Redis from "ioredis";
|
||||
|
||||
let redis: Redis | null = null;
|
||||
|
||||
function getRedis(): Redis | null {
|
||||
if (redis) return redis;
|
||||
|
||||
const url = import.meta.env.REDIS_URL || process.env.REDIS_URL;
|
||||
if (!url) return null;
|
||||
|
||||
redis = new Redis(url);
|
||||
return redis;
|
||||
}
|
||||
|
||||
export async function incrementViews(slug: string): Promise<number> {
|
||||
const r = getRedis();
|
||||
if (!r) return 0;
|
||||
|
||||
try {
|
||||
return await r.incr(`views:${slug}`);
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getViews(slug: string): Promise<number> {
|
||||
const r = getRedis();
|
||||
if (!r) return 0;
|
||||
|
||||
try {
|
||||
const val = await r.get(`views:${slug}`);
|
||||
return val ? parseInt(val, 10) : 0;
|
||||
} catch {
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllViews(slugs: string[]): Promise<Record<string, number>> {
|
||||
const r = getRedis();
|
||||
const result: Record<string, number> = {};
|
||||
|
||||
if (!r || slugs.length === 0) return result;
|
||||
|
||||
try {
|
||||
const keys = slugs.map(s => `views:${s}`);
|
||||
const values = await r.mget(...keys);
|
||||
for (let i = 0; i < slugs.length; i++) {
|
||||
result[slugs[i]] = values[i] ? parseInt(values[i], 10) : 0;
|
||||
}
|
||||
} catch {
|
||||
// Return empty counts if Redis unavailable
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -5,6 +5,7 @@ import ContentLayout from "@/layouts/content.astro";
|
||||
import { getArticleSchema } from "@/lib/structuredData";
|
||||
import { blogWebsite } from "@/lib/structuredData";
|
||||
import { Comments } from "@/components/blog/comments";
|
||||
import { incrementViews, getViews } from "@/lib/views";
|
||||
|
||||
// This is a dynamic route in SSR mode
|
||||
const { slug } = Astro.params;
|
||||
@@ -20,6 +21,14 @@ if (!post || (!import.meta.env.DEV && post.data.isDraft === true)) {
|
||||
});
|
||||
}
|
||||
|
||||
// Track page view and get count
|
||||
let views = 0;
|
||||
if (!import.meta.env.DEV) {
|
||||
views = await incrementViews(post.id);
|
||||
} else {
|
||||
views = await getViews(post.id);
|
||||
}
|
||||
|
||||
// Dynamically render the content
|
||||
const { Content } = await render(post);
|
||||
|
||||
@@ -84,12 +93,18 @@ const jsonLd = {
|
||||
<time dateTime={post.data.date instanceof Date ? post.data.date.toISOString() : post.data.date} class="text-blue">
|
||||
{formattedDate}
|
||||
</time>
|
||||
{views > 0 && (
|
||||
<>
|
||||
<span class="text-foreground/50">•</span>
|
||||
<span class="text-green">{views.toLocaleString()} view{views !== 1 ? "s" : ""}</span>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
<div class="flex flex-wrap gap-2 mt-2">
|
||||
{post.data.tags.map((tag) => (
|
||||
<span
|
||||
class="text-xs md:text-base text-aqua hover:text-aqua-bright transition-colors duration-200"
|
||||
onclick={`window.location.href='/blog/tag/${tag}'`}
|
||||
onclick={`window.location.href='/blog/tags/${encodeURIComponent(tag)}'`}
|
||||
>
|
||||
#{tag}
|
||||
</span>
|
||||
|
||||
32
src/pages/blog/popular/index.astro
Normal file
32
src/pages/blog/popular/index.astro
Normal file
@@ -0,0 +1,32 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import ContentLayout from "@/layouts/content.astro";
|
||||
import { BlogHeader } from "@/components/blog/header";
|
||||
import { BlogPostList } from "@/components/blog/post-list";
|
||||
import { getAllViews } from "@/lib/views";
|
||||
|
||||
const posts = (await getCollection("blog", ({ data }) => {
|
||||
return import.meta.env.DEV || data.isDraft !== true;
|
||||
})).map(post => ({
|
||||
...post,
|
||||
data: {
|
||||
...post.data,
|
||||
date: post.data.date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
// Get view counts and sort by popularity
|
||||
const views = await getAllViews(posts.map(p => p.id));
|
||||
const sorted = [...posts].sort((a, b) => (views[b.id] || 0) - (views[a.id] || 0));
|
||||
---
|
||||
<ContentLayout
|
||||
title="Most Popular | Blog | Timothy Pidashev"
|
||||
description="Most popular blog posts by view count."
|
||||
>
|
||||
<BlogHeader client:load />
|
||||
<BlogPostList posts={sorted} client:load />
|
||||
</ContentLayout>
|
||||
38
src/pages/blog/tags/[...slug].astro
Normal file
38
src/pages/blog/tags/[...slug].astro
Normal file
@@ -0,0 +1,38 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import ContentLayout from "@/layouts/content.astro";
|
||||
import TaggedPosts from "@/components/blog/tagged-posts";
|
||||
|
||||
const { slug } = Astro.params;
|
||||
const tag = decodeURIComponent(slug || "");
|
||||
|
||||
if (!tag) {
|
||||
return Astro.redirect("/blog/tags");
|
||||
}
|
||||
|
||||
const filteredPosts = (await getCollection("blog", ({ data }) => {
|
||||
return (import.meta.env.DEV || data.isDraft !== true) && data.tags.includes(tag);
|
||||
})).sort((a, b) => {
|
||||
return b.data.date.valueOf() - a.data.date.valueOf();
|
||||
}).map(post => ({
|
||||
...post,
|
||||
data: {
|
||||
...post.data,
|
||||
date: post.data.date.toLocaleDateString("en-US", {
|
||||
year: "numeric",
|
||||
month: "long",
|
||||
day: "numeric"
|
||||
})
|
||||
}
|
||||
}));
|
||||
|
||||
if (filteredPosts.length === 0) {
|
||||
return Astro.redirect("/blog/tags");
|
||||
}
|
||||
---
|
||||
<ContentLayout
|
||||
title={`#${tag} | Blog | Timothy Pidashev`}
|
||||
description={`Blog posts tagged with "${tag}".`}
|
||||
>
|
||||
<TaggedPosts tag={tag} posts={filteredPosts} client:load />
|
||||
</ContentLayout>
|
||||
@@ -1,7 +1,7 @@
|
||||
---
|
||||
import { getCollection } from "astro:content";
|
||||
import ContentLayout from "@/layouts/content.astro";
|
||||
|
||||
import { BlogHeader } from "@/components/blog/header";
|
||||
import TagList from "@/components/blog/tag-list";
|
||||
|
||||
const posts = (await getCollection("blog", ({ data }) => {
|
||||
@@ -21,8 +21,9 @@ const posts = (await getCollection("blog", ({ data }) => {
|
||||
}));
|
||||
---
|
||||
<ContentLayout
|
||||
title="Blog | Timothy Pidashev"
|
||||
description="My experiences and technical insights into software development and the ever-evolving world of programming."
|
||||
title="Browse Tags | Blog | Timothy Pidashev"
|
||||
description="Browse blog posts by tag."
|
||||
>
|
||||
<TagList posts={posts} />
|
||||
<BlogHeader client:load />
|
||||
<TagList posts={posts} client:load />
|
||||
</ContentLayout>
|
||||
|
||||
Reference in New Issue
Block a user