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/routes/api.posts.tsx | |
| 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/routes/api.posts.tsx')
| -rw-r--r-- | app/routes/api.posts.tsx | 42 |
1 files changed, 42 insertions, 0 deletions
diff --git a/app/routes/api.posts.tsx b/app/routes/api.posts.tsx new file mode 100644 index 0000000..2d5637c --- /dev/null +++ b/app/routes/api.posts.tsx @@ -0,0 +1,42 @@ +import type { Route } from "./+types/api.posts"; +import { createPost, listPosts } from "~/lib/db.server"; +import { renderMarkdown } from "~/lib/md.server"; + +export async function loader() { + const posts = listPosts(); + return Response.json( + posts.map((p) => ({ ...p, html: renderMarkdown(p.content) })) + ); +} + +export async function action({ request }: Route.ActionArgs) { + if (request.method !== "POST") { + return Response.json({ error: "Method not allowed" }, { status: 405 }); + } + + let content: string; + const ct = request.headers.get("content-type") ?? ""; + if (ct.includes("application/json")) { + const body = (await request.json()) as { content?: unknown }; + content = String(body.content ?? "").trim(); + } else { + const form = await request.formData(); + content = String(form.get("content") ?? "").trim(); + } + + if (!content) { + return Response.json({ error: "content is required" }, { status: 400 }); + } + if (content.length > 10000) { + return Response.json( + { error: "content too long (max 10000 chars)" }, + { status: 400 } + ); + } + + const post = createPost({ id: crypto.randomUUID(), content }); + return Response.json( + { ...post, html: renderMarkdown(post.content) }, + { status: 201 } + ); +} |
