forked from ParadigmEducation/snowflake-notes
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:
@@ -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} />
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
```
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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/MDX;Typst 预留
|
||||
export const builtInLoaders: readonly ContentLoader[] = [
|
||||
createLoader(['.md', '.mdx']),
|
||||
createLoader(['.typ'])
|
||||
];
|
||||
|
||||
export function findLoaderByExtension(ext: string): ContentLoader | undefined {
|
||||
return builtInLoaders.find((l) => l.canLoad(ext));
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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>
|
||||
@@ -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>
|
||||
@@ -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 } }));
|
||||
};
|
||||
@@ -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>
|
||||
Reference in New Issue
Block a user