删除了没用的库,更新sitemap,优化豆瓣服务端函数,优化const内容
This commit is contained in:
parent
a690864864
commit
5196564428
@ -5,36 +5,13 @@ import tailwindcss from "@tailwindcss/vite";
|
|||||||
import mdx from "@astrojs/mdx";
|
import mdx from "@astrojs/mdx";
|
||||||
import react from "@astrojs/react";
|
import react from "@astrojs/react";
|
||||||
import rehypeExternalLinks from "rehype-external-links";
|
import rehypeExternalLinks from "rehype-external-links";
|
||||||
import sitemap from "@astrojs/sitemap";
|
|
||||||
import fs from "node:fs";
|
|
||||||
import path from "node:path";
|
|
||||||
import { SITE_URL } from "./src/consts";
|
import { SITE_URL } from "./src/consts";
|
||||||
import compressor from "astro-compressor";
|
import compressor from "astro-compressor";
|
||||||
import vercel from "@astrojs/vercel";
|
import vercel from "@astrojs/vercel";
|
||||||
import { articleIndexerIntegration } from "./src/plugins/build-article-index.js";
|
import { articleIndexerIntegration } from "./src/plugins/build-article-index.js";
|
||||||
import { rehypeCodeBlocks } from "./src/plugins/rehype-code-blocks.js";
|
import { rehypeCodeBlocks } from "./src/plugins/rehype-code-blocks.js";
|
||||||
import { rehypeTables } from "./src/plugins/rehype-tables.js";
|
import { rehypeTables } from "./src/plugins/rehype-tables.js";
|
||||||
|
import { customSitemapIntegration } from "./src/plugins/sitemap-integration.js";
|
||||||
function getArticleDate(articleId) {
|
|
||||||
try {
|
|
||||||
// 处理多级目录的文章路径
|
|
||||||
const mdPath = path.join(process.cwd(), "src/content", articleId + ".md");
|
|
||||||
const mdxPath = path.join(process.cwd(), "src/content", articleId + ".mdx");
|
|
||||||
|
|
||||||
let filePath = fs.existsSync(mdPath) ? mdPath : mdxPath;
|
|
||||||
|
|
||||||
if (fs.existsSync(filePath)) {
|
|
||||||
const content = fs.readFileSync(filePath, "utf-8");
|
|
||||||
const match = content.match(/date:\s*(\d{4}-\d{2}-\d{2})/);
|
|
||||||
if (match) {
|
|
||||||
return new Date(match[1]).toISOString();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error("Error reading article date:", error);
|
|
||||||
}
|
|
||||||
return new Date().toISOString(); // 如果没有日期,返回当前时间
|
|
||||||
}
|
|
||||||
|
|
||||||
// https://astro.build/config
|
// https://astro.build/config
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
@ -56,44 +33,7 @@ export default defineConfig({
|
|||||||
react(),
|
react(),
|
||||||
// 使用文章索引生成器
|
// 使用文章索引生成器
|
||||||
articleIndexerIntegration(),
|
articleIndexerIntegration(),
|
||||||
sitemap({
|
customSitemapIntegration(),
|
||||||
filter: (page) => !page.includes("/api/"),
|
|
||||||
serialize(item) {
|
|
||||||
if (!item) return undefined;
|
|
||||||
|
|
||||||
// 文章页面
|
|
||||||
if (item.url.includes("/articles/")) {
|
|
||||||
// 从 URL 中提取文章 ID
|
|
||||||
const articleId = item.url
|
|
||||||
.replace(SITE_URL + "/articles/", "")
|
|
||||||
.replace(/\/$/, "");
|
|
||||||
const publishDate = getArticleDate(articleId);
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
priority: 0.8,
|
|
||||||
lastmod: publishDate,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
// 其他页面
|
|
||||||
else {
|
|
||||||
let priority = 0.7; // 默认优先级
|
|
||||||
|
|
||||||
// 首页最高优先级
|
|
||||||
if (item.url === SITE_URL + "/") {
|
|
||||||
priority = 1.0;
|
|
||||||
}
|
|
||||||
// 文章列表页次高优先级
|
|
||||||
else if (item.url === SITE_URL + "/articles/") {
|
|
||||||
priority = 0.9;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...item,
|
|
||||||
priority,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
// 添加压缩插件 (必须放在最后位置)
|
// 添加压缩插件 (必须放在最后位置)
|
||||||
compressor()
|
compressor()
|
||||||
],
|
],
|
||||||
|
411
package-lock.json
generated
411
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@ -12,11 +12,8 @@
|
|||||||
"@astrojs/mdx": "^4.2.5",
|
"@astrojs/mdx": "^4.2.5",
|
||||||
"@astrojs/node": "^9.2.1",
|
"@astrojs/node": "^9.2.1",
|
||||||
"@astrojs/react": "^4.2.5",
|
"@astrojs/react": "^4.2.5",
|
||||||
"@astrojs/sitemap": "^3.3.1",
|
|
||||||
"@astrojs/vercel": "^8.1.4",
|
"@astrojs/vercel": "^8.1.4",
|
||||||
"@astrolib/seo": "^1.0.0-beta.8",
|
"@astrolib/seo": "^1.0.0-beta.8",
|
||||||
"@expressive-code/plugin-collapsible-sections": "^0.41.2",
|
|
||||||
"@expressive-code/plugin-line-numbers": "^0.41.2",
|
|
||||||
"@mermaid-js/mermaid-cli": "^11.4.2",
|
"@mermaid-js/mermaid-cli": "^11.4.2",
|
||||||
"@swup/fragment-plugin": "^1.1.1",
|
"@swup/fragment-plugin": "^1.1.1",
|
||||||
"@swup/head-plugin": "^2.3.1",
|
"@swup/head-plugin": "^2.3.1",
|
||||||
@ -25,17 +22,13 @@
|
|||||||
"@swup/scripts-plugin": "^2.1.0",
|
"@swup/scripts-plugin": "^2.1.0",
|
||||||
"@tailwindcss/vite": "^4.1.4",
|
"@tailwindcss/vite": "^4.1.4",
|
||||||
"@types/react": "^19.1.2",
|
"@types/react": "^19.1.2",
|
||||||
"@types/react-dom": "^19.1.2",
|
|
||||||
"@types/three": "^0.176.0",
|
"@types/three": "^0.176.0",
|
||||||
"astro": "^5.7.5",
|
"astro": "^5.7.5",
|
||||||
"astro-expressive-code": "^0.41.2",
|
|
||||||
"cheerio": "^1.0.0",
|
"cheerio": "^1.0.0",
|
||||||
"mermaid": "^11.6.0",
|
"mermaid": "^11.6.0",
|
||||||
"node-fetch": "^3.3.2",
|
"node-fetch": "^3.3.2",
|
||||||
"octokit": "^4.1.3",
|
"octokit": "^4.1.3",
|
||||||
"puppeteer": "^23.11.1",
|
|
||||||
"react": "^19.1.0",
|
"react": "^19.1.0",
|
||||||
"react-dom": "^19.1.0",
|
|
||||||
"react-masonry-css": "^1.0.16",
|
"react-masonry-css": "^1.0.16",
|
||||||
"swup": "^4.8.2",
|
"swup": "^4.8.2",
|
||||||
"tailwindcss": "^4.1.4",
|
"tailwindcss": "^4.1.4",
|
||||||
|
@ -75,7 +75,7 @@ const currentYear = new Date().getFullYear();
|
|||||||
>·</span
|
>·</span
|
||||||
>
|
>
|
||||||
<a
|
<a
|
||||||
href="/sitemap-index.xml"
|
href="/sitemap.xml"
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
class="hover:text-primary-600 dark:hover:text-primary-400"
|
class="hover:text-primary-600 dark:hover:text-primary-400"
|
||||||
|
@ -1,18 +1,6 @@
|
|||||||
export const SITE_URL = 'https://blog.lsy22.com';
|
export const SITE_URL = 'https://blog.lsy22.com';
|
||||||
export const SITE_NAME = "echoes";
|
export const SITE_NAME = "echoes";
|
||||||
export const SITE_DESCRIPTION = "记录生活,分享所思";
|
export const SITE_DESCRIPTION = "记录生活,分享所思";
|
||||||
|
|
||||||
// 原始导航链接(保留用于兼容)
|
|
||||||
export const NAV_LINKS = [
|
|
||||||
{ href: '/', text: '首页' },
|
|
||||||
{ href: '/filtered', text: '筛选' },
|
|
||||||
{ href: '/articles', text: '文章' },
|
|
||||||
{ href: '/movies', text: '观影' },
|
|
||||||
{ href: '/books', text: '读书' },
|
|
||||||
{ href: '/projects', text: '项目' },
|
|
||||||
{ href: '/other', text: '其他' },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 新的导航结构 - 支持分层导航
|
// 新的导航结构 - 支持分层导航
|
||||||
export const NAV_STRUCTURE = [
|
export const NAV_STRUCTURE = [
|
||||||
{
|
{
|
||||||
@ -50,10 +38,6 @@ export const ICP = '渝ICP备2022009272号';
|
|||||||
export const PSB_ICP = '渝公网安备50011902000520号';
|
export const PSB_ICP = '渝公网安备50011902000520号';
|
||||||
export const PSB_ICP_URL = 'http://www.beian.gov.cn/portal/registerSystemInfo';
|
export const PSB_ICP_URL = 'http://www.beian.gov.cn/portal/registerSystemInfo';
|
||||||
|
|
||||||
export const VISITED_PLACES = ['中国-黑龙江', '中国-吉林', '中国-辽宁', '中国-北京', '中国-天津', '中国-广东', '中国-西藏', '中国-河北', '中国-山东', '中国-湖南', '中国-重庆', '中国-四川', "马来西亚", "印度尼西亚", "泰国"];
|
|
||||||
|
|
||||||
export const DOUBAN_ID = 'lsy22';
|
|
||||||
|
|
||||||
export const ARTICLE_EXPIRY_CONFIG = {
|
export const ARTICLE_EXPIRY_CONFIG = {
|
||||||
enabled: true, // 是否启用文章过期提醒
|
enabled: true, // 是否启用文章过期提醒
|
||||||
expiryDays: 365, // 文章过期天数
|
expiryDays: 365, // 文章过期天数
|
||||||
|
@ -64,16 +64,11 @@ export const NAV_STRUCTURE = [
|
|||||||
}
|
}
|
||||||
];
|
];
|
||||||
|
|
||||||
// 备案信息(如果需要)
|
// 备案信息
|
||||||
export const ICP = "你的ICP备案号";
|
export const ICP = "你的ICP备案号";
|
||||||
export const PSB_ICP = "你的公安备案号";
|
export const PSB_ICP = "你的公安备案号";
|
||||||
export const PSB_ICP_URL = "备案链接";
|
export const PSB_ICP_URL = "备案链接";
|
||||||
|
|
||||||
// 豆瓣配置
|
|
||||||
export const DOUBAN_ID = "你的豆瓣ID";
|
|
||||||
|
|
||||||
// 旅行足迹
|
|
||||||
export const VISITED_PLACES = ["中国-北京", "中国-上海", "美国-纽约"];
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## 文章写作
|
## 文章写作
|
||||||
@ -176,14 +171,14 @@ import MediaGrid from '@/components/MediaGrid.astro';
|
|||||||
<MediaGrid
|
<MediaGrid
|
||||||
type="movie" // 类型:movie 或 book
|
type="movie" // 类型:movie 或 book
|
||||||
title="我看过的电影" // 显示标题
|
title="我看过的电影" // 显示标题
|
||||||
doubanId={DOUBAN_ID} // 使用配置文件中的豆瓣ID
|
doubanId="id" // 豆瓣ID
|
||||||
/>
|
/>
|
||||||
|
|
||||||
// 展示读书记录
|
// 展示读书记录
|
||||||
<MediaGrid
|
<MediaGrid
|
||||||
type="book"
|
type="book"
|
||||||
title="我读过的书"
|
title="我读过的书"
|
||||||
doubanId={DOUBAN_ID}
|
doubanId="id"
|
||||||
/>
|
/>
|
||||||
```
|
```
|
||||||
|
|
||||||
|
@ -9,16 +9,6 @@ const MAX_RETRIES = 0; // 最大重试次数
|
|||||||
const RETRY_DELAY = 1500; // 重试延迟(毫秒)
|
const RETRY_DELAY = 1500; // 重试延迟(毫秒)
|
||||||
const REQUEST_TIMEOUT = 10000; // 请求超时时间(毫秒)
|
const REQUEST_TIMEOUT = 10000; // 请求超时时间(毫秒)
|
||||||
|
|
||||||
// 生成随机的bid Cookie值
|
|
||||||
function generateBid() {
|
|
||||||
const chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789';
|
|
||||||
let result = '';
|
|
||||||
for (let i = 0; i < 11; i++) {
|
|
||||||
result += chars.charAt(Math.floor(Math.random() * chars.length));
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加延迟函数
|
// 添加延迟函数
|
||||||
function delay(ms: number) {
|
function delay(ms: number) {
|
||||||
return new Promise(resolve => setTimeout(resolve, ms));
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
@ -100,36 +90,18 @@ export const GET: APIRoute = async ({ request }) => {
|
|||||||
try {
|
try {
|
||||||
let doubanUrl = '';
|
let doubanUrl = '';
|
||||||
if (type === 'book') {
|
if (type === 'book') {
|
||||||
doubanUrl = `https://book.douban.com/people/${doubanId}/collect?start=${start}&sort=time&rating=all&filter=all&mode=grid`;
|
doubanUrl = `https://book.douban.com/people/${doubanId}/collect?start=${start}`;
|
||||||
} else {
|
} else {
|
||||||
doubanUrl = `https://movie.douban.com/people/${doubanId}/collect?start=${start}&sort=time&rating=all&filter=all&mode=grid`;
|
doubanUrl = `https://movie.douban.com/people/${doubanId}/collect?start=${start}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成随机bid
|
|
||||||
const bid = generateBid();
|
|
||||||
|
|
||||||
// 随机化一些请求参数,减少被检测的风险
|
|
||||||
const userAgents = [
|
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
|
||||||
'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/605.1.15 (KHTML, like Gecko) Version/17.0 Safari/605.1.15',
|
|
||||||
'Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/133.0.0.0 Safari/537.36',
|
|
||||||
'Mozilla/5.0 (Windows NT 10.0; Win64; x64; rv:128.0) Gecko/20100101 Firefox/128.0'
|
|
||||||
];
|
|
||||||
|
|
||||||
const randomUserAgent = userAgents[Math.floor(Math.random() * userAgents.length)];
|
|
||||||
|
|
||||||
// 使用带超时的fetch发送请求
|
// 使用带超时的fetch发送请求
|
||||||
const response = await fetchWithTimeout(doubanUrl, {
|
const response = await fetchWithTimeout(doubanUrl, {
|
||||||
headers: {
|
headers: {
|
||||||
'User-Agent': randomUserAgent,
|
'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/134.0.0.0 Safari/537.36',
|
||||||
'Sec-Fetch-Site': 'none',
|
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8',
|
||||||
'Sec-Fetch-Mode': 'navigate',
|
|
||||||
'Sec-Fetch-User': '?1',
|
|
||||||
'Sec-Fetch-Dest': 'document',
|
|
||||||
'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
||||||
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
'Accept-Language': 'zh-CN,zh;q=0.9,en;q=0.8',
|
||||||
'Cache-Control': 'max-age=0',
|
'Cookie': `bid=doubanAPIClient`
|
||||||
'Cookie': `bid=${bid}`
|
|
||||||
}
|
}
|
||||||
}, REQUEST_TIMEOUT);
|
}, REQUEST_TIMEOUT);
|
||||||
|
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/components/Layout.astro";
|
import Layout from "@/components/Layout.astro";
|
||||||
import MediaGrid from "@/components/MediaGrid.tsx";
|
import MediaGrid from "@/components/MediaGrid.tsx";
|
||||||
import { DOUBAN_ID } from "@/consts";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
@ -13,7 +12,7 @@ import { DOUBAN_ID } from "@/consts";
|
|||||||
|
|
||||||
<MediaGrid
|
<MediaGrid
|
||||||
type="book"
|
type="book"
|
||||||
doubanId={DOUBAN_ID}
|
doubanId="lsy22"
|
||||||
client:load
|
client:load
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -1,7 +1,6 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/components/Layout.astro";
|
import Layout from "@/components/Layout.astro";
|
||||||
import MediaGrid from "@/components/MediaGrid.tsx";
|
import MediaGrid from "@/components/MediaGrid.tsx";
|
||||||
import { DOUBAN_ID } from "@/consts.ts";
|
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout
|
<Layout
|
||||||
@ -13,7 +12,7 @@ import { DOUBAN_ID } from "@/consts.ts";
|
|||||||
|
|
||||||
<MediaGrid
|
<MediaGrid
|
||||||
type="movie"
|
type="movie"
|
||||||
doubanId={DOUBAN_ID}
|
doubanId="lsy22"
|
||||||
client:load
|
client:load
|
||||||
/>
|
/>
|
||||||
</Layout>
|
</Layout>
|
||||||
|
@ -1,27 +1,51 @@
|
|||||||
---
|
---
|
||||||
import Layout from "@/components/Layout.astro";
|
import Layout from "@/components/Layout.astro";
|
||||||
import { Countdown } from "@/components/Countdown";
|
import { Countdown } from "@/components/Countdown";
|
||||||
import WorldHeatmap from '@/components/WorldHeatmap';
|
import WorldHeatmap from "@/components/WorldHeatmap";
|
||||||
import { VISITED_PLACES } from '@/consts';
|
const VISITED_PLACES = [
|
||||||
|
"中国-黑龙江",
|
||||||
|
"中国-吉林",
|
||||||
|
"中国-辽宁",
|
||||||
|
"中国-北京",
|
||||||
|
"中国-天津",
|
||||||
|
"中国-广东",
|
||||||
|
"中国-西藏",
|
||||||
|
"中国-河北",
|
||||||
|
"中国-山东",
|
||||||
|
"中国-湖南",
|
||||||
|
"中国-重庆",
|
||||||
|
"中国-四川",
|
||||||
|
"马来西亚",
|
||||||
|
"印度尼西亚",
|
||||||
|
"泰国",
|
||||||
|
];
|
||||||
---
|
---
|
||||||
|
|
||||||
<Layout title="其他">
|
<Layout title="其他">
|
||||||
<div class="container mx-auto px-4 py-8">
|
<div class="container mx-auto px-4 py-8">
|
||||||
<section class="mb-16">
|
<section class="mb-16">
|
||||||
<h2 class="text-3xl font-semibold text-center mb-6">距离退休还有</h2>
|
<h2 class="text-3xl font-semibold text-center mb-6">距离退休还有</h2>
|
||||||
<div class="max-w-3xl mx-auto bg-white dark:bg-gray-800 rounded-xl shadow-lg p-8 hover:shadow-xl">
|
<div
|
||||||
<Countdown client:load targetDate={"2070-04-06"} />
|
class="max-w-3xl mx-auto bg-white dark:bg-gray-800 rounded-xl shadow-lg p-8 hover:shadow-xl"
|
||||||
</div>
|
>
|
||||||
</section>
|
<Countdown
|
||||||
|
client:load
|
||||||
<section class="mb-16">
|
targetDate={"2070-04-06"}
|
||||||
<h2 class="text-3xl font-semibold text-center mb-6">我的旅行足迹</h2>
|
/>
|
||||||
<div class="mx-auto bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl">
|
</div>
|
||||||
<WorldHeatmap
|
</section>
|
||||||
client:only="react"
|
|
||||||
visitedPlaces={VISITED_PLACES}
|
<section class="mb-16">
|
||||||
/>
|
<h2 class="text-3xl font-semibold text-center mb-6">我的旅行足迹</h2>
|
||||||
</div>
|
<div
|
||||||
</section>
|
class="mx-auto bg-white dark:bg-gray-800 rounded-xl shadow-lg p-6 hover:shadow-xl"
|
||||||
</div>
|
>
|
||||||
</Layout>
|
<WorldHeatmap
|
||||||
|
client:only="react"
|
||||||
|
visitedPlaces={VISITED_PLACES}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
</Layout>
|
||||||
|
|
||||||
|
515
src/plugins/sitemap-integration.js
Normal file
515
src/plugins/sitemap-integration.js
Normal file
@ -0,0 +1,515 @@
|
|||||||
|
// 自定义 Sitemap 集成
|
||||||
|
// 用于生成带 XSLT 样式表的 sitemap.xml
|
||||||
|
|
||||||
|
import fs from 'node:fs/promises';
|
||||||
|
import { readFileSync, existsSync } from 'node:fs';
|
||||||
|
import path from 'node:path';
|
||||||
|
import { SITE_URL } from '../consts';
|
||||||
|
|
||||||
|
// 转义XML特殊字符
|
||||||
|
function escapeXml(unsafe) {
|
||||||
|
if (!unsafe) return '';
|
||||||
|
return unsafe.toString().replace(/[<>&'"]/g, c => {
|
||||||
|
switch (c) {
|
||||||
|
case '<': return '<';
|
||||||
|
case '>': return '>';
|
||||||
|
case '&': return '&';
|
||||||
|
case '\'': return ''';
|
||||||
|
case '"': return '"';
|
||||||
|
default: return c;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成带XSLT的XML
|
||||||
|
function generateXmlWithXslt(entries) {
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<?xml-stylesheet type="text/xsl" href="/sitemap.xsl"?>
|
||||||
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9"
|
||||||
|
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||||
|
xsi:schemaLocation="http://www.sitemaps.org/schemas/sitemap/0.9
|
||||||
|
http://www.sitemaps.org/schemas/sitemap/0.9/sitemap.xsd">
|
||||||
|
${entries.map(entry => ` <url>
|
||||||
|
<loc>${entry.url}</loc>
|
||||||
|
<priority>${entry.priority}</priority>
|
||||||
|
</url>`).join('\n')}
|
||||||
|
</urlset>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成XSLT样式表
|
||||||
|
function generateXsltStylesheet(entries) {
|
||||||
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
||||||
|
<xsl:stylesheet version="1.0"
|
||||||
|
xmlns:xsl="http://www.w3.org/1999/XSL/Transform"
|
||||||
|
xmlns:sitemap="http://www.sitemaps.org/schemas/sitemap/0.9">
|
||||||
|
|
||||||
|
<xsl:output method="html" encoding="UTF-8" indent="yes" />
|
||||||
|
|
||||||
|
<xsl:template match="/">
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<title>网站地图</title>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
|
||||||
|
<style>
|
||||||
|
body {
|
||||||
|
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #333;
|
||||||
|
max-width: 1200px;
|
||||||
|
margin: 0 auto;
|
||||||
|
padding: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (prefers-color-scheme: dark) {
|
||||||
|
body {
|
||||||
|
background-color: #1a1a1a;
|
||||||
|
color: #f0f0f0;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #4da6ff;
|
||||||
|
}
|
||||||
|
a:visited {
|
||||||
|
color: #c58fff;
|
||||||
|
}
|
||||||
|
.table {
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
.table th, .table td {
|
||||||
|
border-color: #444;
|
||||||
|
}
|
||||||
|
.table thead th {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
}
|
||||||
|
.table tbody tr:nth-child(odd) {
|
||||||
|
background-color: #252525;
|
||||||
|
}
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background-color: #303030;
|
||||||
|
}
|
||||||
|
.stat-box {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
}
|
||||||
|
.copy-btn {
|
||||||
|
background-color: #2a2a2a;
|
||||||
|
color: #f0f0f0;
|
||||||
|
border: 1px solid #444;
|
||||||
|
}
|
||||||
|
.copy-btn:hover {
|
||||||
|
background-color: #3a3a3a;
|
||||||
|
}
|
||||||
|
.copy-btn:active {
|
||||||
|
background-color: #4a4a4a;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-title {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
h1 {
|
||||||
|
margin-top: 0;
|
||||||
|
font-size: 2.2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.meta {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table {
|
||||||
|
width: 100%;
|
||||||
|
border-collapse: collapse;
|
||||||
|
margin-bottom: 30px;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table th, .table td {
|
||||||
|
padding: 10px 15px;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
text-align: left;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table thead th {
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:nth-child(odd) {
|
||||||
|
background-color: #f9f9f9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.table tbody tr:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.url {
|
||||||
|
word-break: break-all;
|
||||||
|
width: 80%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.priority {
|
||||||
|
width: 20%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stats {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box {
|
||||||
|
padding: 15px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border-radius: 5px;
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box h3 {
|
||||||
|
margin-top: 0;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 1rem;
|
||||||
|
color: #666;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-box p {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.5rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer {
|
||||||
|
margin-top: 30px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
color: #666;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.table {
|
||||||
|
display: block;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.page-header {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn {
|
||||||
|
padding: 8px 16px;
|
||||||
|
background-color: #f5f5f5;
|
||||||
|
border: 1px solid #ddd;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background-color 0.2s, transform 0.1s;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:hover {
|
||||||
|
background-color: #e5e5e5;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn:active {
|
||||||
|
background-color: #d5d5d5;
|
||||||
|
transform: translateY(1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-btn.success {
|
||||||
|
background-color: #4caf50;
|
||||||
|
color: white;
|
||||||
|
border-color: #4caf50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.copy-icon {
|
||||||
|
width: 14px;
|
||||||
|
height: 14px;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
<script>
|
||||||
|
<![CDATA[
|
||||||
|
// 页面加载完成后执行
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const copyBtn = document.getElementById('copy-urls-btn');
|
||||||
|
const urls = [];
|
||||||
|
|
||||||
|
// 收集所有URL
|
||||||
|
document.querySelectorAll('#sitemap-table tbody a').forEach(function(link) {
|
||||||
|
urls.push(link.textContent.trim());
|
||||||
|
});
|
||||||
|
|
||||||
|
if (copyBtn && urls.length > 0) {
|
||||||
|
copyBtn.addEventListener('click', function() {
|
||||||
|
// 使用现代的Clipboard API复制内容
|
||||||
|
navigator.clipboard.writeText(urls.join('\\n'))
|
||||||
|
.then(function() {
|
||||||
|
// 复制成功
|
||||||
|
copyBtn.innerHTML = '<svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M5 13l4 4L19 7"></path></svg> 复制成功!';
|
||||||
|
copyBtn.classList.add('success');
|
||||||
|
|
||||||
|
// 3秒后恢复按钮状态
|
||||||
|
setTimeout(function() {
|
||||||
|
copyBtn.innerHTML = '<svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path></svg> 复制所有URL';
|
||||||
|
copyBtn.classList.remove('success');
|
||||||
|
}, 3000);
|
||||||
|
})
|
||||||
|
.catch(function(err) {
|
||||||
|
// 复制失败
|
||||||
|
console.error('复制失败:', err);
|
||||||
|
copyBtn.textContent = '复制失败';
|
||||||
|
|
||||||
|
// 3秒后恢复按钮状态
|
||||||
|
setTimeout(function() {
|
||||||
|
copyBtn.innerHTML = '<svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"><path d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path></svg> 复制所有URL';
|
||||||
|
}, 3000);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
]]>
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="page-header">
|
||||||
|
<h1 class="page-title">网站地图</h1>
|
||||||
|
<button id="copy-urls-btn" class="copy-btn">
|
||||||
|
<svg class="copy-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
|
||||||
|
<path d="M8 5H6a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2v-1M8 5a2 2 0 002 2h2a2 2 0 002-2M8 5a2 2 0 012-2h2a2 2 0 012 2m0 0h2a2 2 0 012 2v3m2 4H10m0 0l3-3m-3 3l3 3"></path>
|
||||||
|
</svg>
|
||||||
|
复制所有URL
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="meta">
|
||||||
|
<p>此网站地图包含 <xsl:value-of select="count(sitemap:urlset/sitemap:url)" /> 个 URL</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="stats">
|
||||||
|
<div class="stat-box">
|
||||||
|
<h3>总页面数</h3>
|
||||||
|
<p><xsl:value-of select="count(sitemap:urlset/sitemap:url)" /></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<table id="sitemap-table" class="table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="url">URL</th>
|
||||||
|
<th class="priority">优先级</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<xsl:for-each select="sitemap:urlset/sitemap:url">
|
||||||
|
<xsl:sort select="sitemap:priority" order="descending" />
|
||||||
|
<xsl:variable name="url" select="sitemap:loc/text()" />
|
||||||
|
<xsl:variable name="decodedUrl">
|
||||||
|
<xsl:call-template name="decode-url">
|
||||||
|
<xsl:with-param name="url" select="$url" />
|
||||||
|
</xsl:call-template>
|
||||||
|
</xsl:variable>
|
||||||
|
<tr>
|
||||||
|
<td class="url">
|
||||||
|
<a href="{$url}">
|
||||||
|
<xsl:value-of select="$decodedUrl" />
|
||||||
|
</a>
|
||||||
|
</td>
|
||||||
|
<td class="priority">
|
||||||
|
<xsl:value-of select="sitemap:priority" />
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
</xsl:for-each>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<div class="footer">
|
||||||
|
<p>此网站地图使用 XSLT 样式表生成,既适合搜索引擎索引,又方便人类阅读</p>
|
||||||
|
</div>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!-- 自定义URL解码模板 -->
|
||||||
|
<xsl:template name="decode-url">
|
||||||
|
<xsl:param name="url" />
|
||||||
|
<!-- 直接输出预先解码的URL映射 -->
|
||||||
|
<xsl:variable name="urlKey" select="$url" />
|
||||||
|
<xsl:choose>
|
||||||
|
<xsl:when test="document('')/*/xsl:variable[@name='url-mapping']/url[@key=$urlKey]">
|
||||||
|
<xsl:value-of select="document('')/*/xsl:variable[@name='url-mapping']/url[@key=$urlKey]/@decoded" />
|
||||||
|
</xsl:when>
|
||||||
|
<xsl:otherwise>
|
||||||
|
<xsl:value-of select="$url" />
|
||||||
|
</xsl:otherwise>
|
||||||
|
</xsl:choose>
|
||||||
|
</xsl:template>
|
||||||
|
|
||||||
|
<!-- URL映射变量 -->
|
||||||
|
<xsl:variable name="url-mapping">
|
||||||
|
${entries.map(entry => `<url key="${escapeXml(entry.url)}" decoded="${escapeXml(entry.decodedUrl)}" />`).join('\n ')}
|
||||||
|
</xsl:variable>
|
||||||
|
</xsl:stylesheet>`;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 主集成函数
|
||||||
|
export function customSitemapIntegration() {
|
||||||
|
return {
|
||||||
|
name: 'custom-sitemap-integration',
|
||||||
|
hooks: {
|
||||||
|
// 开发服务器钩子 - 为开发模式添加虚拟API路由
|
||||||
|
'astro:server:setup': ({ server }) => {
|
||||||
|
// 为 sitemap 相关文件提供虚拟路由
|
||||||
|
server.middlewares.use((req, res, next) => {
|
||||||
|
// 检查请求路径是否是 sitemap 相关文件
|
||||||
|
if (req.url === '/sitemap.xml' && req.method === 'GET') {
|
||||||
|
console.log(`虚拟路由请求: ${req.url}`);
|
||||||
|
|
||||||
|
// 尝试返回已构建好的sitemap.xml文件
|
||||||
|
const distPath = path.join(process.cwd(), 'dist/client/sitemap.xml');
|
||||||
|
|
||||||
|
if (existsSync(distPath)) {
|
||||||
|
try {
|
||||||
|
const xmlContent = readFileSync(distPath, 'utf-8');
|
||||||
|
res.setHeader('Content-Type', 'application/xml');
|
||||||
|
res.end(xmlContent);
|
||||||
|
} catch (error) {
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end('读取 sitemap.xml 文件时出错');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
res.statusCode = 404;
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (req.url === '/sitemap.xsl' && req.method === 'GET') {
|
||||||
|
console.log(`虚拟路由请求: ${req.url}`);
|
||||||
|
|
||||||
|
// 尝试返回已构建好的sitemap.xsl文件
|
||||||
|
const distPath = path.join(process.cwd(), 'dist/client/sitemap.xsl');
|
||||||
|
|
||||||
|
if (existsSync(distPath)) {
|
||||||
|
try {
|
||||||
|
const xslContent = readFileSync(distPath, 'utf-8');
|
||||||
|
res.setHeader('Content-Type', 'application/xslt+xml');
|
||||||
|
res.end(xslContent);
|
||||||
|
console.log('已返回构建好的 sitemap.xsl 文件');
|
||||||
|
} catch (error) {
|
||||||
|
console.error('读取 sitemap.xsl 文件时出错:', error);
|
||||||
|
res.statusCode = 500;
|
||||||
|
res.end('读取 sitemap.xsl 文件时出错');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
console.log('未找到构建好的 sitemap.xsl 文件,请先运行 npm run build');
|
||||||
|
res.statusCode = 404;
|
||||||
|
res.end('未找到 sitemap.xsl 文件,请先运行 npm run build');
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 不是 sitemap 相关请求,继续下一个中间件
|
||||||
|
next();
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
// 构建完成钩子 - 生成 sitemap 文件
|
||||||
|
'astro:build:done': async ({ pages, dir }) => {
|
||||||
|
console.log('生成自定义 Sitemap...');
|
||||||
|
|
||||||
|
try {
|
||||||
|
// 获取构建目录路径
|
||||||
|
let buildDirPath;
|
||||||
|
|
||||||
|
// 直接处理URL对象
|
||||||
|
if (dir instanceof URL) {
|
||||||
|
buildDirPath = dir.pathname;
|
||||||
|
// Windows路径修复
|
||||||
|
if (process.platform === 'win32' && buildDirPath.startsWith('/') && /^\/[A-Z]:/i.test(buildDirPath)) {
|
||||||
|
buildDirPath = buildDirPath.substring(1);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
buildDirPath = String(dir);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log(`构建目录路径: ${buildDirPath}`);
|
||||||
|
|
||||||
|
// 收集所有页面信息
|
||||||
|
const sitemapEntries = [];
|
||||||
|
|
||||||
|
for (const page of pages) {
|
||||||
|
// 过滤掉API路径
|
||||||
|
if (page.pathname.includes('/api/')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const url = new URL(page.pathname, SITE_URL).toString();
|
||||||
|
|
||||||
|
// 解码URL
|
||||||
|
const urlObj = new URL(url);
|
||||||
|
const decodedPathname = decodeURIComponent(urlObj.pathname);
|
||||||
|
const decodedUrl = `${urlObj.protocol}//${urlObj.host}${decodedPathname}`;
|
||||||
|
|
||||||
|
// 确定页面优先级
|
||||||
|
let priority = 0.7;
|
||||||
|
|
||||||
|
// 首页最高优先级
|
||||||
|
if (page.pathname === '/') {
|
||||||
|
priority = 1.0;
|
||||||
|
}
|
||||||
|
// 文章列表页次高优先级
|
||||||
|
else if (page.pathname === '/articles/') {
|
||||||
|
priority = 0.9;
|
||||||
|
}
|
||||||
|
// 文章页面
|
||||||
|
else if (page.pathname.includes('/articles/')) {
|
||||||
|
priority = 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
sitemapEntries.push({
|
||||||
|
url,
|
||||||
|
decodedUrl,
|
||||||
|
priority
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按优先级排序
|
||||||
|
sitemapEntries.sort((a, b) => b.priority - a.priority);
|
||||||
|
|
||||||
|
// 生成带XSLT的XML文件
|
||||||
|
const xmlContent = generateXmlWithXslt(sitemapEntries);
|
||||||
|
|
||||||
|
// 写入sitemap.xml
|
||||||
|
await fs.writeFile(path.join(buildDirPath, 'sitemap.xml'), xmlContent);
|
||||||
|
console.log('已生成 sitemap.xml');
|
||||||
|
|
||||||
|
// 写入XSLT样式表文件
|
||||||
|
await fs.writeFile(path.join(buildDirPath, 'sitemap.xsl'), generateXsltStylesheet(sitemapEntries));
|
||||||
|
console.log('已生成 sitemap.xsl');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
console.error('生成 Sitemap 时出错:', error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user