feat: 归档渲染

This commit is contained in:
2026-07-05 22:33:50 +08:00
parent 4c20ee73d3
commit e67829562d
11 changed files with 1018 additions and 62 deletions

56
src/lib/highlight.js Normal file
View File

@@ -0,0 +1,56 @@
import {JSDOM} from 'jsdom'
import {createHighlighter} from 'shiki'
const highlighter = await createHighlighter({
themes: ['github-dark-dimmed'],
langs: ['javascript', 'typescript', 'css', 'tsx', 'less', 'scss', 'text'],
})
const resolveLang = (lang) => {
const supported = highlighter.getLoadedLanguages?.() || []
if (!lang) return 'text'
if (supported.includes(lang)) return lang
return 'text'
}
export const highlightHtmlWithJsdom = async (rawHtml) => {
const dom = new JSDOM(rawHtml)
const document = dom.window.document
const codeElements = document.querySelectorAll('pre code')
for (const codeEl of codeElements) {
const preEl = codeEl.parentElement
if (!preEl || preEl.tagName !== 'PRE') continue
const codeContent = codeEl.textContent?.trim() || ''
// 提取语言
let lang = 'text'
for (const cls of codeEl.classList) {
if (cls.startsWith('language-')) {
lang = cls.slice('language-'.length)
break
}
}
lang = resolveLang(lang)
// 生成高亮 HTML
const highlighted = highlighter.codeToHtml(codeContent, {
lang,
theme: 'github-dark-dimmed',
})
// 替换 DOM
const wrapper = document.createElement('div')
wrapper.innerHTML = highlighted.trim()
const newNode = wrapper.firstChild
if (newNode && preEl.parentNode) {
preEl.parentNode.replaceChild(newNode, preEl)
}
}
return dom.serialize()
}

12
src/lib/request.ts Normal file
View File

@@ -0,0 +1,12 @@
import ky, {type Options} from 'ky'
const api = ky.create({
baseUrl: import.meta.env.VITE_API_BASE_URL
});
// ApiPageResponse<Post[]>
export const http = {
get: async <T = unknown>(url: string, options?: Options): Promise<T> => {
return api.get(url, options).json<T>()
}
};

View File

@@ -1,26 +1,35 @@
---
import Layout from "@/layouts/Layout.astro";
const posts = [
{
title: "First Post",
date: "2024-01-01",
summary: "This is the summary of the first post.",
},
];
import { http } from "@/lib/request";
interface Post {
title: string;
summary: string;
slug: string;
cover: string;
published_at: string;
}
const {list} = await http.get<ApiPageResponse<Post>>("post");
---
<Layout>
<div class="posts">
{
posts.map((post) => (
list.map((post) => (
<a
class="post"
href={`/posts/${post.title.replace(/\s+/g, "-").toLowerCase()}`}
href={`/posts/${post.slug}`}
>
<h2>{post.title}</h2>
<p>{post.date}</p>
<p class="summary">{post.summary}</p>
<div class="cover">
<img src={post.cover} alt={post.title} />
</div>
<div class="post-content">
<h2>{post.title}</h2>
<p class="summary">{post.summary}</p>
<time>{post.published_at}</time>
</div>
</a>
))
}
@@ -39,10 +48,30 @@ const posts = [
padding: 16px;
border-radius: 12px;
box-shadow: rgba(149, 157, 165, 0.2) 0px 2px 14px -4px;
display: flex;
gap: 20px;
}
.post-content{
flex:1;
overflow: hidden;
}
.summary {
font-size: 16px;
color: rgb(66, 63, 63);
}
.cover{
width: 250px;
height: 150px;
border-radius: 8px;
overflow: hidden;
}
.cover img {
width: 100%;
height: 100%;
object-fit: cover;
}
</style>

View File

@@ -1,18 +1,31 @@
---
import Layout from "@/layouts/Layout.astro";
import {http} from "@/lib/request.ts";
import {highlightHtmlWithJsdom} from "@/lib/highlight.js";
import 'github-markdown-css/github-markdown-light.css';
const { slug } = Astro.params;
const str = `
<h1>Sample Post Title</h1>
<p>This is a sample blog post content for the post with slug: </p>
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
`;
interface Post {
title: string;
summary: string;
slug: string;
cover: string;
published_at: string;
content_html: string;
}
const {data} = await http.get<ApiResponse<Post>>(`post/${slug}`);
const content = await highlightHtmlWithJsdom(data.content_html)
---
<Layout>
<div class="post" set:html={str}>
<h1>{data.title}</h1>
<div>{data.published_at}</div>
<div class="post markdown-body" set:html={content}>
</div>
</Layout>

View File

@@ -1,5 +1,4 @@
@import "modern-normalize";
@import "./reset.css";
@import "tailwindcss";
:root {
--layout-width: 1200px;

View File

@@ -1,11 +0,0 @@
/* css reset */
a {
text-decoration: none;
color: inherit;
}
ul, ol {
list-style: none;
padding: 0;
margin: 0;
}

29
src/types/index.d.ts vendored Normal file
View File

@@ -0,0 +1,29 @@
/** 基础 API 响应结构 */
declare interface ApiResponse<T = unknown> {
msg?: string
data: T
}
/** 分页响应结构 */
declare interface ApiPageResponse<T = unknown> {
msg?: string
/** 当前页码 */
page: number
/** 每页数量 */
pageSize: number
/** 业务数据列表 */
list: T[]
/** 总记录数 */
total: number
}
/** 错误响应结构 - 当请求失败时的响应格式 */
declare interface ApiErrorResponse {
msg: string
}
/** 通用请求参数类型 */
declare interface Pagination {
page?: number
pageSize?: number
}