Add Markdown import from Obsidian (Stage 8)

- md-to-tiptap.ts: remark-based converter (headings, lists, blockquotes,
  code blocks, bold/italic/strike, links, images, hr)
- Obsidian ![[wikilink]] stripped, [[link|alias]] → plain text
- POST /api/admin/import-md: parses frontmatter (gray-matter) + converts content
- LessonEditor: "Импорт .md" button populates editor without auto-save
- ROADMAP: marked Stages 2, 3, 5, 6, 7, 8 as complete, fixed numbering

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-04-07 15:44:42 +05:00
parent 6d93a7b406
commit c647b29712
7 changed files with 1085 additions and 92 deletions
+39
View File
@@ -0,0 +1,39 @@
import { NextRequest, NextResponse } from "next/server";
import { headers } from "next/headers";
import { auth } from "@/lib/auth";
import matter from "gray-matter";
import { mdToTiptap } from "@/lib/md-to-tiptap";
export async function POST(req: NextRequest) {
const session = await auth.api.getSession({ headers: await headers() });
if (!session || session.user.role !== "admin") {
return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
}
const formData = await req.formData();
const file = formData.get("file") as File | null;
if (!file) {
return NextResponse.json({ error: "No file provided" }, { status: 400 });
}
if (!file.name.endsWith(".md")) {
return NextResponse.json({ error: "Only .md files are supported" }, { status: 400 });
}
const raw = await file.text();
const { data: fm, content } = matter(raw);
// Extract known frontmatter fields (Obsidian-compatible naming)
const title =
typeof fm.title === "string" ? fm.title.trim() : null;
const kinescopeId =
(fm.kinescopeId ?? fm.kinescope_id ?? fm.videoId ?? fm.video_id ?? "") as string;
const order =
typeof fm.order === "number" ? fm.order : null;
const published =
typeof fm.published === "boolean" ? fm.published : null;
const tiptapContent = mdToTiptap(content);
return NextResponse.json({ title, kinescopeId, order, published, content: tiptapContent });
}