feat: 初始化 Astro 学习博客骨架

- Astro+TS 严格模式(禁止 any、显式返回类型)
- ESLint Flat Config + Prettier 格式化
- 内容集合(posts)与示例文章
- OSS 图床可配置 Img 组件
- 抽离 posts 数据逻辑(更易类型检查)
- getStaticPaths 修正(rest 路由传字符串)
- README/AGENTS 中文文档与约定
- Gitea CI:typecheck + lint
- 忽略 ref/(仅作临时参考)
This commit is contained in:
2025-11-27 17:08:07 +08:00
commit 6cc96a7126
24 changed files with 5381 additions and 0 deletions
+17
View File
@@ -0,0 +1,17 @@
---
export interface Props {
src: string;
alt: string;
width?: number | string;
height?: number | string;
class?: string;
}
const { src, alt, width, height, class: className } = Astro.props;
const base = import.meta.env.PUBLIC_MEDIA_BASE_URL?.replace(/\/$/, '') ?? '';
const isAbsolute = /^(https?:)?\/\//.test(src) || src.startsWith('/');
const resolved = isAbsolute ? src : `${base}/${src}`.replace(/([^:]?)\/\/+/, '$1/');
---
<img src={resolved} alt={alt} width={width} height={height} class={className} />
+14
View File
@@ -0,0 +1,14 @@
import { defineCollection, z } from 'astro:content';
const posts = defineCollection({
type: 'content',
schema: z.object({
title: z.string(),
description: z.string().optional(),
pubDate: z.date().optional(),
tags: z.array(z.string()).default([])
})
});
export const collections = { posts };
+17
View File
@@ -0,0 +1,17 @@
---
title: 你好,Astro
description: 内部学习博客的第一篇文章。
pubDate: 2024-11-27
tags: ["简介", "Astro"]
---
欢迎来到新的内部学习博客!这是第一篇文章,使用 MDX 撰写。
可以直接在文中插入 React/JSX 组件或 Astro 组件。
```ts
export function add(a: number, b: number): number {
return a + b;
}
```
+28
View File
@@ -0,0 +1,28 @@
---
interface Props {
title?: string;
}
const { title } = Astro.props;
const site = import.meta.env.PUBLIC_SITE_URL ?? '';
---
<!doctype html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>{title ? `${title} | 学习博客` : '学习博客'}</title>
<link rel="canonical" href={site} />
</head>
<body>
<header style="padding: 12px; border-bottom: 1px solid #eee">
<a href="/">首页</a>
<span style="margin: 0 8px">|</span>
<a href="/posts">文章</a>
</header>
<main style="padding: 16px">
<slot />
</main>
</body>
</html>
+20
View File
@@ -0,0 +1,20 @@
---
import BaseLayout from './BaseLayout.astro';
interface Props {
title: string;
description?: string;
pubDate?: Date;
}
const { title, description, pubDate } = Astro.props;
---
<BaseLayout title={title}>
<article>
<h1>{title}</h1>
{pubDate && <p style="color:#666;">{pubDate.toLocaleDateString('zh-CN')}</p>}
{description && <p>{description}</p>}
<slot />
</article>
</BaseLayout>
+23
View File
@@ -0,0 +1,23 @@
export type ContentFormat = 'md' | 'mdx' | 'typ';
export interface ContentLoader {
readonly extensions: readonly string[];
canLoad: (ext: string) => boolean;
// 未来扩展:根据路径或数据源加载内容并返回统一结构
// load: (path: string) => Promise<LoadedContent>;
}
export function createLoader(extensions: string[]): ContentLoader {
const canLoad = (ext: string): boolean => extensions.includes(ext);
return { extensions, canLoad } as const satisfies ContentLoader;
}
// 默认注册:Markdown/MDXTypst 预留
export const builtInLoaders: readonly ContentLoader[] = [
createLoader(['.md', '.mdx']),
createLoader(['.typ'])
];
export function findLoaderByExtension(ext: string): ContentLoader | undefined {
return builtInLoaders.find((l) => l.canLoad(ext));
}
+25
View File
@@ -0,0 +1,25 @@
import { getCollection, getEntry } from 'astro:content';
import type { CollectionEntry } from 'astro:content';
export async function listAllPosts(): Promise<CollectionEntry<'posts'>[]> {
const posts = await getCollection('posts');
// 按发布日期倒序
return posts.sort((a, b) => {
const ad = a.data.pubDate ? new Date(a.data.pubDate).getTime() : 0;
const bd = b.data.pubDate ? new Date(b.data.pubDate).getTime() : 0;
return bd - ad;
});
}
export async function listRecentPosts(limit = 5): Promise<CollectionEntry<'posts'>[]> {
const all = await listAllPosts();
return all.slice(0, limit);
}
export async function getPostBySlug(slug: string): Promise<CollectionEntry<'posts'>> {
const entry = await getEntry('posts', slug);
if (!entry) {
throw new Error(`Post not found: ${slug}`);
}
return entry;
}
+15
View File
@@ -0,0 +1,15 @@
---
import BaseLayout from '../layouts/BaseLayout.astro';
import { listRecentPosts } from '../lib/posts';
const posts = await listRecentPosts(5);
---
<BaseLayout title="首页">
<h2>最近文章</h2>
<ul>
{posts.slice(0, 5).map((p) => (
<li>
<a href={`/posts/${p.slug}/`}>{p.data.title}</a>
</li>
))}
</ul>
</BaseLayout>
+18
View File
@@ -0,0 +1,18 @@
---
import PostLayout from '../../layouts/PostLayout.astro';
import { getEntry } from 'astro:content';
import { getStaticPaths } from './getStaticPaths';
export { getStaticPaths };
const { slug } = Astro.params as { slug: string };
const entry = await getEntry('posts', slug);
if (!entry) {
throw new Error(`Post not found: ${slug}`);
}
const { Content } = await entry.render();
const data = entry.data;
---
<PostLayout title={data.title} description={data.description} pubDate={data.pubDate}>
<Content />
</PostLayout>
+8
View File
@@ -0,0 +1,8 @@
import { getCollection } from 'astro:content';
import type { GetStaticPaths, GetStaticPathsResult } from 'astro';
// 为 rest 参数 `[...slug]` 显式声明字符串类型,防止误用数组
export const getStaticPaths: GetStaticPaths = async (): Promise<GetStaticPathsResult> => {
const posts = (await getCollection('posts')) as { slug: string }[];
return posts.map((p) => ({ params: { slug: p.slug } }));
};
+15
View File
@@ -0,0 +1,15 @@
---
import BaseLayout from '../../layouts/BaseLayout.astro';
import { listAllPosts } from '../../lib/posts';
const posts = await listAllPosts();
---
<BaseLayout title="文章">
<h2>全部文章</h2>
<ul>
{posts.map((p) => (
<li>
<a href={`/posts/${p.slug}/`}>{p.data.title}</a> — {p.data.pubDate?.toLocaleDateString?.('zh-CN')}
</li>
))}
</ul>
</BaseLayout>