diff --git a/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/1945ae1d9220b9b9595df3b70c7f12606f5505a287016a57770571eee21dc7690000019d584eb91f b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/1945ae1d9220b9b9595df3b70c7f12606f5505a287016a57770571eee21dc7690000019d584eb91f
new file mode 100644
index 0000000..e440e5c
--- /dev/null
+++ b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/1945ae1d9220b9b9595df3b70c7f12606f5505a287016a57770571eee21dc7690000019d584eb91f
@@ -0,0 +1 @@
+3
\ No newline at end of file
diff --git a/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/88a48a5ca411d99cd63c9bb308aa3868125abf3ae993ef635ba2b83929b9f3750000019d584eb91a b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/88a48a5ca411d99cd63c9bb308aa3868125abf3ae993ef635ba2b83929b9f3750000019d584eb91a
new file mode 100644
index 0000000..56a6051
--- /dev/null
+++ b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/88a48a5ca411d99cd63c9bb308aa3868125abf3ae993ef635ba2b83929b9f3750000019d584eb91a
@@ -0,0 +1 @@
+1
\ No newline at end of file
diff --git a/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/d30c0dd40129710b5dbf376654872cdd10c68631ec1b033fba0574bb30dd3f5e0000019d5849f694 b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/d30c0dd40129710b5dbf376654872cdd10c68631ec1b033fba0574bb30dd3f5e0000019d5849f694
new file mode 100644
index 0000000..c793025
--- /dev/null
+++ b/.wrangler/state/v3/kv/0c407860ba17425581796410dbce9e19/blobs/d30c0dd40129710b5dbf376654872cdd10c68631ec1b033fba0574bb30dd3f5e0000019d5849f694
@@ -0,0 +1 @@
+7
\ No newline at end of file
diff --git a/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/d1866dd99f5441005a55b0a86f46e43724c857ce74a35b86238b75e6e807ff7f.sqlite b/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/d1866dd99f5441005a55b0a86f46e43724c857ce74a35b86238b75e6e807ff7f.sqlite
new file mode 100644
index 0000000..4455741
Binary files /dev/null and b/.wrangler/state/v3/kv/miniflare-KVNamespaceObject/d1866dd99f5441005a55b0a86f46e43724c857ce74a35b86238b75e6e807ff7f.sqlite differ
diff --git a/astro.config.mjs b/astro.config.mjs
index 476e2e3..bb062ad 100644
--- a/astro.config.mjs
+++ b/astro.config.mjs
@@ -13,7 +13,17 @@ export default defineConfig({
output: 'static',
adapter: cloudflare({
imageService: 'cloudflare',
+ routes: {
+ pattern: '/**',
+ exclude: ['/_image/**'],
+ },
}),
+ kvNamespaces: [
+ {
+ binding: 'ARTICLE_VIEWS',
+ id: '4byte-article-views',
+ },
+ ],
integrations: [
UnoCSS(),
mdx(),
diff --git a/package.json b/package.json
index 2b886ff..2257fc1 100644
--- a/package.json
+++ b/package.json
@@ -55,7 +55,8 @@
"prettier": "^3.8.1",
"prettier-plugin-astro": "^0.14.1",
"sharp": "^0.34.5",
- "unocss": "^66.6.6"
+ "unocss": "^66.6.6",
+ "wrangler": "^4.80.0"
},
"lint-staged": {
"*.{js,ts,astro,json,md,mdx}": [
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 8c492b7..41d1c88 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -108,6 +108,9 @@ importers:
unocss:
specifier: ^66.6.6
version: 66.6.6(@unocss/astro@66.6.6(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(yaml@2.8.2)))(vite@7.3.1(@types/node@25.5.0)(jiti@2.6.1)(lightningcss@1.32.0)(terser@5.46.0)(yaml@2.8.2))
+ wrangler:
+ specifier: ^4.80.0
+ version: 4.80.0
packages:
diff --git a/src/components/ArticleViews.astro b/src/components/ArticleViews.astro
new file mode 100644
index 0000000..445de00
--- /dev/null
+++ b/src/components/ArticleViews.astro
@@ -0,0 +1,65 @@
+---
+interface Props {
+ slug: string
+ initialViews?: number
+}
+
+const { slug, initialViews = 0 } = Astro.props
+---
+
+
+
+ {initialViews.toLocaleString()}
+
+
+
diff --git a/src/env.d.ts b/src/env.d.ts
index c561c96..4c6964b 100644
--- a/src/env.d.ts
+++ b/src/env.d.ts
@@ -1,19 +1,10 @@
-interface CloudflareEnv {
+export interface CloudflareEnv {
JWT_SECRET: string
GITHUB_CLIENT_ID: string
GITHUB_CLIENT_SECRET: string
GITHUB_CALLBACK_URL: string
DEV?: string
-}
-
-declare global {
- namespace App {
- interface Locals {
- runtime: {
- env: CloudflareEnv
- }
- }
- }
+ ARTICLE_VIEWS: KVNamespace
}
export {}
diff --git a/src/lib/views.ts b/src/lib/views.ts
new file mode 100644
index 0000000..2bc7fde
--- /dev/null
+++ b/src/lib/views.ts
@@ -0,0 +1,13 @@
+import type { CloudflareEnv } from '../env'
+
+export async function getArticleViews(slug: string, env: CloudflareEnv): Promise {
+ const count = await env.ARTICLE_VIEWS.get(slug)
+ return count ? parseInt(count, 10) : 0
+}
+
+export async function incrementArticleViews(slug: string, env: CloudflareEnv): Promise {
+ const current = await getArticleViews(slug, env)
+ const newCount = current + 1
+ await env.ARTICLE_VIEWS.put(slug, newCount.toString())
+ return newCount
+}
diff --git a/src/middleware.ts b/src/middleware.ts
index 78f7d54..8381bf6 100644
--- a/src/middleware.ts
+++ b/src/middleware.ts
@@ -1,13 +1,13 @@
import { defineMiddleware } from 'astro/middleware'
import { verifyToken, getCookieName, type SessionUser } from './lib/auth'
-import { env } from 'cloudflare:workers'
+import { env as cfEnv } from 'cloudflare:workers'
export const onRequest = defineMiddleware(async (context, next) => {
const cookieName = getCookieName()
const token = context.cookies.get(cookieName)?.value
if (token) {
- const user = await verifyToken(token, env as any)
+ const user = await verifyToken(token, cfEnv as any)
if (user) {
context.locals.user = user
}
diff --git a/src/pages/api/views/[slug].ts b/src/pages/api/views/[slug].ts
new file mode 100644
index 0000000..19a8380
--- /dev/null
+++ b/src/pages/api/views/[slug].ts
@@ -0,0 +1,56 @@
+export const prerender = false
+
+import type { APIRoute } from 'astro'
+import { env } from 'cloudflare:workers'
+import { getArticleViews, incrementArticleViews } from '../../../lib/views'
+
+export const GET: APIRoute = async ({ params }) => {
+ const slug = params.slug
+ if (!slug) {
+ return new Response(JSON.stringify({ error: 'Slug required' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+
+ const views = await getArticleViews(slug, env as any)
+ return new Response(JSON.stringify({ slug, views }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ })
+}
+
+export const POST: APIRoute = async ({ params, cookies }) => {
+ const slug = params.slug
+ if (!slug) {
+ return new Response(JSON.stringify({ error: 'Slug required' }), {
+ status: 400,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+
+ const cookieName = `view_tracked_${slug}`
+ const existingCookie = cookies.get(cookieName)
+
+ if (existingCookie) {
+ const views = await getArticleViews(slug, env as any)
+ return new Response(JSON.stringify({ slug, views }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ })
+ }
+
+ const views = await incrementArticleViews(slug, env as any)
+
+ cookies.set(cookieName, '1', {
+ path: '/',
+ maxAge: 60 * 60 * 24 * 30,
+ httpOnly: false,
+ sameSite: 'lax',
+ })
+
+ return new Response(JSON.stringify({ slug, views }), {
+ status: 200,
+ headers: { 'Content-Type': 'application/json' },
+ })
+}
diff --git a/src/pages/articles/[slug].astro b/src/pages/articles/[slug].astro
index 945a10d..cf8a8cf 100644
--- a/src/pages/articles/[slug].astro
+++ b/src/pages/articles/[slug].astro
@@ -5,6 +5,7 @@ import GitHubUserCard from '../../components/GitHubUserCard.astro'
import ReadingProgress from '../../components/ReadingProgress.astro'
import TableOfContents from '../../components/TableOfContents.astro'
import ArticleNav from '../../components/ArticleNav.astro'
+import ArticleViews from '../../components/ArticleViews.astro'
import { getCollection, render } from 'astro:content'
import { SITE } from '../../config.mjs'
import { getLangFromUrl, t } from '../../i18n/index.ts'
@@ -132,9 +133,7 @@ const onThisPageLabel = t(labels, 'article.onThisPage')
·
{article.data.date}
·
- {article.data.views.toLocaleString()}
+
{
diff --git a/src/pages/articles/index.astro b/src/pages/articles/index.astro
index e4701f5..44392c6 100644
--- a/src/pages/articles/index.astro
+++ b/src/pages/articles/index.astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../layouts/DocsLayout.astro'
+import ArticleViews from '../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import { getLangFromUrl, t } from '../../i18n/index.ts'
@@ -53,10 +54,7 @@ const labels = await import(`../../data/${lang}/labels.json`).then((m) => m.defa
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/categories/[slug].astro b/src/pages/categories/[slug].astro
index fbdb542..be28a08 100644
--- a/src/pages/categories/[slug].astro
+++ b/src/pages/categories/[slug].astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../layouts/DocsLayout.astro'
+import ArticleViews from '../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import categoriesTr from '../../data/tr/categories.json'
import categoriesEn from '../../data/en/categories.json'
@@ -70,10 +71,7 @@ const catArticles = allArticles.filter(
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/en/articles/[slug].astro b/src/pages/en/articles/[slug].astro
index 565fdee..e893efb 100644
--- a/src/pages/en/articles/[slug].astro
+++ b/src/pages/en/articles/[slug].astro
@@ -5,6 +5,7 @@ import GitHubUserCard from '../../../components/GitHubUserCard.astro'
import ReadingProgress from '../../../components/ReadingProgress.astro'
import TableOfContents from '../../../components/TableOfContents.astro'
import ArticleNav from '../../../components/ArticleNav.astro'
+import ArticleViews from '../../../components/ArticleViews.astro'
import { getCollection, render } from 'astro:content'
import { SITE } from '../../../config.mjs'
import { t } from '../../../i18n/index.ts'
@@ -131,9 +132,7 @@ const onThisPageLabel = t(labels, 'article.onThisPage')
·
{article.data.date}
·
- {article.data.views.toLocaleString()}
+
{
diff --git a/src/pages/en/articles/index.astro b/src/pages/en/articles/index.astro
index aaf7ab4..afe0acc 100644
--- a/src/pages/en/articles/index.astro
+++ b/src/pages/en/articles/index.astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../../layouts/DocsLayout.astro'
+import ArticleViews from '../../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import { t } from '../../../i18n/index.ts'
@@ -53,10 +54,7 @@ const labels = await import(`../../../data/${lang}/labels.json`).then((m) => m.d
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/en/categories/[slug].astro b/src/pages/en/categories/[slug].astro
index 198431c..e6a46f5 100644
--- a/src/pages/en/categories/[slug].astro
+++ b/src/pages/en/categories/[slug].astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../../layouts/DocsLayout.astro'
+import ArticleViews from '../../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import categoriesEn from '../../../data/en/categories.json'
import { t } from '../../../i18n/index.ts'
@@ -61,10 +62,7 @@ const catArticles = allArticles.filter(
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/en/index.astro b/src/pages/en/index.astro
index 730bc8a..e7460d7 100644
--- a/src/pages/en/index.astro
+++ b/src/pages/en/index.astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../layouts/DocsLayout.astro'
+import ArticleViews from '../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import { t } from '../../i18n/index.ts'
@@ -63,10 +64,7 @@ const description = t(labels, 'home.description')
{article.data.excerpt}
-
-
- {article.data.views.toLocaleString()}
-
+
))
@@ -107,10 +105,7 @@ const description = t(labels, 'home.description')
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/en/tags/[slug].astro b/src/pages/en/tags/[slug].astro
index 04ce875..eb34620 100644
--- a/src/pages/en/tags/[slug].astro
+++ b/src/pages/en/tags/[slug].astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../../layouts/DocsLayout.astro'
+import ArticleViews from '../../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import tagsEn from '../../../data/en/tags.json'
import { t } from '../../../i18n/index.ts'
@@ -67,10 +68,7 @@ const tagArticles = allArticles.filter(
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/index.astro b/src/pages/index.astro
index 686648c..1f4d8d6 100644
--- a/src/pages/index.astro
+++ b/src/pages/index.astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../layouts/DocsLayout.astro'
+import ArticleViews from '../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import { getLangFromUrl, t } from '../i18n/index.ts'
@@ -63,10 +64,7 @@ const description = t(labels, 'home.description')
{article.data.excerpt}
-
-
- {article.data.views.toLocaleString()}
-
+
))
@@ -107,10 +105,7 @@ const description = t(labels, 'home.description')
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/src/pages/tags/[slug].astro b/src/pages/tags/[slug].astro
index 8f7db84..564b7eb 100644
--- a/src/pages/tags/[slug].astro
+++ b/src/pages/tags/[slug].astro
@@ -1,5 +1,6 @@
---
import DocsLayout from '../../layouts/DocsLayout.astro'
+import ArticleViews from '../../components/ArticleViews.astro'
import { getCollection } from 'astro:content'
import tagsTr from '../../data/tr/tags.json'
import tagsEn from '../../data/en/tags.json'
@@ -77,10 +78,7 @@ const tagArticles = allArticles.filter(
-
-
- {article.data.views.toLocaleString()}
-
+
diff --git a/wrangler.toml b/wrangler.toml
new file mode 100644
index 0000000..69087c1
--- /dev/null
+++ b/wrangler.toml
@@ -0,0 +1,6 @@
+name = "4byte-astro"
+compatibility_date = "2024-04-04"
+
+[[kv_namespaces]]
+binding = "ARTICLE_VIEWS"
+id = "0c407860ba17425581796410dbce9e19"