feat: 归档渲染
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
import { defineConfig } from 'astro/config';
|
||||
import solidJs from '@astrojs/solid-js';
|
||||
import node from '@astrojs/node';
|
||||
import tailwindcss from "@tailwindcss/vite";
|
||||
|
||||
// https://astro.build/config
|
||||
export default defineConfig({
|
||||
@@ -10,5 +11,8 @@ export default defineConfig({
|
||||
integrations: [solidJs()],
|
||||
adapter: node({
|
||||
mode: 'standalone'
|
||||
})
|
||||
}),
|
||||
vite: {
|
||||
plugins: [tailwindcss()],
|
||||
},
|
||||
});
|
||||
|
||||
12
package.json
12
package.json
@@ -12,11 +12,19 @@
|
||||
"dependencies": {
|
||||
"@astrojs/node": "^9.5.1",
|
||||
"@astrojs/solid-js": "^5.1.3",
|
||||
"@tailwindcss/vite": "^4.3.2",
|
||||
"astro": "^5.15.8",
|
||||
"modern-normalize": "^3.0.1",
|
||||
"solid-js": "^1.9.10"
|
||||
"github-markdown-css": "^5.9.0",
|
||||
"jsdom": "^29.1.1",
|
||||
"ky": "^2.0.2",
|
||||
"shiki": "^4.3.0",
|
||||
"solid-js": "^1.9.10",
|
||||
"tailwindcss": "^4.3.2"
|
||||
},
|
||||
"volta": {
|
||||
"node": "22.21.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/jsdom": "^28.0.3"
|
||||
}
|
||||
}
|
||||
|
||||
870
pnpm-lock.yaml
generated
870
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
3
pnpm-workspace.yaml
Normal file
3
pnpm-workspace.yaml
Normal file
@@ -0,0 +1,3 @@
|
||||
allowBuilds:
|
||||
esbuild: true
|
||||
sharp: true
|
||||
56
src/lib/highlight.js
Normal file
56
src/lib/highlight.js
Normal 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
12
src/lib/request.ts
Normal 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>()
|
||||
}
|
||||
};
|
||||
@@ -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}`}
|
||||
>
|
||||
<div class="cover">
|
||||
<img src={post.cover} alt={post.title} />
|
||||
</div>
|
||||
<div class="post-content">
|
||||
<h2>{post.title}</h2>
|
||||
<p>{post.date}</p>
|
||||
<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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
@import "modern-normalize";
|
||||
@import "./reset.css";
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--layout-width: 1200px;
|
||||
|
||||
@@ -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
29
src/types/index.d.ts
vendored
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user