0520发布文章
This commit is contained in:
1 parent
347d52d4b8
commit
c8cdf9a7bc
90 files changed
+11923
-54
No files matched your search
@@ -0,0 +1,259 @@
|
||||
import fs from "fs";
|
||||
import path from "path";
|
||||
import matter from "gray-matter";
|
||||
import { CONTENT_DIR, ITEMS_PER_PAGE } from "./config";
|
||||
|
||||
export interface PostFrontMatter {
|
||||
pid?: number;
|
||||
title: string;
|
||||
date: string;
|
||||
slug: string;
|
||||
draft: boolean;
|
||||
categories?: string[];
|
||||
tags?: string[];
|
||||
author?: string;
|
||||
layout?: string;
|
||||
ai_comment?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export interface Post {
|
||||
slug: string;
|
||||
frontMatter: PostFrontMatter;
|
||||
content: string;
|
||||
filePath: string;
|
||||
dirPath: string;
|
||||
}
|
||||
|
||||
export interface PostListItem {
|
||||
slug: string;
|
||||
title: string;
|
||||
date: string;
|
||||
draft: boolean;
|
||||
categories: string[];
|
||||
tags: string[];
|
||||
author: string;
|
||||
}
|
||||
|
||||
function parseDate(dateStr: string): Date {
|
||||
const d = new Date(dateStr);
|
||||
return isNaN(d.getTime()) ? new Date(0) : d;
|
||||
}
|
||||
|
||||
function findPostDirectories(): string[] {
|
||||
const result: string[] = [];
|
||||
if (!fs.existsSync(CONTENT_DIR)) return result;
|
||||
|
||||
const years = fs.readdirSync(CONTENT_DIR).filter((f) => {
|
||||
const full = path.join(CONTENT_DIR, f);
|
||||
return fs.statSync(full).isDirectory() && /^\d{4}$/.test(f);
|
||||
});
|
||||
|
||||
for (const year of years) {
|
||||
const yearDir = path.join(CONTENT_DIR, year);
|
||||
const posts = fs.readdirSync(yearDir).filter((f) => {
|
||||
return fs.statSync(path.join(yearDir, f)).isDirectory();
|
||||
});
|
||||
for (const post of posts) {
|
||||
result.push(path.join(yearDir, post));
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
function slugFromDirName(dirName: string): string {
|
||||
// e.g. "2026-01-21-how-to-implement-friend-circle-in-hugo" -> "how-to-implement-friend-circle-in-hugo"
|
||||
const match = dirName.match(/^\d{4}-\d{2}-\d{2}-(.+)$/);
|
||||
return match ? match[1] : dirName;
|
||||
}
|
||||
|
||||
function yearFromDirName(dirName: string): string {
|
||||
const match = dirName.match(/^(\d{4})/);
|
||||
return match ? match[1] : "";
|
||||
}
|
||||
|
||||
export function listPosts(options?: {
|
||||
search?: string;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
includeDrafts?: boolean;
|
||||
}): { posts: PostListItem[]; total: number; totalPages: number } {
|
||||
const { search = "", page = 1, perPage = ITEMS_PER_PAGE, includeDrafts = true } = options || {};
|
||||
const dirs = findPostDirectories();
|
||||
|
||||
const allPosts: PostListItem[] = [];
|
||||
|
||||
for (const dirPath of dirs) {
|
||||
const indexPath = path.join(dirPath, "index.md");
|
||||
if (!fs.existsSync(indexPath)) continue;
|
||||
|
||||
const raw = fs.readFileSync(indexPath, "utf-8");
|
||||
const { data } = matter(raw);
|
||||
const dirName = path.basename(dirPath);
|
||||
|
||||
const post: PostListItem = {
|
||||
slug: String(data.slug || slugFromDirName(dirName)),
|
||||
title: data.title || "Untitled",
|
||||
date: data.date ? new Date(data.date).toISOString() : "",
|
||||
draft: data.draft === true,
|
||||
categories: Array.isArray(data.categories) ? data.categories : [],
|
||||
tags: Array.isArray(data.tags) ? data.tags : [],
|
||||
author: data.author || "",
|
||||
};
|
||||
|
||||
if (!includeDrafts && post.draft) continue;
|
||||
|
||||
if (search) {
|
||||
const q = search.toLowerCase();
|
||||
const match =
|
||||
post.title.toLowerCase().includes(q) ||
|
||||
post.slug.toLowerCase().includes(q) ||
|
||||
post.categories.some((c) => c.toLowerCase().includes(q)) ||
|
||||
post.tags.some((t) => t.toLowerCase().includes(q));
|
||||
if (!match) continue;
|
||||
}
|
||||
|
||||
allPosts.push(post);
|
||||
}
|
||||
|
||||
// Sort by date descending
|
||||
allPosts.sort((a, b) => parseDate(b.date).getTime() - parseDate(a.date).getTime());
|
||||
|
||||
const total = allPosts.length;
|
||||
const totalPages = Math.ceil(total / perPage);
|
||||
const start = (page - 1) * perPage;
|
||||
const posts = allPosts.slice(start, start + perPage);
|
||||
|
||||
return { posts, total, totalPages };
|
||||
}
|
||||
|
||||
export function getPost(slug: string): Post | null {
|
||||
const dirs = findPostDirectories();
|
||||
|
||||
for (const dirPath of dirs) {
|
||||
const indexPath = path.join(dirPath, "index.md");
|
||||
if (!fs.existsSync(indexPath)) continue;
|
||||
|
||||
const raw = fs.readFileSync(indexPath, "utf-8");
|
||||
const { data, content } = matter(raw);
|
||||
const existingSlug = String(data.slug || slugFromDirName(path.basename(dirPath)));
|
||||
|
||||
if (existingSlug === slug) {
|
||||
return {
|
||||
slug: existingSlug,
|
||||
frontMatter: data as PostFrontMatter,
|
||||
content,
|
||||
filePath: indexPath,
|
||||
dirPath,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export function createPost(frontMatter: PostFrontMatter, content: string): string {
|
||||
const date = new Date(frontMatter.date || new Date());
|
||||
const year = date.getFullYear().toString();
|
||||
const dateStr = date.toISOString().slice(0, 10);
|
||||
// Folder name: date + sanitized title (Chinese-friendly)
|
||||
const titlePart = (frontMatter.title || "untitled")
|
||||
.replace(/[^\w一-鿿㐀-䶿]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "")
|
||||
.toLowerCase() || "post";
|
||||
const dirName = `${dateStr}-${titlePart}`;
|
||||
const dirPath = path.join(CONTENT_DIR, year, dirName);
|
||||
|
||||
if (fs.existsSync(dirPath)) {
|
||||
throw new Error(`Post already exists at ${dirPath}`);
|
||||
}
|
||||
|
||||
fs.mkdirSync(dirPath, { recursive: true });
|
||||
|
||||
const fm: Record<string, unknown> = {
|
||||
title: frontMatter.title,
|
||||
date: frontMatter.date,
|
||||
slug: frontMatter.slug,
|
||||
draft: frontMatter.draft ?? false,
|
||||
categories: frontMatter.categories || [],
|
||||
tags: frontMatter.tags || [],
|
||||
author: frontMatter.author || "",
|
||||
layout: frontMatter.layout || "post",
|
||||
};
|
||||
|
||||
if (frontMatter.ai_comment) {
|
||||
fm.ai_comment = frontMatter.ai_comment;
|
||||
}
|
||||
|
||||
const fileContent = matter.stringify(content, fm);
|
||||
const indexPath = path.join(dirPath, "index.md");
|
||||
fs.writeFileSync(indexPath, fileContent, "utf-8");
|
||||
|
||||
return dirPath;
|
||||
}
|
||||
|
||||
export function updatePost(
|
||||
slug: string,
|
||||
frontMatter: Partial<PostFrontMatter>,
|
||||
content?: string
|
||||
): { success: true; dirPath: string } | { success: false } {
|
||||
const post = getPost(slug);
|
||||
if (!post) return { success: false };
|
||||
|
||||
const merged = { ...post.frontMatter, ...frontMatter };
|
||||
|
||||
// If slug changed, we need to rename the directory
|
||||
if (frontMatter.slug && frontMatter.slug !== post.frontMatter.slug) {
|
||||
const newDate = new Date(merged.date || post.frontMatter.date);
|
||||
const newYear = newDate.getFullYear().toString();
|
||||
const newDateStr = newDate.toISOString().slice(0, 10);
|
||||
const newDirName = `${newDateStr}-${frontMatter.slug}`;
|
||||
const newDirPath = path.join(CONTENT_DIR, newYear, newDirName);
|
||||
|
||||
if (fs.existsSync(newDirPath) && newDirPath !== post.dirPath) {
|
||||
throw new Error(`Target path already exists: ${newDirPath}`);
|
||||
}
|
||||
|
||||
fs.mkdirSync(path.dirname(newDirPath), { recursive: true });
|
||||
fs.renameSync(post.dirPath, newDirPath);
|
||||
|
||||
const fm: Record<string, unknown> = {
|
||||
title: merged.title,
|
||||
date: merged.date,
|
||||
slug: frontMatter.slug,
|
||||
draft: merged.draft ?? false,
|
||||
categories: merged.categories || [],
|
||||
tags: merged.tags || [],
|
||||
author: merged.author || "",
|
||||
layout: merged.layout || "post",
|
||||
};
|
||||
if (merged.ai_comment) fm.ai_comment = merged.ai_comment;
|
||||
|
||||
const fileContent = matter.stringify(content ?? post.content, fm);
|
||||
fs.writeFileSync(path.join(newDirPath, "index.md"), fileContent, "utf-8");
|
||||
return { success: true, dirPath: newDirPath };
|
||||
}
|
||||
|
||||
// Update in place
|
||||
const fm: Record<string, unknown> = {
|
||||
title: merged.title,
|
||||
date: merged.date,
|
||||
slug: merged.slug,
|
||||
draft: merged.draft ?? false,
|
||||
categories: merged.categories || [],
|
||||
tags: merged.tags || [],
|
||||
author: merged.author || "",
|
||||
layout: merged.layout || "post",
|
||||
};
|
||||
if (merged.ai_comment) fm.ai_comment = merged.ai_comment;
|
||||
|
||||
const fileContent = matter.stringify(content ?? post.content, fm);
|
||||
fs.writeFileSync(post.filePath, fileContent, "utf-8");
|
||||
return { success: true, dirPath: post.dirPath };
|
||||
}
|
||||
|
||||
export function getPostCount(): number {
|
||||
return findPostDirectories().filter((dir) =>
|
||||
fs.existsSync(path.join(dir, "index.md"))
|
||||
).length;
|
||||
}
|
||||
Reference in new issue
Block a user