Files
wiki/scripts/sync-posts.ts

530 lines
18 KiB
TypeScript
Raw Normal View History

import fs from "fs";
import path from "path";
import matter from "gray-matter";
import { ConvexHttpClient } from "convex/browser";
import { api } from "../convex/_generated/api";
import dotenv from "dotenv";
// Load environment variables based on SYNC_ENV
const isProduction = process.env.SYNC_ENV === "production";
if (isProduction) {
// Production: load .env.production.local first
dotenv.config({ path: ".env.production.local" });
console.log("Syncing to PRODUCTION deployment...\n");
} else {
// Development: load .env.local
dotenv.config({ path: ".env.local" });
}
dotenv.config();
const CONTENT_DIR = path.join(process.cwd(), "content", "blog");
const PAGES_DIR = path.join(process.cwd(), "content", "pages");
const RAW_OUTPUT_DIR = path.join(process.cwd(), "public", "raw");
interface PostFrontmatter {
title: string;
description: string;
date: string;
slug: string;
published: boolean;
tags: string[];
readTime?: string;
image?: string; // Header/OG image URL
showImageAtTop?: boolean; // Display image at top of post (default: false)
excerpt?: string; // Short excerpt for card view
featured?: boolean; // Show in featured section
featuredOrder?: number; // Order in featured section (lower = first)
authorName?: string; // Author display name
authorImage?: string; // Author avatar image URL (round)
layout?: string; // Layout type: "sidebar" for docs-style layout
rightSidebar?: boolean; // Enable right sidebar with CopyPageDropdown (default: true when siteConfig.rightSidebar.enabled)
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat?: boolean; // Enable AI chat in right sidebar (requires rightSidebar: true)
blogFeatured?: boolean; // Show as hero featured post on /blog page
}
interface ParsedPost {
slug: string;
title: string;
description: string;
content: string;
date: string;
published: boolean;
tags: string[];
readTime?: string;
image?: string; // Header/OG image URL
showImageAtTop?: boolean; // Display image at top of post (default: false)
excerpt?: string; // Short excerpt for card view
featured?: boolean; // Show in featured section
featuredOrder?: number; // Order in featured section (lower = first)
authorName?: string; // Author display name
authorImage?: string; // Author avatar image URL (round)
layout?: string; // Layout type: "sidebar" for docs-style layout
rightSidebar?: boolean; // Enable right sidebar with CopyPageDropdown (default: true when siteConfig.rightSidebar.enabled)
showFooter?: boolean; // Show footer on this post (overrides siteConfig default)
footer?: string; // Footer markdown content (overrides siteConfig defaultContent)
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat?: boolean; // Enable AI chat in right sidebar (requires rightSidebar: true)
blogFeatured?: boolean; // Show as hero featured post on /blog page
}
// Page frontmatter (for static pages like About, Projects, Contact)
interface PageFrontmatter {
title: string;
slug: string;
published: boolean;
order?: number; // Display order in navigation
showInNav?: boolean; // Show in navigation menu (default: true)
excerpt?: string; // Short excerpt for card view
image?: string; // Thumbnail/OG image URL for featured cards
showImageAtTop?: boolean; // Display image at top of page (default: false)
featured?: boolean; // Show in featured section
featuredOrder?: number; // Order in featured section (lower = first)
authorName?: string; // Author display name
authorImage?: string; // Author avatar image URL (round)
layout?: string; // Layout type: "sidebar" for docs-style layout
rightSidebar?: boolean; // Enable right sidebar with CopyPageDropdown (default: true when siteConfig.rightSidebar.enabled)
showFooter?: boolean; // Show footer on this page (overrides siteConfig default)
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat?: boolean; // Enable AI chat in right sidebar (requires rightSidebar: true)
}
interface ParsedPage {
slug: string;
title: string;
content: string;
published: boolean;
order?: number;
showInNav?: boolean; // Show in navigation menu (default: true)
excerpt?: string; // Short excerpt for card view
image?: string; // Thumbnail/OG image URL for featured cards
showImageAtTop?: boolean; // Display image at top of page (default: false)
featured?: boolean; // Show in featured section
featuredOrder?: number; // Order in featured section (lower = first)
authorName?: string; // Author display name
authorImage?: string; // Author avatar image URL (round)
layout?: string; // Layout type: "sidebar" for docs-style layout
rightSidebar?: boolean; // Enable right sidebar with CopyPageDropdown (default: true when siteConfig.rightSidebar.enabled)
showFooter?: boolean; // Show footer on this page (overrides siteConfig default)
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat?: boolean; // Enable AI chat in right sidebar (requires rightSidebar: true)
}
// Calculate reading time based on word count
function calculateReadTime(content: string): string {
const wordsPerMinute = 200;
const wordCount = content.split(/\s+/).length;
const minutes = Math.ceil(wordCount / wordsPerMinute);
return `${minutes} min read`;
}
// Parse a single markdown file
function parseMarkdownFile(filePath: string): ParsedPost | null {
try {
const fileContent = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(fileContent);
const frontmatter = data as Partial<PostFrontmatter>;
// Validate required fields
if (!frontmatter.title || !frontmatter.date || !frontmatter.slug) {
console.warn(`Skipping ${filePath}: missing required frontmatter fields`);
return null;
}
return {
slug: frontmatter.slug,
title: frontmatter.title,
description: frontmatter.description || "",
content: content.trim(),
date: frontmatter.date,
published: frontmatter.published ?? true,
tags: frontmatter.tags || [],
readTime: frontmatter.readTime || calculateReadTime(content),
image: frontmatter.image, // Header/OG image URL
showImageAtTop: frontmatter.showImageAtTop, // Display image at top of post
excerpt: frontmatter.excerpt, // Short excerpt for card view
featured: frontmatter.featured, // Show in featured section
featuredOrder: frontmatter.featuredOrder, // Order in featured section
authorName: frontmatter.authorName, // Author display name
authorImage: frontmatter.authorImage, // Author avatar image URL
layout: frontmatter.layout, // Layout type: "sidebar" for docs-style layout
rightSidebar: frontmatter.rightSidebar, // Enable right sidebar with CopyPageDropdown
showFooter: frontmatter.showFooter, // Show footer on this post
footer: frontmatter.footer, // Footer markdown content
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat: frontmatter.aiChat, // Enable AI chat in right sidebar
blogFeatured: frontmatter.blogFeatured, // Show as hero featured post on /blog page
};
} catch (error) {
console.error(`Error parsing ${filePath}:`, error);
return null;
}
}
// Get all markdown files from the content directory
function getAllMarkdownFiles(): string[] {
if (!fs.existsSync(CONTENT_DIR)) {
console.log(`Creating content directory: ${CONTENT_DIR}`);
fs.mkdirSync(CONTENT_DIR, { recursive: true });
return [];
}
const files = fs.readdirSync(CONTENT_DIR);
return files
.filter((file) => file.endsWith(".md"))
.map((file) => path.join(CONTENT_DIR, file));
}
// Parse a single page markdown file
function parsePageFile(filePath: string): ParsedPage | null {
try {
const fileContent = fs.readFileSync(filePath, "utf-8");
const { data, content } = matter(fileContent);
const frontmatter = data as Partial<PageFrontmatter>;
// Validate required fields
if (!frontmatter.title || !frontmatter.slug) {
console.warn(
`Skipping page ${filePath}: missing required frontmatter fields`,
);
return null;
}
return {
slug: frontmatter.slug,
title: frontmatter.title,
content: content.trim(),
published: frontmatter.published ?? true,
order: frontmatter.order,
showInNav: frontmatter.showInNav, // Show in navigation menu (default: true)
excerpt: frontmatter.excerpt, // Short excerpt for card view
image: frontmatter.image, // Thumbnail/OG image URL for featured cards
showImageAtTop: frontmatter.showImageAtTop, // Display image at top of page
featured: frontmatter.featured, // Show in featured section
featuredOrder: frontmatter.featuredOrder, // Order in featured section
authorName: frontmatter.authorName, // Author display name
authorImage: frontmatter.authorImage, // Author avatar image URL
layout: frontmatter.layout, // Layout type: "sidebar" for docs-style layout
rightSidebar: frontmatter.rightSidebar, // Enable right sidebar with CopyPageDropdown
showFooter: frontmatter.showFooter, // Show footer on this page
feat: add AI Agent chat integration with Anthropic Claude API Add AI writing assistant (Agent) powered by Anthropic Claude API. Agent can be enabled on Write page (replaces textarea) and optionally in RightSidebar on posts/pages via frontmatter. Features: - AIChatView component with per-page chat history - Page content context support for AI responses - Markdown rendering for AI responses - User-friendly error handling for missing API keys - System prompt configurable via Convex environment variables - Anonymous session authentication using localStorage Environment variables required: - ANTHROPIC_API_KEY (required) - CLAUDE_PROMPT_STYLE, CLAUDE_PROMPT_COMMUNITY, CLAUDE_PROMPT_RULES (optional split prompts) - CLAUDE_SYSTEM_PROMPT (optional single prompt fallback) Configuration: - siteConfig.aiChat.enabledOnWritePage: Enable Agent toggle on /write page - siteConfig.aiChat.enabledOnContent: Allow Agent on posts/pages via frontmatter - Frontmatter aiChat: true (requires rightSidebar: true) Updated files: - src/components/AIChatView.tsx: AI chat interface component - src/components/RightSidebar.tsx: Conditional Agent rendering - src/pages/Write.tsx: Agent mode toggle (title changes to Agent) - convex/aiChats.ts: Chat history queries and mutations - convex/aiChatActions.ts: Claude API integration with error handling - convex/schema.ts: aiChats table with indexes - src/config/siteConfig.ts: AIChatConfig interface - Documentation updated across all files Documentation: - files.md: Updated component descriptions - changelog.md: Added v1.33.0 entry - TASK.md: Marked AI chat tasks as completed - README.md: Added AI Agent Chat section - content/pages/docs.md: Added AI Agent chat documentation - content/blog/setup-guide.md: Added AI Agent chat setup instructions - public/raw/changelog.md: Added v1.33.0 entry
2025-12-26 12:31:33 -08:00
aiChat: frontmatter.aiChat, // Enable AI chat in right sidebar
};
} catch (error) {
console.error(`Error parsing page ${filePath}:`, error);
return null;
}
}
// Get all page markdown files from the pages directory
function getAllPageFiles(): string[] {
if (!fs.existsSync(PAGES_DIR)) {
// Pages directory is optional, don't create it automatically
return [];
}
const files = fs.readdirSync(PAGES_DIR);
return files
.filter((file) => file.endsWith(".md"))
.map((file) => path.join(PAGES_DIR, file));
}
// Main sync function
async function syncPosts() {
console.log("Starting post sync...\n");
// Get Convex URL from environment
const convexUrl = process.env.VITE_CONVEX_URL || process.env.CONVEX_URL;
if (!convexUrl) {
console.error(
"Error: VITE_CONVEX_URL or CONVEX_URL environment variable is not set",
);
process.exit(1);
}
// Initialize Convex client
const client = new ConvexHttpClient(convexUrl);
// Get all markdown files
const markdownFiles = getAllMarkdownFiles();
console.log(`Found ${markdownFiles.length} markdown files\n`);
if (markdownFiles.length === 0) {
console.log("No markdown files found. Creating sample post...");
createSamplePost();
// Re-read files after creating sample
const newFiles = getAllMarkdownFiles();
markdownFiles.push(...newFiles);
}
// Parse all markdown files
const posts: ParsedPost[] = [];
for (const filePath of markdownFiles) {
const post = parseMarkdownFile(filePath);
if (post) {
posts.push(post);
console.log(`Parsed: ${post.title} (${post.slug})`);
}
}
console.log(`\nSyncing ${posts.length} posts to Convex...\n`);
// Sync posts to Convex
try {
const result = await client.mutation(api.posts.syncPostsPublic, { posts });
console.log("Sync complete!");
console.log(` Created: ${result.created}`);
console.log(` Updated: ${result.updated}`);
console.log(` Deleted: ${result.deleted}`);
} catch (error) {
console.error("Error syncing posts:", error);
process.exit(1);
}
// Sync pages if pages directory exists
const pageFiles = getAllPageFiles();
const pages: ParsedPage[] = [];
if (pageFiles.length > 0) {
console.log(`\nFound ${pageFiles.length} page files\n`);
for (const filePath of pageFiles) {
const page = parsePageFile(filePath);
if (page) {
pages.push(page);
console.log(`Parsed page: ${page.title} (${page.slug})`);
}
}
if (pages.length > 0) {
console.log(`\nSyncing ${pages.length} pages to Convex...\n`);
try {
const pageResult = await client.mutation(api.pages.syncPagesPublic, {
pages,
});
console.log("Pages sync complete!");
console.log(` Created: ${pageResult.created}`);
console.log(` Updated: ${pageResult.updated}`);
console.log(` Deleted: ${pageResult.deleted}`);
} catch (error) {
console.error("Error syncing pages:", error);
process.exit(1);
}
}
}
// Generate static raw markdown files in public/raw/
generateRawMarkdownFiles(posts, pages);
}
// Create a sample post if none exist
function createSamplePost() {
const samplePost = `---
title: "Hello World"
description: "Welcome to my blog. This is my first post."
date: "${new Date().toISOString().split("T")[0]}"
slug: "hello-world"
published: true
tags: ["introduction", "blog"]
---
# Hello World
Welcome to my blog! This is my first post.
## What to Expect
I'll be writing about:
- **Development**: Building applications with modern tools
- **AI**: Exploring artificial intelligence and machine learning
- **Productivity**: Tips and tricks for getting things done
## Code Example
Here's a simple TypeScript example:
\`\`\`typescript
function greet(name: string): string {
return \`Hello, \${name}!\`;
}
console.log(greet("World"));
\`\`\`
## Stay Tuned
More posts coming soon. Thanks for reading!
`;
const filePath = path.join(CONTENT_DIR, "hello-world.md");
fs.writeFileSync(filePath, samplePost);
console.log(`Created sample post: ${filePath}`);
}
// Generate static markdown file in public/raw/ directory
function generateRawMarkdownFile(
slug: string,
title: string,
description: string,
content: string,
date: string,
tags: string[],
readTime?: string,
type: "post" | "page" = "post",
): void {
// Ensure raw output directory exists
if (!fs.existsSync(RAW_OUTPUT_DIR)) {
fs.mkdirSync(RAW_OUTPUT_DIR, { recursive: true });
}
// Build metadata section
const metadataLines: string[] = [];
metadataLines.push(`Type: ${type}`);
metadataLines.push(`Date: ${date}`);
if (readTime) metadataLines.push(`Reading time: ${readTime}`);
if (tags && tags.length > 0) metadataLines.push(`Tags: ${tags.join(", ")}`);
// Build the full markdown document
let markdown = `# ${title}\n\n`;
// Add description if available
if (description) {
markdown += `> ${description}\n\n`;
}
// Add metadata block
markdown += `---\n${metadataLines.join("\n")}\n---\n\n`;
// Add main content
markdown += content;
// Write to file
const filePath = path.join(RAW_OUTPUT_DIR, `${slug}.md`);
fs.writeFileSync(filePath, markdown);
}
// Generate homepage index markdown file listing all posts
function generateHomepageIndex(posts: ParsedPost[], pages: ParsedPage[]): void {
const publishedPosts = posts.filter((p) => p.published);
const publishedPages = pages.filter((p) => p.published);
// Sort posts by date (newest first)
const sortedPosts = [...publishedPosts].sort((a, b) => {
return new Date(b.date).getTime() - new Date(a.date).getTime();
});
// Build markdown content
let markdown = `# Homepage\n\n`;
markdown += `This is the homepage index of all published content.\n\n`;
// Add posts section
if (sortedPosts.length > 0) {
markdown += `## Blog Posts (${sortedPosts.length})\n\n`;
for (const post of sortedPosts) {
markdown += `- **[${post.title}](/raw/${post.slug}.md)**`;
if (post.description) {
markdown += ` - ${post.description}`;
}
markdown += `\n - Date: ${post.date}`;
if (post.readTime) {
markdown += ` | Reading time: ${post.readTime}`;
}
if (post.tags && post.tags.length > 0) {
markdown += ` | Tags: ${post.tags.join(", ")}`;
}
markdown += `\n`;
}
markdown += `\n`;
}
// Add pages section
if (publishedPages.length > 0) {
markdown += `## Pages (${publishedPages.length})\n\n`;
// Sort pages by order if available, otherwise alphabetically
const sortedPages = [...publishedPages].sort((a, b) => {
if (a.order !== undefined && b.order !== undefined) {
return a.order - b.order;
}
if (a.order !== undefined) return -1;
if (b.order !== undefined) return 1;
return a.title.localeCompare(b.title);
});
for (const page of sortedPages) {
markdown += `- **[${page.title}](/raw/${page.slug}.md)**`;
if (page.excerpt) {
markdown += ` - ${page.excerpt}`;
}
markdown += `\n`;
}
markdown += `\n`;
}
// Add summary
markdown += `---\n\n`;
markdown += `**Total Content:** ${sortedPosts.length} posts, ${publishedPages.length} pages\n`;
markdown += `\nAll content is available as raw markdown files at \`/raw/{slug}.md\`\n`;
// Write index.md file
const indexPath = path.join(RAW_OUTPUT_DIR, "index.md");
fs.writeFileSync(indexPath, markdown);
console.log("Generated homepage index: index.md");
}
// Generate all raw markdown files during sync
function generateRawMarkdownFiles(
posts: ParsedPost[],
pages: ParsedPage[],
): void {
console.log("\nGenerating static markdown files in public/raw/...");
// Clear existing raw files
if (fs.existsSync(RAW_OUTPUT_DIR)) {
const existingFiles = fs.readdirSync(RAW_OUTPUT_DIR);
for (const file of existingFiles) {
if (file.endsWith(".md")) {
fs.unlinkSync(path.join(RAW_OUTPUT_DIR, file));
}
}
}
// Generate files for published posts
const publishedPosts = posts.filter((p) => p.published);
for (const post of publishedPosts) {
generateRawMarkdownFile(
post.slug,
post.title,
post.description,
post.content,
post.date,
post.tags,
post.readTime,
"post",
);
}
// Generate files for published pages
const publishedPages = pages.filter((p) => p.published);
for (const page of publishedPages) {
generateRawMarkdownFile(
page.slug,
page.title,
"", // pages don't have description
page.content,
new Date().toISOString().split("T")[0], // pages don't have date
[], // pages don't have tags
undefined,
"page",
);
}
// Generate homepage index markdown file
generateHomepageIndex(posts, pages);
console.log(
`Generated ${publishedPosts.length} post files, ${publishedPages.length} page files, and 1 index file`,
);
}
// Run the sync
syncPosts().catch(console.error);