diff options
| author | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-06-19 15:18:55 +0900 |
|---|---|---|
| committer | yyamashita <yyamashita@hetzner.yyamashita.com> | 2026-06-19 15:18:55 +0900 |
| commit | 30f24e137ee2d1acec11a0da61e134f512ee02dd (patch) | |
| tree | 28bd91bde3ad883f9f8fa543fca4b25ed50d05e9 /app/lib | |
| parent | da4934ae84f8a81fe3f4f8b392a4a3dc3d566f30 (diff) | |
Initial implementation of microblog
Markdown-based personal microblog with REST API.
XSS protection via sanitize-html on server-side markdown rendering.
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Diffstat (limited to 'app/lib')
| -rw-r--r-- | app/lib/db.server.ts | 50 | ||||
| -rw-r--r-- | app/lib/md.server.ts | 33 |
2 files changed, 83 insertions, 0 deletions
diff --git a/app/lib/db.server.ts b/app/lib/db.server.ts new file mode 100644 index 0000000..b076ec7 --- /dev/null +++ b/app/lib/db.server.ts @@ -0,0 +1,50 @@ +import Database from "better-sqlite3"; +import path from "path"; + +let db: Database.Database | null = null; + +export function getDb(): Database.Database { + if (!db) { + const dbPath = process.env.DB_PATH ?? path.resolve("microblog.db"); + db = new Database(dbPath); + db.pragma("journal_mode = WAL"); + db.pragma("foreign_keys = ON"); + initSchema(db); + } + return db; +} + +function initSchema(db: Database.Database) { + db.exec(` + CREATE TABLE IF NOT EXISTS posts ( + id TEXT PRIMARY KEY, + content TEXT NOT NULL, + created_at TEXT NOT NULL DEFAULT (datetime('now')) + ); + CREATE INDEX IF NOT EXISTS idx_posts_created_at ON posts(created_at DESC); + `); +} + +export interface Post { + id: string; + content: string; + created_at: string; +} + +export interface CreatePostInput { + id: string; + content: string; +} + +export function listPosts(): Post[] { + return getDb().prepare("SELECT * FROM posts ORDER BY created_at DESC").all() as Post[]; +} + +export function getPostById(id: string): Post | null { + return getDb().prepare("SELECT * FROM posts WHERE id = ?").get(id) as Post | null; +} + +export function createPost(input: CreatePostInput): Post { + getDb().prepare("INSERT INTO posts (id, content) VALUES (?, ?)").run(input.id, input.content); + return getPostById(input.id)!; +} diff --git a/app/lib/md.server.ts b/app/lib/md.server.ts new file mode 100644 index 0000000..8f0d6d6 --- /dev/null +++ b/app/lib/md.server.ts @@ -0,0 +1,33 @@ +import { marked } from "marked"; +import sanitizeHtml from "sanitize-html"; + +const ALLOWED_TAGS = [ + "p", "br", + "h1", "h2", "h3", "h4", "h5", "h6", + "ul", "ol", "li", + "blockquote", + "pre", "code", + "strong", "em", "del", "s", + "a", + "hr", + "table", "thead", "tbody", "tr", "th", "td", + "img", +]; + +const ALLOWED_ATTRIBUTES: sanitizeHtml.IOptions["allowedAttributes"] = { + a: ["href", "title"], + img: ["src", "alt", "title"], + code: ["class"], + td: ["align"], + th: ["align"], +}; + +export function renderMarkdown(content: string): string { + const raw = marked.parse(content, { async: false }) as string; + return sanitizeHtml(raw, { + allowedTags: ALLOWED_TAGS, + allowedAttributes: ALLOWED_ATTRIBUTES, + allowedSchemes: ["http", "https", "mailto"], + allowedSchemesByTag: { img: ["http", "https"] }, + }); +} |
