mirror of
https://github.com/lbr77/blog-astro.git
synced 2026-04-08 16:11:56 +00:00
Some customization
This commit is contained in:
13
public/avatar-placeholder.svg
Normal file
13
public/avatar-placeholder.svg
Normal file
@@ -0,0 +1,13 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 120 120" role="img" aria-labelledby="title desc">
|
||||
<title id="title">Placeholder Avatar</title>
|
||||
<desc id="desc">Simple circular avatar placeholder</desc>
|
||||
<defs>
|
||||
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
||||
<stop offset="0%" stop-color="#0f172a" stop-opacity="0.8"/>
|
||||
<stop offset="100%" stop-color="#1e293b" stop-opacity="0.9"/>
|
||||
</linearGradient>
|
||||
</defs>
|
||||
<circle cx="60" cy="60" r="58" fill="url(#g)" stroke="#1f2937" stroke-width="4"/>
|
||||
<circle cx="60" cy="50" r="20" fill="#e2e8f0" opacity="0.9"/>
|
||||
<path d="M30 96c6-16 18-24 30-24s24 8 30 24" fill="#e2e8f0" opacity="0.9"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 682 B |
@@ -15,13 +15,13 @@ const isSubpost = !!parentPost
|
||||
)}
|
||||
>
|
||||
<Link
|
||||
href={olderPost ? `/blog/${olderPost.id}#post-title` : '#'}
|
||||
href={newerPost ? `/blog/${newerPost.id}#post-title` : '#'}
|
||||
class={cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'rounded-lg group flex items-center justify-start size-full',
|
||||
!olderPost && 'pointer-events-none opacity-50 cursor-not-allowed',
|
||||
!newerPost && 'pointer-events-none opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
aria-disabled={!olderPost}
|
||||
aria-disabled={!newerPost}
|
||||
>
|
||||
<Icon
|
||||
name="lucide:arrow-left"
|
||||
@@ -33,47 +33,21 @@ const isSubpost = !!parentPost
|
||||
</span>
|
||||
<span class="w-full text-left text-sm text-balance text-ellipsis">
|
||||
{
|
||||
olderPost?.data.title ||
|
||||
(isSubpost ? 'No older subpost' : "You're at the oldest post!")
|
||||
newerPost?.data.title ||
|
||||
(isSubpost ? 'No newer subpost' : "You're at the newest post!")
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
{
|
||||
isSubpost && (
|
||||
<Link
|
||||
href={parentPost ? `/blog/${parentPost.id}#post-title` : '#'}
|
||||
class={cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'group flex size-full items-center justify-center rounded-lg',
|
||||
!parentPost && 'pointer-events-none cursor-not-allowed opacity-50',
|
||||
)}
|
||||
>
|
||||
<Icon
|
||||
name="lucide:corner-left-up"
|
||||
class="mr-2 size-4 transition-transform group-hover:-translate-y-1"
|
||||
/>
|
||||
<div class="flex flex-col items-center overflow-hidden text-wrap">
|
||||
<span class="text-muted-foreground text-center text-xs">
|
||||
Parent Post
|
||||
</span>
|
||||
<span class="w-full text-center text-sm text-balance text-ellipsis">
|
||||
{parentPost?.data.title || 'No parent post'}
|
||||
</span>
|
||||
</div>
|
||||
</Link>
|
||||
)
|
||||
}
|
||||
|
||||
<Link
|
||||
href={newerPost ? `/blog/${newerPost.id}#post-title` : '#'}
|
||||
href={olderPost ? `/blog/${olderPost.id}#post-title` : '#'}
|
||||
class={cn(
|
||||
buttonVariants({ variant: 'outline' }),
|
||||
'rounded-lg group flex items-center justify-end size-full',
|
||||
!newerPost && 'pointer-events-none opacity-50 cursor-not-allowed',
|
||||
!olderPost && 'pointer-events-none opacity-50 cursor-not-allowed',
|
||||
)}
|
||||
aria-disabled={!newerPost}
|
||||
aria-disabled={!olderPost}
|
||||
>
|
||||
<div class="flex flex-col items-end overflow-hidden text-wrap">
|
||||
<span class="text-muted-foreground text-right text-xs">
|
||||
@@ -81,8 +55,8 @@ const isSubpost = !!parentPost
|
||||
</span>
|
||||
<span class="w-full text-right text-sm text-balance text-ellipsis">
|
||||
{
|
||||
newerPost?.data.title ||
|
||||
(isSubpost ? 'No newer subpost' : "You're at the newest post!")
|
||||
olderPost?.data.title ||
|
||||
(isSubpost ? 'No older subpost' : "You're at the oldest post!")
|
||||
}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
@@ -129,7 +129,36 @@ const PaginationComponent: React.FC<PaginationProps> = ({
|
||||
totalPages,
|
||||
baseUrl,
|
||||
}) => {
|
||||
const pages = Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||
const buildPages = () => {
|
||||
if (totalPages <= 7) return Array.from({ length: totalPages }, (_, i) => i + 1)
|
||||
|
||||
const pageSet = new Set<number>([
|
||||
1,
|
||||
2,
|
||||
totalPages - 1,
|
||||
totalPages,
|
||||
currentPage - 1,
|
||||
currentPage,
|
||||
currentPage + 1,
|
||||
])
|
||||
|
||||
const sortedPages = Array.from(pageSet)
|
||||
.filter((p) => p >= 1 && p <= totalPages)
|
||||
.sort((a, b) => a - b)
|
||||
|
||||
const pages: Array<number | 'ellipsis'> = []
|
||||
let prev: number | null = null
|
||||
for (const page of sortedPages) {
|
||||
if (prev !== null && page - prev > 1) {
|
||||
pages.push('ellipsis')
|
||||
}
|
||||
pages.push(page)
|
||||
prev = page
|
||||
}
|
||||
return pages
|
||||
}
|
||||
|
||||
const pages = buildPages()
|
||||
|
||||
const getPageUrl = (page: number) => {
|
||||
if (page === 1) return baseUrl
|
||||
@@ -146,7 +175,12 @@ const PaginationComponent: React.FC<PaginationProps> = ({
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{pages.map((page) => (
|
||||
{pages.map((page, idx) =>
|
||||
page === 'ellipsis' ? (
|
||||
<PaginationItem key={`ellipsis-${idx}`}>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
) : (
|
||||
<PaginationItem key={page}>
|
||||
<PaginationLink
|
||||
href={getPageUrl(page)}
|
||||
@@ -155,12 +189,7 @@ const PaginationComponent: React.FC<PaginationProps> = ({
|
||||
{page}
|
||||
</PaginationLink>
|
||||
</PaginationItem>
|
||||
))}
|
||||
|
||||
{totalPages > 5 && (
|
||||
<PaginationItem>
|
||||
<PaginationEllipsis />
|
||||
</PaginationItem>
|
||||
),
|
||||
)}
|
||||
|
||||
<PaginationItem>
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import type { IconMap, SocialLink, Site } from '@/types'
|
||||
|
||||
export const SITE: Site = {
|
||||
title: 'astro-erudite',
|
||||
title: '溴化锂的笔记本',
|
||||
description:
|
||||
'astro-erudite is a opinionated, unstyled blogging template—built with Astro, Tailwind, and shadcn/ui.',
|
||||
href: 'https://astro-erudite.vercel.app',
|
||||
author: 'jktrn',
|
||||
locale: 'en-US',
|
||||
featuredPostCount: 2,
|
||||
postsPerPage: 3,
|
||||
'Security, Programming, Life',
|
||||
href: 'https://nvme0n1p.dev',
|
||||
author: 'libr',
|
||||
locale: 'zh-CN',
|
||||
featuredPostCount: 5,
|
||||
postsPerPage: 8,
|
||||
}
|
||||
|
||||
export const NAV_LINKS: SocialLink[] = [
|
||||
@@ -16,27 +16,27 @@ export const NAV_LINKS: SocialLink[] = [
|
||||
href: '/blog',
|
||||
label: 'blog',
|
||||
},
|
||||
{
|
||||
href: '/authors',
|
||||
label: 'authors',
|
||||
},
|
||||
{
|
||||
href: '/about',
|
||||
label: 'about',
|
||||
},
|
||||
{
|
||||
href: '/friends',
|
||||
label: 'friends',
|
||||
},
|
||||
]
|
||||
|
||||
export const SOCIAL_LINKS: SocialLink[] = [
|
||||
{
|
||||
href: 'https://github.com/jktrn',
|
||||
href: 'https://github.com/lbr77',
|
||||
label: 'GitHub',
|
||||
},
|
||||
{
|
||||
href: 'https://twitter.com/enscry',
|
||||
href: 'https://twitter.com/0x88ffa357',
|
||||
label: 'Twitter',
|
||||
},
|
||||
{
|
||||
href: 'mailto:jason@enscribe.dev',
|
||||
href: 'mailto:me@nvme0n1p.dev',
|
||||
label: 'Email',
|
||||
},
|
||||
{
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
---
|
||||
name: 'enscribe'
|
||||
pronouns: 'he/him'
|
||||
avatar: 'https://gravatar.com/avatar/9bfdc4ec972793cf05cb91efce5f4aaaec2a0da1bf4ec34dad0913f1d845faf6.webp?size=256'
|
||||
bio: 'd(-_-)b'
|
||||
website: 'https://enscribe.dev'
|
||||
twitter: 'https://twitter.com/enscry'
|
||||
github: 'https://github.com/jktrn'
|
||||
mail: 'jason@enscribe.dev'
|
||||
---
|
||||
10
src/content/authors/libr.md
Normal file
10
src/content/authors/libr.md
Normal file
@@ -0,0 +1,10 @@
|
||||
---
|
||||
name: '溴化锂'
|
||||
pronouns: 'he/him'
|
||||
avatar: 'https://github.com/lbr77.png'
|
||||
bio: ''
|
||||
website: 'https://nvme0n1p.dev'
|
||||
twitter: 'https://twitter.com/0x88ffa357'
|
||||
github: 'https://github.com/lbr77'
|
||||
mail: 'me@nvme0n1p.dev'
|
||||
---
|
||||
@@ -1,24 +1,86 @@
|
||||
import { getCollection, render, type CollectionEntry } from 'astro:content'
|
||||
import { readingTime, calculateWordCountFromHtml } from '@/lib/utils'
|
||||
|
||||
type NotionPost = {
|
||||
id: string
|
||||
slug?: string
|
||||
Content?: string
|
||||
excerpt?: string
|
||||
Tags?: string[]
|
||||
Published?: boolean
|
||||
'Published Date'?: string
|
||||
created_time?: string
|
||||
last_edited_time?: string
|
||||
}
|
||||
|
||||
export interface LinkEntry {
|
||||
id: string
|
||||
picLink?: string
|
||||
published?: boolean
|
||||
Description?: string
|
||||
links: string
|
||||
name: string
|
||||
created_time?: string
|
||||
last_edited_time?: string
|
||||
}
|
||||
|
||||
export async function getAllAuthors(): Promise<CollectionEntry<'authors'>[]> {
|
||||
return await getCollection('authors')
|
||||
}
|
||||
|
||||
export const POSTS_API_URL = 'https://notion-api.nvme0n1p.dev/v2/posts'
|
||||
|
||||
export async function getAllPosts(): Promise<CollectionEntry<'blog'>[]> {
|
||||
const fallback = async () => {
|
||||
const posts = await getCollection('blog')
|
||||
return posts
|
||||
.filter((post) => !post.data.draft && !isSubpost(post.id))
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
}
|
||||
|
||||
try {
|
||||
const res = await fetch(POSTS_API_URL)
|
||||
if (!res.ok) throw new Error(`Failed to fetch posts: ${res.status}`)
|
||||
const payload = (await res.json()) as { posts?: NotionPost[] }
|
||||
const posts = (payload.posts ?? []).filter(
|
||||
(post) => post.Published ?? true,
|
||||
)
|
||||
|
||||
const normalized = posts.map((post) => {
|
||||
const dateString =
|
||||
post['Published Date'] ?? post.created_time ?? new Date().toISOString()
|
||||
const date = new Date(dateString)
|
||||
const id = post.slug || post.id
|
||||
|
||||
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: [],
|
||||
},
|
||||
body: '',
|
||||
}
|
||||
})
|
||||
|
||||
return normalized
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
.filter((post) => !isSubpost(post.id)) as unknown as CollectionEntry<'blog'>[]
|
||||
} catch (error) {
|
||||
console.error('getAllPosts remote fetch failed, using fallback:', error)
|
||||
return fallback()
|
||||
}
|
||||
}
|
||||
|
||||
export async function getAllPostsAndSubposts(): Promise<
|
||||
CollectionEntry<'blog'>[]
|
||||
> {
|
||||
const posts = await getCollection('blog')
|
||||
return posts
|
||||
.filter((post) => !post.data.draft)
|
||||
.sort((a, b) => b.data.date.valueOf() - a.data.date.valueOf())
|
||||
// 远程源不区分子文章,直接沿用 getAllPosts 结果
|
||||
return getAllPosts()
|
||||
}
|
||||
|
||||
export async function getAllProjects(): Promise<CollectionEntry<'projects'>[]> {
|
||||
@@ -179,6 +241,38 @@ export function isSubpost(postId: string): boolean {
|
||||
return postId.includes('/')
|
||||
}
|
||||
|
||||
export const FRIENDS_API_URL = 'https://notion-api.nvme0n1p.dev/v2/links'
|
||||
export async function fetchRemotePost(
|
||||
slug: string,
|
||||
): Promise<NotionPost | null> {
|
||||
try {
|
||||
const res = await fetch(`${POSTS_API_URL}/${slug}`)
|
||||
if (!res.ok) throw new Error(`Failed to fetch post: ${res.status}`)
|
||||
const data = (await res.json()) as NotionPost
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error(`fetchRemotePost error for slug "${slug}":`, error)
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export async function getFriendLinks(): Promise<LinkEntry[]> {
|
||||
const fallback: LinkEntry[] = []
|
||||
|
||||
try {
|
||||
const res = await fetch(FRIENDS_API_URL)
|
||||
if (!res.ok) throw new Error(`Failed to fetch links: ${res.status}`)
|
||||
|
||||
const data = (await res.json()) as LinkEntry[]
|
||||
if (!Array.isArray(data)) return fallback
|
||||
|
||||
return data
|
||||
} catch (error) {
|
||||
console.error('getFriendLinks error:', error)
|
||||
return fallback
|
||||
}
|
||||
}
|
||||
|
||||
export async function getParentPost(
|
||||
subpostId: string,
|
||||
): Promise<CollectionEntry<'blog'> | null> {
|
||||
|
||||
@@ -2,46 +2,182 @@
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||
import Link from '@/components/Link.astro'
|
||||
import PageHead from '@/components/PageHead.astro'
|
||||
import ProjectCard from '@/components/ProjectCard.astro'
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import { getAllProjects } from '@/lib/data-utils'
|
||||
|
||||
const projects = await getAllProjects()
|
||||
---
|
||||
|
||||
<Layout class="max-w-3xl">
|
||||
<PageHead slot="head" title="About" />
|
||||
<Breadcrumbs items={[{ label: 'About', icon: 'lucide:info' }]} />
|
||||
|
||||
<section>
|
||||
<div class="min-w-full">
|
||||
<div class="prose mb-8">
|
||||
<p class="mt-0">
|
||||
astro-erudite is an opinionated, unstyled static blogging template
|
||||
that prioritizes simplicity and performance, built with <Link
|
||||
href="https://astro.build"
|
||||
external
|
||||
underline>Astro</Link
|
||||
>, <Link href="https://tailwindcss.com" external underline
|
||||
>Tailwind</Link
|
||||
>, and <Link href="https://ui.shadcn.com" external underline
|
||||
>shadcn/ui</Link
|
||||
>. It provides a clean foundation for your content while being
|
||||
extremely easy to customize.
|
||||
<section class="grid gap-6">
|
||||
<div class="relative rounded-lg border p-6">
|
||||
<div class="relative flex flex-col gap-4">
|
||||
<p class="text-xs uppercase tracking-[0.35em] text-muted-foreground">
|
||||
profile
|
||||
</p>
|
||||
<p>
|
||||
To learn more about the philosophy behind this template, check out the
|
||||
following blog post: <Link
|
||||
href="/blog/the-state-of-static-blogs"
|
||||
underline>The State of Static Blogs in 2024</Link
|
||||
>.
|
||||
<h1 class="text-3xl font-semibold">关于我</h1>
|
||||
<p class="max-w-3xl text-muted-foreground">
|
||||
啥都写一点,但是啥都不精通。前端后端客户端,运维网安都会那么一点点,尝试写出点什么项目但总是失败。
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 class="mb-4 text-2xl font-medium">Example Projects Listing</h2>
|
||||
<div class="flex flex-col gap-4">
|
||||
{projects.map((project) => <ProjectCard project={project} />)}
|
||||
<div class="grid gap-4 md:grid-cols-3">
|
||||
<div class="rounded-lg border p-5">
|
||||
<h3 class="text-lg font-semibold">安全</h3>
|
||||
<p class="text-sm leading-relaxed text-muted-foreground">
|
||||
非典型 Misc,啥都做点,(感觉更像是全栈?)
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border p-5">
|
||||
<h3 class="text-lg font-semibold">代码</h3>
|
||||
<p class="text-sm leading-relaxed text-muted-foreground">
|
||||
近期在写 C 和 Swift,之前写过 Golang、Python、JavaScript
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border p-5">
|
||||
<h3 class="text-lg font-semibold">生活</h3>
|
||||
<p class="text-sm leading-relaxed text-muted-foreground">
|
||||
二次元、骑车、游泳、羽毛球、CS、摄影
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="rounded-lg border p-6">
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<p class="text-xs uppercase text-muted-foreground">setup</p>
|
||||
<h3 class="text-xl font-semibold">常用设备</h3>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-4 grid gap-3 md:grid-cols-2">
|
||||
<div class="rounded-lg border p-4">
|
||||
<p class="text-sm font-semibold">PC</p>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
M2 Macbook Air · MSI Stealth 14 · iPad Air 5
|
||||
</p>
|
||||
</div>
|
||||
<div class="rounded-lg border p-4">
|
||||
<p class="text-sm font-semibold">手机</p>
|
||||
<p class="text-sm text-muted-foreground">iPhone 17 · vivo x100 · iPhone 13 mini</p>
|
||||
</div>
|
||||
<div class="rounded-lg border p-4 md:col-span-2">
|
||||
<p class="text-sm font-semibold">影音与记录</p>
|
||||
<p class="text-sm text-muted-foreground">Sony XM5 · Nikon Zfc</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<section class="rounded-lg border p-6">
|
||||
<p class="text-xs uppercase text-muted-foreground">site</p>
|
||||
<h3 class="text-xl font-semibold">关于本站</h3>
|
||||
<p class="mt-2 text-sm leading-relaxed text-muted-foreground">
|
||||
栈在 <Link href="https://astro.build" external underline>Astro</Link>,
|
||||
<Link href="https://tailwindcss.com" external underline>Tailwind</Link>,
|
||||
<Link href="https://ui.shadcn.com" external underline>shadcn/ui</Link> 之上,开源在
|
||||
<Link href="https://github.com/jktrn/astro-erudite" external underline>GitHub</Link>。
|
||||
</p>
|
||||
<h4 class="text-l font-semibold mt-2">大事记</h4>
|
||||
<div class="mt-3 overflow-x-auto rounded-lg border">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-muted/60 text-left text-muted-foreground">
|
||||
<tr>
|
||||
<th class="px-4 py-2 font-semibold">时间</th>
|
||||
<th class="px-4 py-2 font-semibold">事件</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="divide-y">
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">2018</td>
|
||||
<td class="px-4 py-2">
|
||||
第一次建站,域名 <code>stevelbr.ga</code>,使用 hexo 和不知道有几种主题
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">2020</td>
|
||||
<td class="px-4 py-2">
|
||||
购入腾讯云轻量服务器,购入域名 <code>stevelbr.top</code> ,使用 typecho 和
|
||||
typecho-theme-handsome 建站,接入备案。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">2023</td>
|
||||
<td class="px-4 py-2">
|
||||
停止续费腾讯云轻量,购入域名 <code>nvme0n1p.dev</code>,使用 hugo+serverless service 建站。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">2024.1</td>
|
||||
<td class="px-4 py-2">
|
||||
使用 <Link href="http://cali.so" external underline>cali.so</Link> nextjs自建服务,运行于 vercel。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">2024.2</td>
|
||||
<td class="px-4 py-2">
|
||||
使用 astro 重构 typecho theme void 主题,使用 notion 作为 cms。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">2024.8</td>
|
||||
<td class="px-4 py-2">
|
||||
使用 nuxtjs 重构 typecho theme void,同样使用 notion 作为 cms。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">2024.10</td>
|
||||
<td class="px-4 py-2">
|
||||
优化 nuxtjs 代码,现在全站支持无刷新页面切换。<del>同时增加 algolia docsearch 魔改版作为站内搜索。</del>好像没开 Server Side Rendering ,炸了
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">2024.11</td>
|
||||
<td class="px-4 py-2">
|
||||
感谢<Link href="https://www.dkdun.cn/" external underline>林枫云</Link>对 ctfer 的支持,切换到了他家的香港服务器。
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">2025.2</td>
|
||||
<td class="px-4 py-2">用nextjs重写了。优化了访问速度,减少了了图片。</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">2025.7</td>
|
||||
<td class="px-4 py-2">抽空把algolia search弄好了。(真的吗</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td class="px-4 py-2 whitespace-nowrap">2025.12</td>
|
||||
<td class="px-4 py-2">换成Astro!</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border p-6">
|
||||
<p class="text-xs uppercase text-muted-foreground">comment</p>
|
||||
<h3 class="text-xl font-semibold mb-3">留言 / 评论</h3>
|
||||
<div id="tcomment"></div>
|
||||
<script
|
||||
is:inline
|
||||
src="https://cdn.jsdelivr.net/npm/twikoo@1.6.44/dist/twikoo.min.js"
|
||||
></script>
|
||||
<script is:inline>
|
||||
const mountTwikoo = () => {
|
||||
if (!window.twikoo) return
|
||||
window.twikoo.init({
|
||||
envId: 'https://twikoo.hk.nvme0n1p.dev/',
|
||||
el: '#tcomment',
|
||||
})
|
||||
}
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
mountTwikoo()
|
||||
} else {
|
||||
addEventListener('astro:page-load', mountTwikoo)
|
||||
addEventListener('DOMContentLoaded', mountTwikoo, { once: true })
|
||||
}
|
||||
</script>
|
||||
</section>
|
||||
</section>
|
||||
</Layout>
|
||||
|
||||
@@ -1,58 +0,0 @@
|
||||
---
|
||||
import AuthorCard from '@/components/AuthorCard.astro'
|
||||
import BlogCard from '@/components/BlogCard.astro'
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||
import PageHead from '@/components/PageHead.astro'
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import { getAllAuthors, getPostsByAuthor } from '@/lib/data-utils'
|
||||
|
||||
export async function getStaticPaths() {
|
||||
const authors = await getAllAuthors()
|
||||
return authors.map((author) => ({
|
||||
params: { id: author.id },
|
||||
props: { author },
|
||||
}))
|
||||
}
|
||||
|
||||
const { author } = Astro.props
|
||||
const authorPosts = await getPostsByAuthor(author.id)
|
||||
---
|
||||
|
||||
<Layout class="max-w-3xl">
|
||||
<PageHead
|
||||
slot="head"
|
||||
title={`${author.data.name} (Author)`}
|
||||
description={author.data.bio || `Profile of ${author.data.name}.`}
|
||||
noindex
|
||||
/>
|
||||
<Breadcrumbs
|
||||
items={[
|
||||
{ href: '/authors', label: 'Authors', icon: 'lucide:users' },
|
||||
{ label: author.data.name, icon: 'lucide:user' },
|
||||
]}
|
||||
/>
|
||||
|
||||
<section>
|
||||
<AuthorCard author={author} />
|
||||
</section>
|
||||
<section class="flex flex-col gap-y-4">
|
||||
<h2 class="text-2xl font-medium">Posts by {author.data.name}</h2>
|
||||
{
|
||||
authorPosts.length > 0 ? (
|
||||
<ul class="flex flex-col gap-4">
|
||||
{authorPosts
|
||||
.filter((post) => !post.data.draft)
|
||||
.map((post) => (
|
||||
<li>
|
||||
<BlogCard entry={post} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="text-muted-foreground">
|
||||
No posts available from this author.
|
||||
</p>
|
||||
)
|
||||
}
|
||||
</section>
|
||||
</Layout>
|
||||
@@ -1,27 +0,0 @@
|
||||
---
|
||||
import AuthorCard from '@/components/AuthorCard.astro'
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||
import PageHead from '@/components/PageHead.astro'
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import { getAllAuthors } from '@/lib/data-utils'
|
||||
|
||||
const authors = await getAllAuthors()
|
||||
---
|
||||
|
||||
<Layout class="max-w-3xl">
|
||||
<PageHead slot="head" title="Authors" />
|
||||
<Breadcrumbs items={[{ label: 'Authors', icon: 'lucide:users' }]} />
|
||||
{
|
||||
authors.length > 0 ? (
|
||||
<ul class="flex flex-col gap-4">
|
||||
{authors.map((author) => (
|
||||
<li>
|
||||
<AuthorCard author={author} />
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
) : (
|
||||
<p class="text-muted-foreground text-center">No authors found.</p>
|
||||
)
|
||||
}
|
||||
</Layout>
|
||||
107
src/pages/friends.astro
Normal file
107
src/pages/friends.astro
Normal file
@@ -0,0 +1,107 @@
|
||||
---
|
||||
import Breadcrumbs from '@/components/Breadcrumbs.astro'
|
||||
import Link from '@/components/Link.astro'
|
||||
import PageHead from '@/components/PageHead.astro'
|
||||
import Layout from '@/layouts/Layout.astro'
|
||||
import {
|
||||
FRIENDS_API_URL,
|
||||
getFriendLinks,
|
||||
type LinkEntry,
|
||||
} from '@/lib/data-utils'
|
||||
|
||||
const placeholderAvatar = '/avatar-placeholder.svg'
|
||||
const visibleLinks = await getFriendLinks()
|
||||
---
|
||||
|
||||
<Layout class="max-w-3xl">
|
||||
<PageHead slot="head" title="Friends" />
|
||||
<Breadcrumbs items={[{ label: 'Friends', icon: 'lucide:users' }]} />
|
||||
|
||||
<section class="rounded-lg border p-6">
|
||||
<div class="flex flex-col gap-2">
|
||||
<h1 class="text-2xl font-semibold">友链 / Friends</h1>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
这里收集一些朋友和有趣的网站
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="grid gap-4">
|
||||
{
|
||||
visibleLinks.map((friend) => (
|
||||
<article class="rounded-lg border p-4 transition hover:border-primary/60">
|
||||
<div class="flex items-start gap-3">
|
||||
{
|
||||
friend.picLink ? (
|
||||
<img
|
||||
src={friend.picLink}
|
||||
alt={friend.name}
|
||||
loading="lazy"
|
||||
class="h-12 w-12 rounded-full border object-cover"
|
||||
width="48"
|
||||
height="48"
|
||||
onerror={`this.onerror=null;this.src='${placeholderAvatar}';`}
|
||||
/>
|
||||
) : (
|
||||
<img
|
||||
src={placeholderAvatar}
|
||||
alt="avatar placeholder"
|
||||
loading="lazy"
|
||||
class="h-12 w-12 rounded-full border object-cover"
|
||||
width="48"
|
||||
height="48"
|
||||
/>
|
||||
)
|
||||
}
|
||||
<div class="flex-1 space-y-1">
|
||||
<div class="flex items-center justify-between gap-3">
|
||||
<h2 class="text-lg font-semibold leading-tight">
|
||||
<Link href={friend.links} external underline>
|
||||
{friend.name}
|
||||
</Link>
|
||||
</h2>
|
||||
</div>
|
||||
<p class="text-sm text-muted-foreground">
|
||||
{friend.Description || '你来到了知识的荒原'}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</article>
|
||||
))
|
||||
}
|
||||
</section>
|
||||
|
||||
<section class="rounded-lg border p-6">
|
||||
<h3 class="text-lg font-semibold">申请方式</h3>
|
||||
<p class="text-sm text-muted-foreground mt-2">
|
||||
请提供站点名称、链接、一句话介绍。
|
||||
</p>
|
||||
<pre class="mt-3 whitespace-pre-wrap break-words rounded-md bg-muted/50 p-3 text-xs text-muted-foreground">
|
||||
name: 溴化锂的笔记本
|
||||
link: https://nvme0n1p.dev
|
||||
avatar: https://gravatar.com/avatar/29d64df3ca2a9dac5a7fffa5372fb80fb3270ceb223de2af0c33cdc4b2cbe954?v=1687917579000&size=256&d=initials
|
||||
description: 醉后不知天在水,满船清梦压星河。
|
||||
</pre>
|
||||
<div id="tcomment"></div>
|
||||
<script
|
||||
is:inline
|
||||
src="https://cdn.jsdelivr.net/npm/twikoo@1.6.44/dist/twikoo.min.js"
|
||||
></script>
|
||||
<script is:inline>
|
||||
const mountTwikoo = () => {
|
||||
if (!window.twikoo) return
|
||||
window.twikoo.init({
|
||||
envId: 'https://twikoo.hk.nvme0n1p.dev/',
|
||||
el: '#tcomment',
|
||||
})
|
||||
}
|
||||
|
||||
if (document.readyState === 'complete') {
|
||||
mountTwikoo()
|
||||
} else {
|
||||
addEventListener('astro:page-load', mountTwikoo)
|
||||
addEventListener('DOMContentLoaded', mountTwikoo, { once: true })
|
||||
}
|
||||
</script>
|
||||
</section>
|
||||
</Layout>
|
||||
@@ -14,48 +14,16 @@ const blog = await getRecentPosts(SITE.featuredPostCount)
|
||||
<PageHead slot="head" title="Home" />
|
||||
<section class="rounded-lg border">
|
||||
<div class="flex flex-col space-y-1.5 p-6">
|
||||
<h3 class="text-3xl leading-none font-medium">er·u·dite</h3>
|
||||
<h3 class="text-3xl leading-none font-medium">溴化锂的笔记本</h3>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
/ˈer(y)əˌdīt/ • <span class="font-medium">adjective</span>
|
||||
LiBr's Notebook
|
||||
</p>
|
||||
</div>
|
||||
<div class="p-6 pt-0">
|
||||
<p class="text-muted-foreground mb-2 text-sm">
|
||||
astro-erudite is an opinionated, unstyled static blogging template built
|
||||
with <Link
|
||||
href="https://astro.build"
|
||||
class="text-foreground"
|
||||
external
|
||||
underline>Astro</Link
|
||||
>, <Link
|
||||
href="https://tailwindcss.com"
|
||||
class="text-foreground"
|
||||
external
|
||||
underline>Tailwind</Link
|
||||
>, and <Link
|
||||
href="https://ui.shadcn.com"
|
||||
class="text-foreground"
|
||||
external
|
||||
underline>shadcn/ui</Link
|
||||
>. Extraordinarily loosely based on the <Link
|
||||
href="https://astro-micro.vercel.app/"
|
||||
class="text-foreground"
|
||||
external
|
||||
underline>Astro Micro</Link
|
||||
> theme.
|
||||
</p>
|
||||
<p class="text-muted-foreground text-sm">
|
||||
To use this template, check out the <Link
|
||||
href="https://github.com/jktrn/astro-erudite"
|
||||
class="text-foreground"
|
||||
underline
|
||||
external>GitHub</Link
|
||||
> repository. To learn more about why this template exists, read this blog
|
||||
post: <Link
|
||||
href="/blog/the-state-of-static-blogs"
|
||||
class="text-foreground"
|
||||
underline>The State of Static Blogs in 2024</Link
|
||||
>.
|
||||
nameless cybersecurity researcher. always studying.
|
||||
<br />
|
||||
More about me <Link href="/about" underline>here</Link>.
|
||||
</p>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
Reference in New Issue
Block a user