mirror of
https://github.com/lbr77/blog-astro.git
synced 2026-04-08 16:11:56 +00:00
feat: refactor Notion API integration and update Docker configuration
This commit is contained in:
@@ -1,3 +1,5 @@
|
||||
node_modules/
|
||||
dist/
|
||||
.astro/
|
||||
.env
|
||||
.git
|
||||
.gitignore
|
||||
dist
|
||||
node_modules
|
||||
|
||||
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
||||
NOTION_API_BASE_URL=http://127.0.0.1:3000
|
||||
PUBLIC_NOTION_API_BASE_URL=http://127.0.0.1:3000
|
||||
27
Dockerfile
27
Dockerfile
@@ -1,14 +1,27 @@
|
||||
FROM oven/bun:latest AS build
|
||||
FROM oven/bun:1 AS build
|
||||
|
||||
ADD * /app/
|
||||
WORKDIR /app
|
||||
RUN bun install && \
|
||||
bun run astro build
|
||||
COPY package.json bun.lock ./
|
||||
COPY tsconfig.json components.json astro.config.ts ./
|
||||
COPY patches ./patches
|
||||
COPY public ./public
|
||||
COPY src ./src
|
||||
|
||||
ENV ASTRO_ADAPTER=node
|
||||
RUN bun install --frozen-lockfile
|
||||
RUN bun run build
|
||||
|
||||
FROM node:22-bookworm-slim
|
||||
|
||||
FROM node:22-alpine
|
||||
WORKDIR /app
|
||||
COPY --from=build /app/dist ./dist
|
||||
COPY --from=build /app/package.json ./package.json
|
||||
COPY --from=build /app/node_modules ./node_modules
|
||||
COPY --from=build /app/dist ./dist
|
||||
|
||||
ENV NODE_ENV=production
|
||||
ENV HOST=0.0.0.0
|
||||
ENV PORT=3000
|
||||
|
||||
EXPOSE 3000
|
||||
CMD ["node", "dist/server/entry.mjs"]
|
||||
|
||||
CMD ["node", "dist/server/entry.mjs"]
|
||||
|
||||
@@ -17,8 +17,11 @@ import { pluginCollapsibleSections } from '@expressive-code/plugin-collapsible-s
|
||||
import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers'
|
||||
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import vercel from "@astrojs/vercel"
|
||||
import node from '@astrojs/node';
|
||||
import vercel from '@astrojs/vercel'
|
||||
import node from '@astrojs/node'
|
||||
|
||||
const adapterName = process.env.ASTRO_ADAPTER ?? 'vercel'
|
||||
const adapter = adapterName === 'node' ? node({ mode: 'standalone' }) : vercel()
|
||||
|
||||
export default defineConfig({
|
||||
site: 'https://nvme0n1p.dev',
|
||||
@@ -44,8 +47,7 @@ export default defineConfig({
|
||||
codeFontSize: '0.75rem',
|
||||
borderColor: 'var(--border)',
|
||||
codeFontFamily: 'var(--font-mono)',
|
||||
codeBackground:
|
||||
'color-mix(in oklab, var(--muted) 25%, transparent)',
|
||||
codeBackground: 'color-mix(in oklab, var(--muted) 25%, transparent)',
|
||||
frames: {
|
||||
editorActiveTabForeground: 'var(--muted-foreground)',
|
||||
editorActiveTabBackground:
|
||||
@@ -112,5 +114,5 @@ export default defineConfig({
|
||||
remarkPlugins: [remarkMath, remarkEmoji],
|
||||
},
|
||||
|
||||
adapter: vercel(),
|
||||
})
|
||||
adapter,
|
||||
})
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
---
|
||||
import { SITE } from '@/consts'
|
||||
import { isSubpost } from '@/lib/data-utils'
|
||||
import { buildNotionApiUrl } from '@/lib/notion/api'
|
||||
import type { CollectionEntry } from 'astro:content'
|
||||
|
||||
interface Props {
|
||||
@@ -17,7 +18,7 @@ const author =
|
||||
? post.data.authors.join(', ')
|
||||
: SITE.author
|
||||
const ogImage = {
|
||||
url: new URL(`/api/og/${encodeURIComponent(post.id)}`, siteUrl).toString(),
|
||||
url: buildNotionApiUrl(`/og/${encodeURIComponent(post.id)}.png`),
|
||||
width: 1200,
|
||||
height: 630,
|
||||
type: 'image/png',
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import { getCollection, render, type CollectionEntry } from 'astro:content'
|
||||
import { readingTime, calculateWordCountFromHtml } from '@/lib/utils'
|
||||
import type { Block, RichText } from './interfaces'
|
||||
import { NotionApiError, fetchNotionApiJson } from './notion/api'
|
||||
import {
|
||||
buildHeadingId,
|
||||
buildText,
|
||||
@@ -56,6 +57,11 @@ export type RemotePostPayload = {
|
||||
rootId: string
|
||||
}
|
||||
|
||||
type PostsPayload = {
|
||||
posts?: NotionPost[]
|
||||
length?: number
|
||||
}
|
||||
|
||||
export interface LinkEntry {
|
||||
id: string
|
||||
picLink?: string
|
||||
@@ -73,43 +79,57 @@ export async function getAllAuthors(): Promise<CollectionEntry<'authors'>[]> {
|
||||
|
||||
export function normalizePost(post: NotionPost): CollectionEntry<'blog'> {
|
||||
const dateString =
|
||||
post['Published Date'] ?? post.created_time ?? new Date().toISOString()
|
||||
const date = new Date(dateString)
|
||||
const id = post.slug || post.id
|
||||
const banner = (post as any).banner || null
|
||||
const image = banner
|
||||
return {
|
||||
id,
|
||||
collection: 'blog',
|
||||
data: {
|
||||
title: post.Content || id || 'Untitled',
|
||||
description: (post as any).excerpt || '',
|
||||
date,
|
||||
tags: Array.isArray(post.Tags) ? post.Tags : [],
|
||||
draft: !(post.Published ?? true),
|
||||
authors: [DEFAULT_AUTHOR_ID],
|
||||
banner,
|
||||
image,
|
||||
},
|
||||
body: '',
|
||||
}
|
||||
post['Published Date'] ?? post.created_time ?? new Date().toISOString()
|
||||
const date = new Date(dateString)
|
||||
const id = post.slug || post.id
|
||||
const banner = (post as any).banner || null
|
||||
const image = banner
|
||||
return {
|
||||
id,
|
||||
collection: 'blog',
|
||||
data: {
|
||||
title: post.Content || id || 'Untitled',
|
||||
description: (post as any).excerpt || '',
|
||||
date,
|
||||
tags: Array.isArray(post.Tags) ? post.Tags : [],
|
||||
draft: !(post.Published ?? true),
|
||||
authors: [DEFAULT_AUTHOR_ID],
|
||||
banner,
|
||||
image,
|
||||
},
|
||||
body: '',
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllPosts(page?: Number, size?: Number): Promise<CollectionEntry<'blog'>[]> {
|
||||
export async function fetchPostsPage(
|
||||
page = 1,
|
||||
size = 1000,
|
||||
): Promise<{
|
||||
posts: CollectionEntry<'blog'>[]
|
||||
totalPages: number
|
||||
}> {
|
||||
const payload = await fetchNotionApiJson<PostsPayload>(
|
||||
`/v2/posts?page=${page}&length=${size}`,
|
||||
)
|
||||
const posts = (payload.posts ?? []).filter((post) => post.Published ?? true)
|
||||
const normalized = posts
|
||||
.map(normalizePost)
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
.filter((post) => !isSubpost(post.id)) as CollectionEntry<'blog'>[]
|
||||
|
||||
return {
|
||||
posts: normalized,
|
||||
totalPages: payload.length ?? 0,
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllPosts(
|
||||
page?: number,
|
||||
size?: number,
|
||||
): Promise<CollectionEntry<'blog'>[]> {
|
||||
try {
|
||||
const res = await fetch(`https://notion-api.nvme0n1p.dev/v2/posts?page=${page ?? 1}&length=${size ?? 1000}`)
|
||||
if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`)
|
||||
const payload = (await res.json()) as { posts?: NotionPost[], length?: number }
|
||||
const posts = (payload.posts ?? []).filter(
|
||||
(post) => post.Published ?? true,
|
||||
)
|
||||
const length = payload.length;
|
||||
const normalized = posts.map(normalizePost)
|
||||
|
||||
return normalized
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
.filter((post) => !isSubpost(post.id)) as unknown as CollectionEntry<'blog'>[]
|
||||
const { posts } = await fetchPostsPage(page ?? 1, size ?? 1000)
|
||||
return posts
|
||||
} catch (error) {
|
||||
console.error('getAllPosts remote fetch failed, using fallback:', error)
|
||||
return []
|
||||
@@ -122,7 +142,6 @@ export async function getAllPostsAndSubposts(): Promise<
|
||||
return getAllPosts()
|
||||
}
|
||||
|
||||
|
||||
export async function getAllTags(): Promise<Map<string, number>> {
|
||||
const posts = await getAllPosts()
|
||||
return posts.reduce((acc, post) => {
|
||||
@@ -238,15 +257,19 @@ export function isSubpost(postId: string): boolean {
|
||||
|
||||
export async function fetchRemotePost(
|
||||
slug: string,
|
||||
): Promise<NotionPost | null> {
|
||||
): Promise<{ post: NotionPost } | null> {
|
||||
try {
|
||||
const res = await fetch(`https://notion-api.nvme0n1p.dev/v2/posts/${slug}`)
|
||||
if (!res.ok) throw new Error(`Failed to fetch post: ${res.status}`)
|
||||
const data = (await res.json()) as NotionPost
|
||||
const data = await fetchNotionApiJson<Partial<{ post: NotionPost }>>(
|
||||
`/v2/posts/${encodeURI(slug)}?info=true`,
|
||||
)
|
||||
if (!data.post) return null
|
||||
return data
|
||||
} catch (error) {
|
||||
if (error instanceof NotionApiError && error.status === 404) {
|
||||
return null
|
||||
}
|
||||
console.error(`fetchRemotePost error for slug "${slug}":`, error)
|
||||
return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
@@ -254,12 +277,12 @@ export async function fetchRemotePostContent(
|
||||
slug: string,
|
||||
): Promise<RemotePostPayload | null> {
|
||||
try {
|
||||
const res = await fetch(`https://notion-api.nvme0n1p.dev/v2/posts/${encodeURI(slug)}`)
|
||||
if (!res.ok) throw new Error(`Failed to fetch post content: ${res.status}`)
|
||||
const data = (await res.json()) as Partial<{
|
||||
post: NotionPost
|
||||
blockMap: NotionBlockMap
|
||||
}>
|
||||
const data = await fetchNotionApiJson<
|
||||
Partial<{
|
||||
post: NotionPost
|
||||
blockMap: NotionBlockMap
|
||||
}>
|
||||
>(`/v2/posts/${encodeURI(slug)}`)
|
||||
if (!data.post || !data.blockMap) return null
|
||||
|
||||
const rootId = data.post.id
|
||||
@@ -271,18 +294,17 @@ export async function fetchRemotePostContent(
|
||||
rootId,
|
||||
}
|
||||
} catch (error) {
|
||||
if (error instanceof NotionApiError && error.status === 404) {
|
||||
return null
|
||||
}
|
||||
console.error(`fetchRemotePostContent error for slug "${slug}":`, error)
|
||||
return null
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFriendLinks(): Promise<LinkEntry[]> {
|
||||
|
||||
try {
|
||||
const res = await fetch("https://notion-api.nvme0n1p.dev/v2/links")
|
||||
if (!res.ok) throw new Error(`Failed to fetch links: ${res.status}`)
|
||||
|
||||
const data = (await res.json()) as LinkEntry[]
|
||||
const data = await fetchNotionApiJson<LinkEntry[]>('/v2/links')
|
||||
if (!Array.isArray(data)) throw new Error('Invalid links data format')
|
||||
|
||||
return data
|
||||
@@ -295,7 +317,6 @@ export async function getFriendLinks(): Promise<LinkEntry[]> {
|
||||
export async function getParentPost(
|
||||
subpostId: string,
|
||||
): Promise<CollectionEntry<'blog'> | null> {
|
||||
|
||||
const parentId = getParentId(subpostId)
|
||||
const allPosts = await getAllPosts()
|
||||
return allPosts.find((post) => post.id === parentId) || null
|
||||
@@ -635,7 +656,8 @@ function buildBlocks(
|
||||
|
||||
blocks.push({
|
||||
Id: `${targetType}-${listItems[0]?.Id ?? i}`,
|
||||
Type: targetType === 'bulleted_list' ? 'bulleted_list' : 'numbered_list',
|
||||
Type:
|
||||
targetType === 'bulleted_list' ? 'bulleted_list' : 'numbered_list',
|
||||
HasChildren: listItems.some((item) => item.HasChildren),
|
||||
ListItems: listItems,
|
||||
})
|
||||
@@ -975,11 +997,7 @@ export function renderRemoteBlockMap(
|
||||
slug: buildHeadingId(heading),
|
||||
text: richTextToPlainText(heading?.RichTexts ?? []),
|
||||
depth:
|
||||
block.Type === 'heading_1'
|
||||
? 2
|
||||
: block.Type === 'heading_2'
|
||||
? 3
|
||||
: 4,
|
||||
block.Type === 'heading_1' ? 2 : block.Type === 'heading_2' ? 3 : 4,
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
58
src/lib/notion/api.ts
Normal file
58
src/lib/notion/api.ts
Normal file
@@ -0,0 +1,58 @@
|
||||
const DEFAULT_NOTION_API_BASE_URL = 'https://notion-api.nvme0n1p.dev'
|
||||
|
||||
function readRuntimeEnv(key: string): string | undefined {
|
||||
if (typeof process === 'undefined') return undefined
|
||||
const value = process.env[key]
|
||||
return value && value.trim() ? value.trim() : undefined
|
||||
}
|
||||
|
||||
export class NotionApiError extends Error {
|
||||
status: number
|
||||
url: string
|
||||
|
||||
constructor(message: string, status: number, url: string) {
|
||||
super(message)
|
||||
this.name = 'NotionApiError'
|
||||
this.status = status
|
||||
this.url = url
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeBaseUrl(value: string): string {
|
||||
return value.replace(/\/+$/, '')
|
||||
}
|
||||
|
||||
export function getNotionApiServerBaseUrl(): string {
|
||||
return normalizeBaseUrl(
|
||||
readRuntimeEnv('NOTION_API_BASE_URL') ||
|
||||
readRuntimeEnv('PUBLIC_NOTION_API_BASE_URL') ||
|
||||
import.meta.env.NOTION_API_BASE_URL ||
|
||||
import.meta.env.PUBLIC_NOTION_API_BASE_URL ||
|
||||
DEFAULT_NOTION_API_BASE_URL,
|
||||
)
|
||||
}
|
||||
|
||||
export function getNotionApiPublicBaseUrl(): string {
|
||||
return normalizeBaseUrl(
|
||||
readRuntimeEnv('PUBLIC_NOTION_API_BASE_URL') ||
|
||||
readRuntimeEnv('NOTION_API_BASE_URL') ||
|
||||
import.meta.env.PUBLIC_NOTION_API_BASE_URL ||
|
||||
import.meta.env.NOTION_API_BASE_URL ||
|
||||
DEFAULT_NOTION_API_BASE_URL,
|
||||
)
|
||||
}
|
||||
|
||||
export function buildNotionApiUrl(path: string): string {
|
||||
return new URL(path, `${getNotionApiPublicBaseUrl()}/`).toString()
|
||||
}
|
||||
|
||||
export async function fetchNotionApiJson<T>(path: string): Promise<T> {
|
||||
const url = new URL(path, `${getNotionApiServerBaseUrl()}/`).toString()
|
||||
const res = await fetch(url)
|
||||
|
||||
if (!res.ok) {
|
||||
throw new NotionApiError(`Failed to fetch ${url}: ${res.status}`, res.status, url)
|
||||
}
|
||||
|
||||
return (await res.json()) as T
|
||||
}
|
||||
@@ -1,14 +1,11 @@
|
||||
|
||||
import type { Post } from '../interfaces'
|
||||
import { fetchNotionApiJson } from './api'
|
||||
|
||||
export async function getPostByPageId(pageId: string): Promise<Post | null> {
|
||||
if (!pageId) return null
|
||||
|
||||
try {
|
||||
const res = await fetch("https://notion-api.nvme0n1p.dev/v2/posts")
|
||||
if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`)
|
||||
|
||||
const payload = (await res.json()) as { posts?: any[] }
|
||||
const payload = await fetchNotionApiJson<{ posts?: any[] }>('/v2/posts')
|
||||
const match = (payload.posts ?? []).find((post) => post.id === pageId)
|
||||
if (!match) return null
|
||||
|
||||
|
||||
@@ -1,256 +0,0 @@
|
||||
import type { APIRoute } from 'astro'
|
||||
import type { CSSProperties } from 'react'
|
||||
import { createElement } from 'react'
|
||||
import { ImageResponse } from '@vercel/og'
|
||||
import { SITE } from '@/consts'
|
||||
import { fetchRemotePost, normalizePost } from '@/lib/data-utils'
|
||||
import { formatDate } from '@/lib/utils'
|
||||
|
||||
const WIDTH = 1200
|
||||
const HEIGHT = 630
|
||||
|
||||
const FONT_STACK =
|
||||
'Geist, "Inter", "Noto Sans SC", "PingFang SC", "Microsoft YaHei", sans-serif'
|
||||
|
||||
export const config = { runtime: 'edge' }
|
||||
|
||||
function truncateChars(input: string, maxChars: number) {
|
||||
const chars = Array.from(input)
|
||||
if (chars.length <= maxChars) return input
|
||||
if (maxChars <= 3) return chars.slice(0, maxChars).join('')
|
||||
return chars.slice(0, maxChars - 3).join('') + '...'
|
||||
}
|
||||
|
||||
function wrapText(input: string, maxChars: number, maxLines: number) {
|
||||
const text = input.replace(/\s+/g, ' ').trim()
|
||||
if (!text) return []
|
||||
const chars = Array.from(text)
|
||||
const lines: string[] = []
|
||||
let current: string[] = []
|
||||
|
||||
for (const ch of chars) {
|
||||
if (current.length >= maxChars) {
|
||||
lines.push(current.join(''))
|
||||
current = []
|
||||
if (lines.length >= maxLines) break
|
||||
}
|
||||
current.push(ch)
|
||||
}
|
||||
|
||||
if (lines.length < maxLines && current.length > 0) {
|
||||
lines.push(current.join(''))
|
||||
}
|
||||
|
||||
const maxTotal = maxChars * maxLines
|
||||
if (chars.length > maxTotal && lines.length > 0) {
|
||||
lines[lines.length - 1] = truncateChars(
|
||||
lines[lines.length - 1],
|
||||
maxChars,
|
||||
)
|
||||
}
|
||||
|
||||
return lines
|
||||
}
|
||||
|
||||
const containerStyle: CSSProperties = {
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
position: 'relative',
|
||||
padding: '48px',
|
||||
color: '#0f172a',
|
||||
fontFamily: FONT_STACK,
|
||||
backgroundImage: 'linear-gradient(130deg, #fdf2f8 0%, #e0f2fe 60%, #f0fdf4 100%)',
|
||||
overflow: 'hidden',
|
||||
}
|
||||
|
||||
const cardStyle: CSSProperties = {
|
||||
position: 'relative',
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '32px',
|
||||
width: '100%',
|
||||
maxWidth: '1040px',
|
||||
padding: '64px',
|
||||
borderRadius: '32px',
|
||||
border: '1px solid rgba(226, 232, 240, 0.7)',
|
||||
background:
|
||||
'linear-gradient(120deg, rgba(255, 255, 255, 0.92), rgba(240, 253, 244, 0.95))',
|
||||
boxShadow: '0 40px 120px rgba(148, 163, 184, 0.35)',
|
||||
}
|
||||
|
||||
const gridStyle: CSSProperties = {
|
||||
position: 'absolute',
|
||||
inset: 0,
|
||||
backgroundImage:
|
||||
'linear-gradient(rgba(14, 116, 144, 0.08) 1px, transparent 1px), linear-gradient(90deg, rgba(14, 116, 144, 0.08) 1px, transparent 1px)',
|
||||
backgroundSize: '40px 40px',
|
||||
borderRadius: '32px',
|
||||
maskImage: 'radial-gradient(circle at center, rgba(255, 255, 255, 0.9), transparent 70%)',
|
||||
pointerEvents: 'none',
|
||||
}
|
||||
|
||||
const badgeStyle: CSSProperties = {
|
||||
fontSize: 20,
|
||||
fontWeight: 600,
|
||||
letterSpacing: '0.25em',
|
||||
textTransform: 'uppercase',
|
||||
color: 'rgba(15, 23, 42, 0.75)',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '10px',
|
||||
}
|
||||
|
||||
const badgeDotStyle: CSSProperties = {
|
||||
width: 10,
|
||||
height: 10,
|
||||
borderRadius: '999px',
|
||||
background:
|
||||
'radial-gradient(circle, rgba(236, 72, 153, 0.9), rgba(236, 72, 153, 0.3))',
|
||||
boxShadow: '0 0 20px rgba(236, 72, 153, 0.4)',
|
||||
}
|
||||
|
||||
const titleStyle: CSSProperties = {
|
||||
fontSize: 64,
|
||||
fontWeight: 700,
|
||||
letterSpacing: '-0.02em',
|
||||
lineHeight: 1.15,
|
||||
color: '#0f172a',
|
||||
whiteSpace: 'pre-line',
|
||||
}
|
||||
|
||||
const metaRowStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
justifyContent: 'space-between',
|
||||
alignItems: 'center',
|
||||
borderTop: '1px solid rgba(148, 163, 184, 0.3)',
|
||||
paddingTop: '28px',
|
||||
marginTop: '8px',
|
||||
}
|
||||
|
||||
const metaLeftStyle: CSSProperties = {
|
||||
display: 'flex',
|
||||
flexDirection: 'column',
|
||||
gap: '6px',
|
||||
}
|
||||
|
||||
const metaLabelStyle: CSSProperties = {
|
||||
fontSize: 18,
|
||||
textTransform: 'uppercase',
|
||||
letterSpacing: '0.2em',
|
||||
color: 'rgba(15, 118, 110, 0.7)',
|
||||
}
|
||||
|
||||
const metaValueStyle: CSSProperties = {
|
||||
fontSize: 24,
|
||||
fontWeight: 600,
|
||||
color: '#0f172a',
|
||||
}
|
||||
|
||||
const signatureStyle: CSSProperties = {
|
||||
fontSize: 22,
|
||||
fontWeight: 500,
|
||||
color: 'rgba(14, 116, 144, 0.85)',
|
||||
}
|
||||
|
||||
const wrapAsBlock = (value: string, maxChars: number, maxLines: number) =>
|
||||
wrapText(value, maxChars, maxLines).join('\n')
|
||||
|
||||
export const GET: APIRoute = async ({ params }) => {
|
||||
let slug = params.slug ?? ''
|
||||
if (slug) {
|
||||
try {
|
||||
slug = decodeURIComponent(slug)
|
||||
} catch {
|
||||
// Keep the raw slug if decoding fails.
|
||||
}
|
||||
}
|
||||
const remotePost = slug ? await fetchRemotePost(slug) : null
|
||||
const post = remotePost ? normalizePost(remotePost.post) : null
|
||||
const title = post?.data?.title || SITE.title
|
||||
const dateValue = post?.data?.date ?? null
|
||||
const meta = dateValue
|
||||
? `${SITE.title} | ${formatDate(dateValue)}`
|
||||
: SITE.title
|
||||
const siteHost = new URL(SITE.href).host
|
||||
const formattedDate = dateValue ? formatDate(dateValue) : null
|
||||
const titleBlock = wrapAsBlock(title, 24, 2)
|
||||
|
||||
const badge = createElement('div', { style: badgeStyle }, [
|
||||
createElement('span', { style: badgeDotStyle }),
|
||||
siteHost,
|
||||
])
|
||||
|
||||
const cardChildren = [
|
||||
createElement('div', { style: gridStyle }),
|
||||
badge,
|
||||
createElement('div', { style: titleStyle }, titleBlock),
|
||||
]
|
||||
|
||||
const metaRow = createElement('div', { style: metaRowStyle }, [
|
||||
createElement('div', { style: metaLeftStyle }, [
|
||||
createElement('div', { style: metaLabelStyle }, 'PUBLISHED'),
|
||||
createElement(
|
||||
'div',
|
||||
{ style: metaValueStyle },
|
||||
formattedDate ?? 'Fresh dispatches',
|
||||
),
|
||||
]),
|
||||
createElement(
|
||||
'div',
|
||||
{ style: signatureStyle },
|
||||
formattedDate ? `${SITE.title} · ${siteHost}` : meta,
|
||||
),
|
||||
])
|
||||
|
||||
cardChildren.push(metaRow)
|
||||
|
||||
const accentRing = createElement('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
width: 520,
|
||||
height: 520,
|
||||
borderRadius: '50%',
|
||||
border: '1px solid rgba(236, 72, 153, 0.45)',
|
||||
right: -120,
|
||||
top: -140,
|
||||
transform: 'rotate(25deg)',
|
||||
opacity: 0.8,
|
||||
},
|
||||
})
|
||||
|
||||
const accentGlow = createElement('div', {
|
||||
style: {
|
||||
position: 'absolute',
|
||||
width: 420,
|
||||
height: 420,
|
||||
borderRadius: '50%',
|
||||
left: -120,
|
||||
bottom: -160,
|
||||
background: 'radial-gradient(circle, rgba(59, 130, 246, 0.35), transparent 65%)',
|
||||
filter: 'blur(2px)',
|
||||
},
|
||||
})
|
||||
|
||||
const card = createElement('div', { style: cardStyle }, cardChildren)
|
||||
const root = createElement('div', { style: containerStyle }, [
|
||||
accentGlow,
|
||||
accentRing,
|
||||
card,
|
||||
])
|
||||
|
||||
return new ImageResponse(
|
||||
root,
|
||||
{
|
||||
width: WIDTH,
|
||||
height: HEIGHT,
|
||||
headers: {
|
||||
'Content-Type': 'image/png',
|
||||
'Cache-Control':
|
||||
'public, max-age=0, s-maxage=86400, stale-while-revalidate=604800',
|
||||
},
|
||||
},
|
||||
)
|
||||
}
|
||||
@@ -5,7 +5,7 @@ import PageHead from '@/components/PageHead.astro'
|
||||
import PaginationComponent from '@/components/ui/pagination'
|
||||
import { SITE } from '@/consts'
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import { groupPostsByYear, normalizePost } from '@/lib/data-utils'
|
||||
import { fetchPostsPage, groupPostsByYear } from '@/lib/data-utils'
|
||||
|
||||
// export async function getStaticPaths({
|
||||
// paginate,
|
||||
@@ -22,22 +22,12 @@ const nowPage = parseInt(
|
||||
// use url param
|
||||
// const { page } = Astro.para
|
||||
|
||||
const page = await fetch(
|
||||
`https://notion-api.nvme0n1p.dev/v2/posts/?page=${nowPage}&length=${SITE.postsPerPage}`,
|
||||
)
|
||||
.then((res) => {
|
||||
if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`)
|
||||
return res.json()
|
||||
})
|
||||
.then((allPosts) => {
|
||||
const totalPages = allPosts.length
|
||||
const currentPage = nowPage
|
||||
return {
|
||||
data: allPosts.posts.map((post) => normalizePost(post)),
|
||||
currentPage,
|
||||
lastPage: totalPages,
|
||||
}
|
||||
})
|
||||
const postsPage = await fetchPostsPage(nowPage, SITE.postsPerPage)
|
||||
const page = {
|
||||
data: postsPage.posts,
|
||||
currentPage: nowPage,
|
||||
lastPage: postsPage.totalPages,
|
||||
}
|
||||
// fetch page
|
||||
if (page.lastPage < page.currentPage) {
|
||||
console.log('redirect to 404')
|
||||
|
||||
Reference in New Issue
Block a user