Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
3
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
1
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
7
Binary file not shown.
10 changes: 10 additions & 0 deletions astro.config.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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}": [
Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

65 changes: 65 additions & 0 deletions src/components/ArticleViews.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
---
interface Props {
slug: string
initialViews?: number
}

const { slug, initialViews = 0 } = Astro.props
---

<span
class="article-views flex items-center gap-1 text-sm text-muted-foreground dark:text-muted-dark-foreground"
data-slug={slug}
data-initial={initialViews}
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="16"
height="16"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
class="lucide lucide-eye"
>
<path d="M2 12s3-7 10-7 10 7 10 7-3 7-10 7-10-7-10-7Z"></path>
<circle cx="12" cy="12" r="3"></circle>
</svg>
<span class="view-count">{initialViews.toLocaleString()}</span>
</span>

<script>
import { onHydration } from '../utils/hydration'

async function updateViews(element: HTMLElement) {
const slug = element.dataset.slug
const initial = parseInt(element.dataset.initial || '0', 10)

try {
const response = await fetch(`/api/views/${slug}`)
if (!response.ok) return
const data = await response.json()
const countSpan = element.querySelector('.view-count') as HTMLElement
if (countSpan && data.views !== undefined) {
countSpan.textContent = data.views.toLocaleString()
}
} catch (e) {
console.error('Failed to fetch views:', e)
}

const cookieName = `view_tracked_${slug}`
if (!document.cookie.includes(cookieName)) {
try {
await fetch(`/api/views/${slug}`, { method: 'POST' })
} catch (e) {
console.error('Failed to track view:', e)
}
}
}

onHydration(() => {
document.querySelectorAll('.article-views[data-slug]').forEach(updateViews)
})
</script>
13 changes: 2 additions & 11 deletions src/env.d.ts
Original file line number Diff line number Diff line change
@@ -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 {}
13 changes: 13 additions & 0 deletions src/lib/views.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { CloudflareEnv } from '../env'

export async function getArticleViews(slug: string, env: CloudflareEnv): Promise<number> {
const count = await env.ARTICLE_VIEWS.get(slug)
return count ? parseInt(count, 10) : 0
}

export async function incrementArticleViews(slug: string, env: CloudflareEnv): Promise<number> {
const current = await getArticleViews(slug, env)
const newCount = current + 1
await env.ARTICLE_VIEWS.put(slug, newCount.toString())
return newCount
}
4 changes: 2 additions & 2 deletions src/middleware.ts
Original file line number Diff line number Diff line change
@@ -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
}
Expand Down
56 changes: 56 additions & 0 deletions src/pages/api/views/[slug].ts
Original file line number Diff line number Diff line change
@@ -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' },
})
}
5 changes: 2 additions & 3 deletions src/pages/articles/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -132,9 +133,7 @@ const onThisPageLabel = t(labels, 'article.onThisPage')
<span class="text-muted-foreground/50 dark:text-muted-dark-foreground/50">·</span>
<span>{article.data.date}</span>
<span class="text-muted-foreground/50 dark:text-muted-dark-foreground/50">·</span>
<span class="flex items-center gap-1"
><span class="i-lucide-eye w-3.5 h-3.5"></span>{article.data.views.toLocaleString()}</span
>
<ArticleViews slug={article.data.slug} initialViews={article.data.views} />
</div>
<div class="flex gap-2 flex-wrap">
{
Expand Down
6 changes: 2 additions & 4 deletions src/pages/articles/index.astro
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -53,10 +54,7 @@ const labels = await import(`../../data/${lang}/labels.json`).then((m) => m.defa
</p>
</div>
<div class="flex items-center gap-4 ml-4 text-xs text-muted-foreground dark:text-muted-dark-foreground shrink-0">
<span class="flex items-center gap-1 tabular-nums">
<span class="i-lucide-eye w-3 h-3" />
{article.data.views.toLocaleString()}
</span>
<ArticleViews slug={article.data.slug} initialViews={article.data.views} />
<span class="i-lucide-arrow-right w-3.5 h-3.5" />
</div>
</a>
Expand Down
6 changes: 2 additions & 4 deletions src/pages/categories/[slug].astro
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -70,10 +71,7 @@ const catArticles = allArticles.filter(
</p>
</div>
<div class="flex items-center gap-4 ml-4 text-xs text-muted-foreground dark:text-muted-dark-foreground shrink-0">
<span class="flex items-center gap-1 tabular-nums">
<span class="i-lucide-eye w-3 h-3" />
{article.data.views.toLocaleString()}
</span>
<ArticleViews slug={article.data.slug} initialViews={article.data.views} />
<span class="i-lucide-arrow-right w-3.5 h-3.5" />
</div>
</a>
Expand Down
5 changes: 2 additions & 3 deletions src/pages/en/articles/[slug].astro
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down Expand Up @@ -131,9 +132,7 @@ const onThisPageLabel = t(labels, 'article.onThisPage')
<span class="text-muted-foreground/50 dark:text-muted-dark-foreground/50">·</span>
<span>{article.data.date}</span>
<span class="text-muted-foreground/50 dark:text-muted-dark-foreground/50">·</span>
<span class="flex items-center gap-1"
><span class="i-lucide-eye w-3.5 h-3.5"></span>{article.data.views.toLocaleString()}</span
>
<ArticleViews slug={article.data.slug} initialViews={article.data.views} />
</div>
<div class="flex gap-2 flex-wrap">
{
Expand Down
6 changes: 2 additions & 4 deletions src/pages/en/articles/index.astro
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -53,10 +54,7 @@ const labels = await import(`../../../data/${lang}/labels.json`).then((m) => m.d
</p>
</div>
<div class="flex items-center gap-4 ml-4 text-xs text-muted-foreground dark:text-muted-dark-foreground shrink-0">
<span class="flex items-center gap-1 tabular-nums">
<span class="i-lucide-eye w-3 h-3" />
{article.data.views.toLocaleString()}
</span>
<ArticleViews slug={article.data.slug} initialViews={article.data.views} />
<span class="i-lucide-arrow-right w-3.5 h-3.5" />
</div>
</a>
Expand Down
6 changes: 2 additions & 4 deletions src/pages/en/categories/[slug].astro
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -61,10 +62,7 @@ const catArticles = allArticles.filter(
</p>
</div>
<div class="flex items-center gap-4 ml-4 text-xs text-muted-foreground dark:text-muted-dark-foreground shrink-0">
<span class="flex items-center gap-1 tabular-nums">
<span class="i-lucide-eye w-3 h-3" />
{article.data.views.toLocaleString()}
</span>
<ArticleViews slug={article.data.slug} initialViews={article.data.views} />
<span class="i-lucide-arrow-right w-3.5 h-3.5" />
</div>
</a>
Expand Down
11 changes: 3 additions & 8 deletions src/pages/en/index.astro
Original file line number Diff line number Diff line change
@@ -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'

Expand Down Expand Up @@ -63,10 +64,7 @@ const description = t(labels, 'home.description')
{article.data.excerpt}
</p>
<div class="flex items-center gap-4 text-xs text-muted-foreground dark:text-muted-dark-foreground">
<span class="flex items-center gap-1">
<span class="i-lucide-eye w-3 h-3" />
{article.data.views.toLocaleString()}
</span>
<ArticleViews slug={article.data.slug} initialViews={article.data.views} />
</div>
</a>
))
Expand Down Expand Up @@ -107,10 +105,7 @@ const description = t(labels, 'home.description')
</h3>
</div>
<div class="flex items-center gap-4 ml-4 text-xs text-muted-foreground dark:text-muted-dark-foreground shrink-0">
<span class="flex items-center gap-1 tabular-nums">
<span class="i-lucide-eye w-3 h-3" />
{article.data.views.toLocaleString()}
</span>
<ArticleViews slug={article.data.slug} initialViews={article.data.views} />
<span class="i-lucide-arrow-right w-3.5 h-3.5" />
</div>
</a>
Expand Down
6 changes: 2 additions & 4 deletions src/pages/en/tags/[slug].astro
Original file line number Diff line number Diff line change
@@ -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'
Expand Down Expand Up @@ -67,10 +68,7 @@ const tagArticles = allArticles.filter(
</p>
</div>
<div class="flex items-center gap-4 ml-4 text-xs text-muted-foreground dark:text-muted-dark-foreground shrink-0">
<span class="flex items-center gap-1 tabular-nums">
<span class="i-lucide-eye w-3 h-3" />
{article.data.views.toLocaleString()}
</span>
<ArticleViews slug={article.data.slug} initialViews={article.data.views} />
<span class="i-lucide-arrow-right w-3.5 h-3.5" />
</div>
</a>
Expand Down
Loading
Loading