Files
wiki/convex/importAction.ts

146 lines
3.9 KiB
TypeScript
Raw Normal View History

"use node";
import { v } from "convex/values";
import { action } from "./_generated/server";
import { api } from "./_generated/api";
import FirecrawlApp from "@mendable/firecrawl-js";
/**
* Generate a URL-safe slug from a title
*/
function generateSlug(title: string): string {
return title
.toLowerCase()
.replace(/[^a-z0-9\s-]/g, "")
.replace(/\s+/g, "-")
.replace(/-+/g, "-")
.replace(/^-|-$/g, "")
.substring(0, 60);
}
/**
* Clean up markdown content
*/
function cleanMarkdown(content: string): string {
return content.replace(/^\s+|\s+$/g, "").replace(/\n{3,}/g, "\n\n");
}
/**
* Calculate reading time from content
*/
function calculateReadTime(content: string): string {
const wordsPerMinute = 200;
const wordCount = content.split(/\s+/).length;
const minutes = Math.ceil(wordCount / wordsPerMinute);
return `${minutes} min read`;
}
/**
* Import content from a URL using Firecrawl and save directly to database
*/
export const importFromUrl = action({
args: {
url: v.string(),
published: v.optional(v.boolean()),
},
returns: v.object({
success: v.boolean(),
slug: v.optional(v.string()),
title: v.optional(v.string()),
error: v.optional(v.string()),
}),
handler: async (ctx, args) => {
const apiKey = process.env.FIRECRAWL_API_KEY;
if (!apiKey) {
return {
success: false,
error:
"FIRECRAWL_API_KEY not configured. Add it to your Convex environment variables.",
};
}
try {
const firecrawl = new FirecrawlApp({ apiKey });
const result = await firecrawl.scrapeUrl(args.url, {
formats: ["markdown"],
});
if (!result.success || !result.markdown) {
return {
success: false,
error: result.error || "Failed to scrape URL - no content returned",
};
}
const title = result.metadata?.title || "Imported Post";
const description = result.metadata?.description || "";
const content = cleanMarkdown(result.markdown);
const baseSlug = generateSlug(title);
const slug = baseSlug || `imported-${Date.now()}`;
const today = new Date().toISOString().split("T")[0];
// Add source attribution
let hostname: string;
try {
hostname = new URL(args.url).hostname;
} catch {
hostname = "external source";
}
const contentWithAttribution = `${content}\n\n---\n\n*Originally published at [${hostname}](${args.url})*`;
// Create post directly in database using the CMS mutation
try {
await ctx.runMutation(api.cms.createPost, {
post: {
slug,
title,
description,
content: contentWithAttribution,
date: today,
published: args.published ?? false,
tags: ["imported"],
readTime: calculateReadTime(content),
},
});
} catch (mutationError) {
// Handle slug conflict by adding timestamp
if (
mutationError instanceof Error &&
mutationError.message.includes("already exists")
) {
const uniqueSlug = `${slug}-${Date.now()}`;
await ctx.runMutation(api.cms.createPost, {
post: {
slug: uniqueSlug,
title,
description,
content: contentWithAttribution,
date: today,
published: args.published ?? false,
tags: ["imported"],
readTime: calculateReadTime(content),
},
});
return {
success: true,
slug: uniqueSlug,
title,
};
}
throw mutationError;
}
return {
success: true,
slug,
title,
};
} catch (error) {
return {
success: false,
error: error instanceof Error ? error.message : "Unknown error occurred",
};
}
},
});