Complete WIP: Architecture refactor.

Mount JSX server side templating for blog posts. Send AppShell conditionally. Maintain support for HMR via HTMLbundles using Bun's native fullstack dev server under an /hmr path. This is only mounted in development and is supported by the onImport Bun plugin. Add DB creation on startup and load pages based on those records.
This commit is contained in:
2026-01-08 05:13:48 -08:00
parent 3abd97702d
commit f46f4667a1
32 changed files with 2779 additions and 353 deletions

View File

@@ -1,29 +0,0 @@
import { main, type BunPlugin } from 'bun';
import { loadMetadata } from "./utils";
import matter from 'gray-matter';
import { marked } from 'marked';
import { AppShell } from "../src/frontend/AppShell";
import AppShellPage from "../src/frontend/AppShell.html" with { type: "text" };
import { renderToString } from "react-dom/server";
const markdownLoader: BunPlugin = {
name: 'markdown-loader',
setup(build) {
// Plugin implementation
build.onLoad({filter: /\.md$/}, async args => {
console.log("Loading markdown file:", args.path);
const {data, content } = matter(await Bun.file(args.path).text());
loadMetadata(args.path, data);
const html = marked.parse(content);
// JSX Approach
console.log(renderToString(AppShell({ post: html })))
return {
contents: renderToString(AppShell({ post: html })),
loader: 'html',
};
});
},
};
export default markdownLoader;

View File

@@ -0,0 +1,52 @@
import type { BunPlugin } from 'bun';
import React from 'react';
import { renderToString } from "react-dom/server";
import matter from 'gray-matter';
import { marked } from 'marked';
import { addToDatabase } from "../src/db/index";
import { AppShell } from "../src/frontend/AppShell";
import { Post } from "../src/frontend/pages/post";
// TODO: Add better type handling for if Markdown parsing fails
const markdownLoader: BunPlugin = {
name: 'markdown-loader',
setup(build) {
// Plugin implementation
build.onLoad({filter: /\.md$/}, async args => {
console.log("Loading markdown file:", args.path);
const {data, content } = matter(await Bun.file(args.path).text());
// Remove the title from content if it matches the frontmatter title to avoid duplicate H1s
let processedContent = content;
if (data.title) {
const titleHeadingRegex = new RegExp(`^#\\s+${data.title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm');
processedContent = content.replace(titleHeadingRegex, '').trim();
}
const bodyHtml = await marked.parse(processedContent);
// AppShell is required here for rendering. If used at route level
// Bun will only see an htmlBundle and fail to load anything
// Validate required fields
if (!data.title || !data.date) {
throw new Error(`Markdown files must include title and date in frontmatter: ${args.path}`);
}
const meta = {
title: data.title,
date: new Date(data.date),
readingTime: data.readingTime || `${Math.ceil(content.split(/\s+/).length / 200)} min read`
};
const renderedHtml = renderToString(<AppShell><Post meta={meta} children={bodyHtml} /></AppShell>);
addToDatabase(args.path, meta, bodyHtml); // Load the post to the database for dynamic querying
// JSX Approach
return {
contents: renderedHtml,
loader: 'html',
};
});
},
};
export default markdownLoader;

View File

@@ -1,15 +1,28 @@
import matter from 'gray-matter';
import { loadMetadata } from "./utils";
import { marked } from 'marked';
import { addToDatabase } from "../src/db/index";
// When the server starts, import all the blog post metadata into the database
// Executed on startup because it's included in <root> ./bunfig.toml
// Only import if not running in production
(async () => {
if (process.env.NODE_ENV === 'production') return;
const glob = new Bun.Glob("**/*.md");
for await (const file of glob.scan("./content")) {
const {data, content } = matter(await Bun.file(`./content/${file}`).text());
const route = `/${file.replace(/\.md$/, "")}`;
loadMetadata(route, data);
// Remove the title from content if it matches the frontmatter title to avoid duplicate H1s
let processedContent = content;
if (data.title) {
const titleHeadingRegex = new RegExp(`^#\\s+${data.title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}\\s*$`, 'm');
processedContent = content.replace(titleHeadingRegex, '').trim();
}
const bodyHtml = await marked.parse(processedContent);
addToDatabase(route, data, bodyHtml);
}
console.log('Posts have been imported into db');

View File

@@ -1,72 +0,0 @@
import { Database } from 'bun:sqlite';
import path from 'path';
// Initialize the database if it doesn't exist
const dbPath = path.join(process.cwd(), 'blog_metadata.sqlite');
const db = new Database(dbPath);
// Create the posts table if it doesn't exist
db.run(`
CREATE TABLE IF NOT EXISTS posts (
id INTEGER PRIMARY KEY AUTOINCREMENT,
path TEXT UNIQUE NOT NULL,
title TEXT,
date TEXT,
author TEXT,
tags TEXT,
published INTEGER,
summary TEXT,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
)
`);
// Load metadata from the blog post into a SQLite database
// This allows us to index and make queries against the metadata
// to support functions like search, filtering, and sorting
export function loadMetadata(filePath: string, data: { [key: string]: any }) {
if (!data) return;
try {
// Extract common fields
const { title, date, author, tags, published, summary } = data;
// Log the values for debugging
// console.log('Metadata values:', {
// filePath: typeof filePath,
// title: typeof title,
// date: typeof date,
// author: typeof author,
// tags: typeof tags,
// published: typeof published,
// summary: typeof summary
// });
// Convert all values to strings or null explicitly
const values = [
filePath ? String(filePath) : null,
title ? String(title) : null,
date ? String(date) : null,
author ? String(author) : null,
tags ? JSON.stringify(tags) : null,
published !== undefined ? (published ? 1 : 0) : null,
summary ? String(summary) : null
];
// Log the prepared values
// console.log('Prepared values:', values);
// Query to insert or replace metadata
const query = db.query(`
INSERT OR REPLACE INTO posts
(path, title, date, author, tags, published, summary, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, CURRENT_TIMESTAMP)
`);
query.run(...values);
// console.log(`Stored metadata for: ${filePath}`);
} catch (error) {
// console.error(`Failed to store metadata for ${filePath}:`, error);
}
}