customize more

This commit is contained in:
2025-11-28 16:02:10 +08:00
parent 9133d23a15
commit 8b9feeeb10
13 changed files with 70 additions and 112 deletions

3
.prettierignore Normal file
View File

@@ -0,0 +1,3 @@
dist/**/*
tmp/**/*
public/js/fslightbox.js

12
.prettierrc Normal file
View File

@@ -0,0 +1,12 @@
{
"trailingComma": "es5",
"singleQuote": true,
"overrides": [
{
"files": ["*.ts", "*.astro"],
"options": {
"semi": false
}
}
]
}

View File

@@ -19,7 +19,8 @@ import { pluginLineNumbers } from '@expressive-code/plugin-line-numbers'
import tailwindcss from '@tailwindcss/vite' import tailwindcss from '@tailwindcss/vite'
export default defineConfig({ export default defineConfig({
site: 'https://astro-erudite.vercel.app', site: 'https://nvme0n1p.dev',
output: 'server',
integrations: [ integrations: [
expressiveCode({ expressiveCode({
themes: ['github-light', 'github-dark'], themes: ['github-light', 'github-dark'],

View File

@@ -73,8 +73,6 @@ const subpostCount = !isSubpost(entry.id) ? await getSubpostCount(entry.id) : 0
) )
} }
<span>{formattedDate}</span> <span>{formattedDate}</span>
<Separator orientation="vertical" className="h-4!" />
<span>{readTime}</span>
{ {
subpostCount > 0 && ( subpostCount > 0 && (
<> <>

View File

@@ -5,7 +5,6 @@ import {
getParentId, getParentId,
getParentPost, getParentPost,
getPostById, getPostById,
getPostReadingTime,
getSubpostsForParent, getSubpostsForParent,
isSubpost, isSubpost,
} from '@/lib/data-utils' } from '@/lib/data-utils'
@@ -23,17 +22,9 @@ const parentPost = isCurrentSubpost ? await getParentPost(currentPostId) : null
const activePost = parentPost || currentPost const activePost = parentPost || currentPost
const isActivePost = activePost?.id === currentPostId const isActivePost = activePost?.id === currentPostId
const activePostReadingTime = activePost
? await getPostReadingTime(activePost.id)
: null
const activePostCombinedReadingTime =
activePost && subposts.length > 0
? await getCombinedReadingTime(activePost.id)
: null
const subpostsWithReadingTime = await Promise.all( const subpostsWithReadingTime = await Promise.all(
subposts.map(async (subpost) => ({ subposts.map(async (subpost) => ({
...subpost, ...subpost,
readingTime: await getPostReadingTime(subpost.id),
})), })),
) )
@@ -97,7 +88,7 @@ const currentSubpostDetails = isCurrentSubpost
<span class="line-clamp-2"> <span class="line-clamp-2">
{activePost.data.title} {activePost.data.title}
</span> </span>
{activePostReadingTime && ( {/* {activePostReadingTime && (
<span class="text-muted-foreground/80 text-xs"> <span class="text-muted-foreground/80 text-xs">
{activePostReadingTime} {activePostReadingTime}
{activePostCombinedReadingTime && {activePostCombinedReadingTime &&
@@ -109,7 +100,7 @@ const currentSubpostDetails = isCurrentSubpost
</span> </span>
)} )}
</span> </span>
)} )} */}
</div> </div>
</div> </div>
) : ( ) : (
@@ -126,19 +117,6 @@ const currentSubpostDetails = isCurrentSubpost
<span class="line-clamp-2"> <span class="line-clamp-2">
{activePost.data.title} {activePost.data.title}
</span> </span>
{activePostReadingTime && (
<span class="text-muted-foreground/80 hover:text-foreground/80 text-xs">
{activePostReadingTime}
{activePostCombinedReadingTime &&
activePostCombinedReadingTime !==
activePostReadingTime && (
<span>
{' '}
({activePostCombinedReadingTime} total)
</span>
)}
</span>
)}
</div> </div>
</a> </a>
)} )}
@@ -157,9 +135,6 @@ const currentSubpostDetails = isCurrentSubpost
/> />
<div class="flex flex-col"> <div class="flex flex-col">
<span class="line-clamp-2">{subpost.data.title}</span> <span class="line-clamp-2">{subpost.data.title}</span>
<span class="text-muted-foreground/80 text-xs">
{subpost.readingTime}
</span>
</div> </div>
</div> </div>
) : ( ) : (
@@ -174,9 +149,6 @@ const currentSubpostDetails = isCurrentSubpost
/> />
<div class="flex flex-col"> <div class="flex flex-col">
<span class="line-clamp-2">{subpost.data.title}</span> <span class="line-clamp-2">{subpost.data.title}</span>
<span class="text-muted-foreground/80 hover:text-foreground/80 text-xs">
{subpost.readingTime}
</span>
</div> </div>
</a> </a>
), ),

View File

@@ -6,7 +6,6 @@ import {
getParentId, getParentId,
getParentPost, getParentPost,
getPostById, getPostById,
getPostReadingTime,
getSubpostsForParent, getSubpostsForParent,
isSubpost, isSubpost,
} from '@/lib/data-utils' } from '@/lib/data-utils'
@@ -24,17 +23,9 @@ const parentPost = isCurrentSubpost ? await getParentPost(currentPostId) : null
const activePost = parentPost || currentPost const activePost = parentPost || currentPost
const isActivePost = activePost?.id === currentPostId const isActivePost = activePost?.id === currentPostId
const activePostReadingTime = activePost
? await getPostReadingTime(activePost.id)
: null
const activePostCombinedReadingTime =
activePost && subposts.length > 0
? await getCombinedReadingTime(activePost.id)
: null
const subpostsWithReadingTime = await Promise.all( const subpostsWithReadingTime = await Promise.all(
subposts.map(async (subpost) => ({ subposts.map(async (subpost) => ({
...subpost, ...subpost,
readingTime: await getPostReadingTime(subpost.id),
})), })),
) )
--- ---
@@ -64,19 +55,6 @@ const subpostsWithReadingTime = await Promise.all(
<span class="line-clamp-2 text-pretty"> <span class="line-clamp-2 text-pretty">
{activePost.data.title} {activePost.data.title}
</span> </span>
{activePostReadingTime && (
<span class="text-muted-foreground/80 text-xs">
{activePostReadingTime}
{activePostCombinedReadingTime &&
activePostCombinedReadingTime !==
activePostReadingTime && (
<span>
{' '}
({activePostCombinedReadingTime} total)
</span>
)}
</span>
)}
</div> </div>
</div> </div>
) : ( ) : (
@@ -93,19 +71,6 @@ const subpostsWithReadingTime = await Promise.all(
<span class="line-clamp-2 text-pretty"> <span class="line-clamp-2 text-pretty">
{activePost.data.title} {activePost.data.title}
</span> </span>
{activePostReadingTime && (
<span class="text-muted-foreground/80 text-xs">
{activePostReadingTime}
{activePostCombinedReadingTime &&
activePostCombinedReadingTime !==
activePostReadingTime && (
<span>
{' '}
({activePostCombinedReadingTime} total)
</span>
)}
</span>
)}
</div> </div>
</Link> </Link>
)} )}
@@ -128,9 +93,6 @@ const subpostsWithReadingTime = await Promise.all(
<span class="line-clamp-2 text-pretty"> <span class="line-clamp-2 text-pretty">
{subpost.data.title} {subpost.data.title}
</span> </span>
<span class="text-muted-foreground/80 text-xs">
{subpost.readingTime}
</span>
</div> </div>
</div> </div>
) : ( ) : (
@@ -147,9 +109,6 @@ const subpostsWithReadingTime = await Promise.all(
<span class="line-clamp-2 text-pretty"> <span class="line-clamp-2 text-pretty">
{subpost.data.title} {subpost.data.title}
</span> </span>
<span class="text-muted-foreground/80 text-xs">
{subpost.readingTime}
</span>
</div> </div>
</Link> </Link>
), ),

View File

@@ -18,6 +18,10 @@ export const NAV_LINKS: SocialLink[] = [
href: '/blog', href: '/blog',
label: 'blog', label: 'blog',
}, },
{
href: '/tags',
label: 'tags',
},
{ {
href: '/about', href: '/about',
label: 'about', label: 'about',

View File

@@ -9,6 +9,17 @@ import {
} from './blog-helpers' } from './blog-helpers'
const DEFAULT_AUTHOR_ID = 'libr' const DEFAULT_AUTHOR_ID = 'libr'
type ContentCollection = 'blog' | 'authors' | 'projects'
async function getCollectionSafe<T extends ContentCollection>(
name: T,
): Promise<CollectionEntry<T>[]> {
try {
return await getCollection(name)
} catch {
return []
}
}
type NotionPost = { type NotionPost = {
id: string id: string
@@ -56,14 +67,14 @@ export interface LinkEntry {
} }
export async function getAllAuthors(): Promise<CollectionEntry<'authors'>[]> { export async function getAllAuthors(): Promise<CollectionEntry<'authors'>[]> {
return await getCollection('authors') return getCollectionSafe('authors')
} }
export const POSTS_API_URL = 'https://notion-api.nvme0n1p.dev/v2/posts' export const POSTS_API_URL = 'https://notion-api.nvme0n1p.dev/v2/posts'
export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> { export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
const fallback = async () => { const fallback = async () => {
const posts = await getCollection('blog') const posts = await getCollectionSafe('blog')
return posts return posts
.filter((post) => !post.data.draft && !isSubpost(post.id)) .filter((post) => !post.data.draft && !isSubpost(post.id))
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf()) .sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
@@ -91,6 +102,7 @@ export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
const date = new Date(dateString) const date = new Date(dateString)
const id = post.slug || post.id const id = post.slug || post.id
const banner = (post as any).banner || null const banner = (post as any).banner || null
const image = banner
return { return {
id, id,
@@ -103,6 +115,7 @@ export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
draft: !(post.Published ?? true), draft: !(post.Published ?? true),
authors: [DEFAULT_AUTHOR_ID], authors: [DEFAULT_AUTHOR_ID],
banner, banner,
image,
}, },
body: '', body: '',
} }
@@ -125,7 +138,7 @@ export async function getAllPostsAndSubposts(): Promise<
} }
export async function getAllProjects(): Promise<CollectionEntry<'projects'>[]> { export async function getAllProjects(): Promise<CollectionEntry<'projects'>[]> {
const projects = await getCollection('projects') const projects = await getCollectionSafe('projects')
return projects.sort((a, b) => { return projects.sort((a, b) => {
const dateA = a.data.startDate?.getTime() || 0 const dateA = a.data.startDate?.getTime() || 0
const dateB = b.data.startDate?.getTime() || 0 const dateB = b.data.startDate?.getTime() || 0
@@ -155,7 +168,7 @@ export async function getAdjacentPosts(currentId: string): Promise<{
const allPosts = await getAllPosts() const allPosts = await getAllPosts()
const parent = allPosts.find((post) => post.id === parentId) || null const parent = allPosts.find((post) => post.id === parentId) || null
const posts = await getCollection('blog') const posts = await getCollectionSafe('blog')
const subposts = posts const subposts = posts
.filter( .filter(
(post) => (post) =>
@@ -242,7 +255,7 @@ export function getParentId(subpostId: string): string {
export async function getSubpostsForParent( export async function getSubpostsForParent(
parentId: string, parentId: string,
): Promise<CollectionEntry<'blog'>[]> { ): Promise<CollectionEntry<'blog'>[]> {
const posts = await getCollection('blog') const posts = await getCollectionSafe('blog')
return posts return posts
.filter( .filter(
(post) => (post) =>
@@ -305,6 +318,12 @@ export async function fetchRemotePostContent(
if (!res.ok) throw new Error(`Failed to fetch post content: ${res.status}`) if (!res.ok) throw new Error(`Failed to fetch post content: ${res.status}`)
const data = (await res.json()) as Partial<RemotePostPayload> const data = (await res.json()) as Partial<RemotePostPayload>
if (!data.post || !data.blockMap) return null if (!data.post || !data.blockMap) return null
const post = data.post as any
if (post?.banner && !post?.image) {
post.image = post.banner
}
return data as RemotePostPayload return data as RemotePostPayload
} catch (error) { } catch (error) {
console.error(`fetchRemotePostContent error for slug "${slug}":`, error) console.error(`fetchRemotePostContent error for slug "${slug}":`, error)

View File

@@ -1,5 +1,6 @@
import { type ClassValue, clsx } from 'clsx' import { type ClassValue, clsx } from 'clsx'
import { twMerge } from 'tailwind-merge' import { twMerge } from 'tailwind-merge'
import type { ImageMetadata } from 'astro:assets'
export function cn(...inputs: ClassValue[]) { export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs)) return twMerge(clsx(inputs))
@@ -35,3 +36,19 @@ export function getHeadingMargin(depth: number): string {
} }
return margins[depth] || '' return margins[depth] || ''
} }
export type ImageSource = string | ImageMetadata
export function normalizeImageSource(value: unknown): ImageSource | null {
if (!value) return null
if (typeof value === 'string') return value
if (
typeof value === 'object' &&
value !== null &&
'src' in value &&
typeof (value as ImageMetadata).src === 'string'
) {
return value as ImageMetadata
}
return null
}

View File

@@ -14,10 +14,8 @@ import Layout from '@/layouts/Layout.astro'
import { import {
getAdjacentPosts, getAdjacentPosts,
getAllPostsAndSubposts, getAllPostsAndSubposts,
getCombinedReadingTime,
getParentId, getParentId,
getParentPost, getParentPost,
getPostReadingTime,
getSubpostCount, getSubpostCount,
getTOCSections, getTOCSections,
fetchRemotePostContent, fetchRemotePostContent,
@@ -27,7 +25,7 @@ import {
parseAuthors, parseAuthors,
} from '@/lib/data-utils' } from '@/lib/data-utils'
import type { TOCSection } from '@/lib/data-utils' import type { TOCSection } from '@/lib/data-utils'
import { formatDate, readingTime } from '@/lib/utils' import { formatDate } from '@/lib/utils'
import { Icon } from 'astro-icon/components' import { Icon } from 'astro-icon/components'
import { Image } from 'astro:assets' import { Image } from 'astro:assets'
import { render } from 'astro:content' import { render } from 'astro:content'
@@ -68,13 +66,6 @@ const hasChildPosts = await hasSubposts(currentPostId)
const subpostCount = !isCurrentSubpost const subpostCount = !isCurrentSubpost
? await getSubpostCount(currentPostId) ? await getSubpostCount(currentPostId)
: 0 : 0
const postReadingTime = remoteContent
? readingTime(remoteContent.wordCount)
: await getPostReadingTime(currentPostId)
const combinedReadingTime =
!remoteContent && hasChildPosts && !isCurrentSubpost
? await getCombinedReadingTime(currentPostId)
: null
const tocSections: TOCSection[] = remoteContent const tocSections: TOCSection[] = remoteContent
? remoteContent.headings.length > 0 ? remoteContent.headings.length > 0
@@ -87,12 +78,8 @@ const tocSections: TOCSection[] = remoteContent
] ]
: [] : []
: await getTOCSections(currentPostId) : await getTOCSections(currentPostId)
const heroImage = const heroImage = post.data.banner
post.data.banner && typeof post.data.banner === 'object' && 'src' in post.data.banner export const prerender = false;
? post.data.banner
: post.data.image && typeof post.data.image === 'object' && 'src' in post.data.image
? post.data.image
: null
--- ---
<Layout> <Layout>
@@ -204,23 +191,6 @@ const heroImage =
<span>{formatDate(post.data.date)}</span> <span>{formatDate(post.data.date)}</span>
</div> </div>
<div
class="flex w-full items-center justify-center gap-2 py-2 sm:w-fit sm:px-2 sm:py-0 first:sm:pl-0 last:sm:pr-0"
>
<span>
{postReadingTime}
{
combinedReadingTime &&
combinedReadingTime !== postReadingTime && (
<span class="text-muted-foreground">
{' '}
({combinedReadingTime} total)
</span>
)
}
</span>
</div>
{ {
subpostCount > 0 && ( subpostCount > 0 && (
<div class="flex w-full items-center justify-center gap-1 py-2 sm:w-fit sm:px-2 sm:py-0 first:sm:pl-0 last:sm:pr-0"> <div class="flex w-full items-center justify-center gap-1 py-2 sm:w-fit sm:px-2 sm:py-0 first:sm:pl-0 last:sm:pr-0">

View File

@@ -21,6 +21,7 @@ const { page } = Astro.props
const postsByYear = groupPostsByYear(page.data) const postsByYear = groupPostsByYear(page.data)
const years = Object.keys(postsByYear).sort((a, b) => parseInt(b) - parseInt(a)) const years = Object.keys(postsByYear).sort((a, b) => parseInt(b) - parseInt(a))
export const prerender = false;
--- ---
<Layout class="max-w-3xl"> <Layout class="max-w-3xl">

View File

@@ -24,6 +24,7 @@ export async function getStaticPaths() {
} }
const { tag, posts } = Astro.props const { tag, posts } = Astro.props
export const prerender = false;
--- ---
<Layout class="max-w-3xl"> <Layout class="max-w-3xl">

View File

@@ -8,6 +8,7 @@ import { getSortedTags } from '@/lib/data-utils'
import { Icon } from 'astro-icon/components' import { Icon } from 'astro-icon/components'
const sortedTags = await getSortedTags() const sortedTags = await getSortedTags()
export const prerender = false;
--- ---
<Layout class="max-w-3xl"> <Layout class="max-w-3xl">