This commit is contained in:
2026-06-18 10:36:41 +08:00 Unverified
parent 1a862adddc
commit 7c3679e885
41 changed files with 10544 additions and 163 deletions
+21
View File
@@ -0,0 +1,21 @@
---
import type { BlogPost } from '@/lib/posts';
interface Props {
post: BlogPost;
}
const { post } = Astro.props;
---
<article class="post-card">
<a href={post.href}>
<time datetime={post.date.toISOString()}>{post.dateText}</time>
<h2>{post.title}</h2>
{post.summary && <p>{post.summary}</p>}
</a>
<div class="meta-list">
{post.categories.map((category) => <a href={`/categories/${encodeURIComponent(category)}/`}>{category}</a>)}
{post.tags.map((tag) => <a href={`/tags/${encodeURIComponent(tag)}/`}>#{tag}</a>)}
</div>
</article>
+2
View File
@@ -0,0 +1,2 @@
/// <reference path="../.astro/types.d.ts" />
/// <reference types="astro/client" />
+44
View File
@@ -0,0 +1,44 @@
---
import '../styles/site.css';
interface Props {
title?: string;
description?: string;
}
const siteTitle = "Bi's Blog";
const { title = siteTitle, description = 'Personal blog by biss.' } = Astro.props;
const pageTitle = title === siteTitle ? siteTitle : `${title} | ${siteTitle}`;
---
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta name="description" content={description} />
<link rel="icon" href="/images/Bi.ico" />
<title>{pageTitle}</title>
</head>
<body>
<header class="site-header">
<nav class="nav">
<a class="brand" href="/">Bi's Blog</a>
<div class="nav-links">
<a href="/archives/">归档</a>
<a href="/categories/">分类</a>
<a href="/tags/">标签</a>
<a href="/about/">关于</a>
<a href="/link/">友链</a>
</div>
</nav>
</header>
<main>
<slot />
</main>
<footer class="site-footer">
<span>© {new Date().getFullYear()} Bi's Blog</span>
<a href="/rss.xml">RSS</a>
</footer>
</body>
</html>
+28
View File
@@ -0,0 +1,28 @@
type PageModule = {
frontmatter: Record<string, unknown>;
Content: unknown;
rawContent?: () => string;
};
export type SitePage = PageModule & {
title: string;
slug: string;
};
const modules = import.meta.glob<PageModule>('/source/**/index.md', { eager: true });
export function getSitePages(): SitePage[] {
return Object.entries(modules)
.filter(([path]) => {
if (path.includes('/_posts/') || path.includes('/_drafts/')) return false;
return !['/source/categories/index.md', '/source/tags/index.md'].includes(path);
})
.map(([path, module]) => {
const slug = path.replace('/source/', '').replace('/index.md', '');
return {
...module,
title: String(module.frontmatter.title ?? slug),
slug,
};
});
}
+94
View File
@@ -0,0 +1,94 @@
type MarkdownModule = {
frontmatter: Record<string, unknown>;
Content: unknown;
rawContent?: () => string;
file?: string;
};
export type BlogPost = MarkdownModule & {
title: string;
slug: string;
date: Date;
dateText: string;
tags: string[];
categories: string[];
summary: string;
href: string;
};
const modules = import.meta.glob<MarkdownModule>('/source/_posts/**/*.md', { eager: true });
function asArray(value: unknown): string[] {
if (Array.isArray(value)) return value.map(String).filter(Boolean);
if (typeof value === 'string' && value.trim()) return [value.trim()];
return [];
}
function toDate(value: unknown): Date {
if (value instanceof Date) return value;
if (typeof value === 'string' || typeof value === 'number') {
const date = new Date(value);
if (!Number.isNaN(date.valueOf())) return date;
}
return new Date(0);
}
function formatDate(date: Date): string {
return new Intl.DateTimeFormat('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
}).format(date);
}
function fallbackSlug(path: string): string {
return path
.split('/')
.pop()
?.replace(/\.md$/, '')
.toLowerCase() ?? 'post';
}
function makeSummary(module: MarkdownModule): string {
const explicit = module.frontmatter.summary ?? module.frontmatter.description;
if (typeof explicit === 'string' && explicit.trim()) return explicit.trim();
const raw = module.rawContent?.() ?? '';
return raw
.replace(/```[\s\S]*?```/g, '')
.replace(/<[^>]+>/g, '')
.replace(/[#>*_`~\-\[\]\(\)]/g, '')
.replace(/\s+/g, ' ')
.trim()
.slice(0, 120);
}
export function getAllPosts(): BlogPost[] {
return Object.entries(modules)
.map(([path, module]) => {
const date = toDate(module.frontmatter.date);
const slug = String(module.frontmatter.abbrlink ?? fallbackSlug(path));
const title = String(module.frontmatter.title ?? slug);
return {
...module,
title,
slug,
date,
dateText: formatDate(date),
tags: asArray(module.frontmatter.tags),
categories: asArray(module.frontmatter.categories),
summary: makeSummary(module),
href: `/posts/${slug}/`,
};
})
.sort((a, b) => b.date.valueOf() - a.date.valueOf());
}
export function getAllTags(): string[] {
return [...new Set(getAllPosts().flatMap((post) => post.tags))].sort((a, b) => a.localeCompare(b, 'zh-CN'));
}
export function getAllCategories(): string[] {
return [...new Set(getAllPosts().flatMap((post) => post.categories))].sort((a, b) => a.localeCompare(b, 'zh-CN'));
}
+25
View File
@@ -0,0 +1,25 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getSitePages } from '@/lib/pages';
export function getStaticPaths() {
return getSitePages().map((page) => ({
params: { slug: page.slug },
props: { page },
}));
}
const { page } = Astro.props;
const { Content } = page;
---
<BaseLayout title={page.title}>
<article class="article">
<header class="article-header">
<h1>{page.title}</h1>
</header>
<div class="prose">
<Content />
</div>
</article>
</BaseLayout>
+20
View File
@@ -0,0 +1,20 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getAllPosts } from '@/lib/posts';
const posts = getAllPosts();
---
<BaseLayout title="归档">
<section class="content-wrap narrow">
<h1>归档</h1>
<div class="archive-list">
{posts.map((post) => (
<a href={post.href}>
<time datetime={post.date.toISOString()}>{post.dateText}</time>
<span>{post.title}</span>
</a>
))}
</div>
</section>
</BaseLayout>
+1
View File
@@ -0,0 +1 @@
export { GET } from './rss.xml.js';
+24
View File
@@ -0,0 +1,24 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import PostCard from '@/components/PostCard.astro';
import { getAllCategories, getAllPosts } from '@/lib/posts';
export function getStaticPaths() {
return getAllCategories().map((category) => ({
params: { category },
props: { category },
}));
}
const { category } = Astro.props;
const posts = getAllPosts().filter((post) => post.categories.includes(category));
---
<BaseLayout title={category}>
<section class="content-wrap">
<h1>{category}</h1>
<div class="post-list">
{posts.map((post) => <PostCard post={post} />)}
</div>
</section>
</BaseLayout>
+21
View File
@@ -0,0 +1,21 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getAllCategories, getAllPosts } from '@/lib/posts';
const posts = getAllPosts();
const categories = getAllCategories();
---
<BaseLayout title="分类">
<section class="content-wrap narrow">
<h1>分类</h1>
<div class="term-grid">
{categories.map((category) => (
<a href={`/categories/${encodeURIComponent(category)}/`}>
<span>{category}</span>
<strong>{posts.filter((post) => post.categories.includes(category)).length}</strong>
</a>
))}
</div>
</section>
</BaseLayout>
+26
View File
@@ -0,0 +1,26 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import PostCard from '@/components/PostCard.astro';
import { getAllPosts } from '@/lib/posts';
const posts = getAllPosts();
---
<BaseLayout>
<section class="hero">
<div>
<p class="eyebrow">写给日常、技术和正在发生的自己</p>
<h1>Bi's Blog</h1>
<p>把折腾、学习和生活片段安静地收在这里。</p>
</div>
</section>
<section class="content-wrap">
<div class="section-heading">
<h2>最新文章</h2>
<a href="/archives/">查看全部</a>
</div>
<div class="post-list">
{posts.slice(0, 16).map((post) => <PostCard post={post} />)}
</div>
</section>
</BaseLayout>
+30
View File
@@ -0,0 +1,30 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getAllPosts } from '@/lib/posts';
export function getStaticPaths() {
return getAllPosts().map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
const { Content } = post;
---
<BaseLayout title={post.title} description={post.summary}>
<article class="article">
<header class="article-header">
<time datetime={post.date.toISOString()}>{post.dateText}</time>
<h1>{post.title}</h1>
<div class="meta-list">
{post.categories.map((category) => <a href={`/categories/${encodeURIComponent(category)}/`}>{category}</a>)}
{post.tags.map((tag) => <a href={`/tags/${encodeURIComponent(tag)}/`}>#{tag}</a>)}
</div>
</header>
<div class="prose">
<Content />
</div>
</article>
</BaseLayout>
+18
View File
@@ -0,0 +1,18 @@
import rss from '@astrojs/rss';
import { getAllPosts } from '@/lib/posts';
export function GET(context) {
const posts = getAllPosts();
return rss({
title: "Bi's Blog",
description: 'Personal blog by biss.',
site: context.site,
items: posts.map((post) => ({
title: post.title,
pubDate: post.date,
description: post.summary,
link: post.href,
})),
});
}
+24
View File
@@ -0,0 +1,24 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import PostCard from '@/components/PostCard.astro';
import { getAllPosts, getAllTags } from '@/lib/posts';
export function getStaticPaths() {
return getAllTags().map((tag) => ({
params: { tag },
props: { tag },
}));
}
const { tag } = Astro.props;
const posts = getAllPosts().filter((post) => post.tags.includes(tag));
---
<BaseLayout title={tag}>
<section class="content-wrap">
<h1>#{tag}</h1>
<div class="post-list">
{posts.map((post) => <PostCard post={post} />)}
</div>
</section>
</BaseLayout>
+21
View File
@@ -0,0 +1,21 @@
---
import BaseLayout from '@/layouts/BaseLayout.astro';
import { getAllPosts, getAllTags } from '@/lib/posts';
const posts = getAllPosts();
const tags = getAllTags();
---
<BaseLayout title="标签">
<section class="content-wrap narrow">
<h1>标签</h1>
<div class="term-grid">
{tags.map((tag) => (
<a href={`/tags/${encodeURIComponent(tag)}/`}>
<span>{tag}</span>
<strong>{posts.filter((post) => post.tags.includes(tag)).length}</strong>
</a>
))}
</div>
</section>
</BaseLayout>
+295
View File
@@ -0,0 +1,295 @@
:root {
color-scheme: light;
--bg: #f7f5ef;
--surface: #fffdf8;
--text: #20242a;
--muted: #686f78;
--line: #ded8cc;
--accent: #0f766e;
--accent-strong: #14532d;
--shadow: 0 14px 45px rgba(58, 51, 38, 0.1);
}
* {
box-sizing: border-box;
}
body {
margin: 0;
color: var(--text);
font-family:
Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
"Microsoft YaHei", sans-serif;
line-height: 1.75;
background:
linear-gradient(rgba(247, 245, 239, 0.86), rgba(247, 245, 239, 0.95)),
url('/images/background.png') center top / cover fixed;
}
a {
color: inherit;
text-decoration: none;
}
.site-header {
position: sticky;
top: 0;
z-index: 10;
border-bottom: 1px solid rgba(222, 216, 204, 0.8);
background: rgba(255, 253, 248, 0.88);
backdrop-filter: blur(14px);
}
.nav {
width: min(1120px, calc(100% - 32px));
min-height: 64px;
margin: 0 auto;
display: flex;
align-items: center;
justify-content: space-between;
gap: 24px;
}
.brand {
font-size: 1.05rem;
font-weight: 800;
}
.nav-links {
display: flex;
align-items: center;
gap: 18px;
color: var(--muted);
font-size: 0.94rem;
white-space: nowrap;
}
.nav-links a:hover,
.site-footer a:hover,
.section-heading a:hover {
color: var(--accent);
}
.hero {
width: min(1120px, calc(100% - 32px));
min-height: 360px;
margin: 0 auto;
display: grid;
align-items: center;
}
.hero h1 {
margin: 0;
font-size: clamp(3rem, 8vw, 6.5rem);
line-height: 0.95;
}
.hero p {
max-width: 620px;
margin: 18px 0 0;
color: var(--muted);
font-size: 1.12rem;
}
.eyebrow {
margin: 0 0 16px !important;
color: var(--accent-strong) !important;
font-size: 0.95rem !important;
font-weight: 700;
}
.content-wrap {
width: min(980px, calc(100% - 32px));
margin: 0 auto 72px;
}
.content-wrap.narrow {
width: min(780px, calc(100% - 32px));
}
.section-heading {
display: flex;
align-items: end;
justify-content: space-between;
gap: 20px;
margin-bottom: 18px;
}
.section-heading h2,
.content-wrap h1 {
margin: 0;
font-size: 1.8rem;
line-height: 1.2;
}
.section-heading a {
color: var(--muted);
}
.post-list {
display: grid;
gap: 16px;
}
.post-card {
border: 1px solid var(--line);
border-radius: 8px;
padding: 22px;
background: rgba(255, 253, 248, 0.88);
box-shadow: var(--shadow);
}
.post-card h2 {
margin: 6px 0 8px;
font-size: 1.35rem;
line-height: 1.35;
}
.post-card p {
margin: 0;
color: var(--muted);
}
time {
color: var(--muted);
font-size: 0.9rem;
}
.meta-list {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 14px;
}
.meta-list a {
border: 1px solid rgba(15, 118, 110, 0.28);
border-radius: 999px;
padding: 2px 10px;
color: var(--accent);
font-size: 0.86rem;
background: rgba(236, 253, 245, 0.7);
}
.article {
width: min(820px, calc(100% - 32px));
margin: 64px auto 84px;
border: 1px solid var(--line);
border-radius: 8px;
padding: clamp(24px, 5vw, 48px);
background: rgba(255, 253, 248, 0.93);
box-shadow: var(--shadow);
}
.article-header {
margin-bottom: 28px;
padding-bottom: 22px;
border-bottom: 1px solid var(--line);
}
.article-header h1 {
margin: 8px 0 0;
font-size: clamp(2rem, 6vw, 3.3rem);
line-height: 1.14;
}
.prose :where(h2, h3, h4) {
margin-top: 2em;
line-height: 1.35;
}
.prose :where(p, ul, ol, blockquote, pre) {
margin: 1.1em 0;
}
.prose a {
color: var(--accent);
text-decoration: underline;
text-underline-offset: 3px;
}
.prose img {
max-width: 100%;
border-radius: 8px;
}
.prose pre {
overflow-x: auto;
border-radius: 8px;
padding: 18px;
}
.prose code {
font-family: "JetBrains Mono", Consolas, monospace;
}
.archive-list {
margin-top: 24px;
display: grid;
gap: 10px;
}
.archive-list a {
display: grid;
grid-template-columns: 116px 1fr;
gap: 16px;
border-bottom: 1px solid var(--line);
padding: 10px 0;
}
.term-grid {
margin-top: 24px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
gap: 12px;
}
.term-grid a {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
border: 1px solid var(--line);
border-radius: 8px;
padding: 14px 16px;
background: rgba(255, 253, 248, 0.9);
}
.site-footer {
width: min(1120px, calc(100% - 32px));
margin: 0 auto;
padding: 32px 0 44px;
display: flex;
justify-content: space-between;
gap: 18px;
color: var(--muted);
}
@media (max-width: 720px) {
.nav {
min-height: auto;
padding: 14px 0;
align-items: flex-start;
flex-direction: column;
}
.nav-links {
width: 100%;
overflow-x: auto;
gap: 14px;
padding-bottom: 2px;
}
.hero {
min-height: 300px;
}
.archive-list a {
grid-template-columns: 1fr;
gap: 2px;
}
.article {
margin-top: 32px;
}
}