summaryrefslogtreecommitdiff
path: root/app/routes/api.posts.tsx
diff options
context:
space:
mode:
authoryyamashita <yyamashita@hetzner.yyamashita.com>2026-06-19 15:18:55 +0900
committeryyamashita <yyamashita@hetzner.yyamashita.com>2026-06-19 15:18:55 +0900
commit30f24e137ee2d1acec11a0da61e134f512ee02dd (patch)
tree28bd91bde3ad883f9f8fa543fca4b25ed50d05e9 /app/routes/api.posts.tsx
parentda4934ae84f8a81fe3f4f8b392a4a3dc3d566f30 (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.tsx42
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 }
+ );
+}