feat: 归档渲染
This commit is contained in:
@@ -2,6 +2,7 @@
|
|||||||
import { defineConfig } from 'astro/config';
|
import { defineConfig } from 'astro/config';
|
||||||
import solidJs from '@astrojs/solid-js';
|
import solidJs from '@astrojs/solid-js';
|
||||||
import node from '@astrojs/node';
|
import node from '@astrojs/node';
|
||||||
|
import tailwindcss from "@tailwindcss/vite";
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@@ -10,5 +11,8 @@ export default defineConfig({
|
|||||||
integrations: [solidJs()],
|
integrations: [solidJs()],
|
||||||
adapter: node({
|
adapter: node({
|
||||||
mode: 'standalone'
|
mode: 'standalone'
|
||||||
})
|
}),
|
||||||
|
vite: {
|
||||||
|
plugins: [tailwindcss()],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
12
package.json
12
package.json
@@ -12,11 +12,19 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@astrojs/node": "^9.5.1",
|
"@astrojs/node": "^9.5.1",
|
||||||
"@astrojs/solid-js": "^5.1.3",
|
"@astrojs/solid-js": "^5.1.3",
|
||||||
|
"@tailwindcss/vite": "^4.3.2",
|
||||||
"astro": "^5.15.8",
|
"astro": "^5.15.8",
|
||||||
"modern-normalize": "^3.0.1",
|
"github-markdown-css": "^5.9.0",
|
||||||
"solid-js": "^1.9.10"
|
"jsdom": "^29.1.1",
|
||||||
|
"ky": "^2.0.2",
|
||||||
|
"shiki": "^4.3.0",
|
||||||
|
"solid-js": "^1.9.10",
|
||||||
|
"tailwindcss": "^4.3.2"
|
||||||
},
|
},
|
||||||
"volta": {
|
"volta": {
|
||||||
"node": "22.21.1"
|
"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";
|
import Layout from "@/layouts/Layout.astro";
|
||||||
|
|
||||||
const posts = [
|
import { http } from "@/lib/request";
|
||||||
{
|
|
||||||
title: "First Post",
|
interface Post {
|
||||||
date: "2024-01-01",
|
title: string;
|
||||||
summary: "This is the summary of the first post.",
|
summary: string;
|
||||||
},
|
slug: string;
|
||||||
];
|
cover: string;
|
||||||
|
published_at: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const {list} = await http.get<ApiPageResponse<Post>>("post");
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout>
|
<Layout>
|
||||||
<div class="posts">
|
<div class="posts">
|
||||||
{
|
{
|
||||||
posts.map((post) => (
|
list.map((post) => (
|
||||||
<a
|
<a
|
||||||
class="post"
|
class="post"
|
||||||
href={`/posts/${post.title.replace(/\s+/g, "-").toLowerCase()}`}
|
href={`/posts/${post.slug}`}
|
||||||
>
|
>
|
||||||
<h2>{post.title}</h2>
|
<div class="cover">
|
||||||
<p>{post.date}</p>
|
<img src={post.cover} alt={post.title} />
|
||||||
<p class="summary">{post.summary}</p>
|
</div>
|
||||||
|
<div class="post-content">
|
||||||
|
<h2>{post.title}</h2>
|
||||||
|
<p class="summary">{post.summary}</p>
|
||||||
|
<time>{post.published_at}</time>
|
||||||
|
</div>
|
||||||
</a>
|
</a>
|
||||||
))
|
))
|
||||||
}
|
}
|
||||||
@@ -39,10 +48,30 @@ const posts = [
|
|||||||
padding: 16px;
|
padding: 16px;
|
||||||
border-radius: 12px;
|
border-radius: 12px;
|
||||||
box-shadow: rgba(149, 157, 165, 0.2) 0px 2px 14px -4px;
|
box-shadow: rgba(149, 157, 165, 0.2) 0px 2px 14px -4px;
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.post-content{
|
||||||
|
flex:1;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.summary {
|
.summary {
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
color: rgb(66, 63, 63);
|
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>
|
</style>
|
||||||
|
|||||||
@@ -1,18 +1,31 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/layouts/Layout.astro";
|
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 { slug } = Astro.params;
|
||||||
|
|
||||||
const str = `
|
interface Post {
|
||||||
<h1>Sample Post Title</h1>
|
title: string;
|
||||||
<p>This is a sample blog post content for the post with slug: </p>
|
summary: string;
|
||||||
<p>Lorem ipsum dolor sit amet, consectetur adipiscing elit. Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua.</p>
|
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>
|
<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>
|
</div>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
@import "modern-normalize";
|
@import "tailwindcss";
|
||||||
@import "./reset.css";
|
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--layout-width: 1200px;
|
--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