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"