/* Build-time SEO generator for WatchVIM (static HTML) - Pulls titles from Supabase at build time (preferred) - Falls back to /data/titles.json if Supabase env vars are missing - Generates /seo/titles/.html from /seo/_templates/title.html - Generates category pages - Writes /sitemap.xml and /robots.txt */ const fs = require("fs"); const path = require("path"); const { createClient } = require("@supabase/supabase-js"); const SITE = "https://watchvim.com"; const read = (p) => fs.readFileSync(p, "utf8"); const write = (p, s) => { fs.mkdirSync(path.dirname(p), { recursive: true }); fs.writeFileSync(p, s); }; const escHtml = (s = "") => String(s) .replaceAll("&", "&") .replaceAll("<", "<") .replaceAll(">", ">") .replaceAll('"', """) .replaceAll("'", "'"); const loadJSON = (p) => JSON.parse(read(p)); async function fetchTitlesFromSupabase() { const url = process.env.SUPABASE_URL; const key = process.env.SUPABASE_SERVICE_ROLE_KEY; // If not configured, return null so we fall back to JSON. if (!url || !key) return null; const supabase = createClient(url, key); const { data, error } = await supabase .from("seo_titles") .select("slug,name,year,type,genres,tags,description,poster,watchurl,updated_at") .order("updated_at", { ascending: false }); if (error) throw error; return (data || []).map((r) => ({ slug: r.slug, name: r.name, year: r.year, type: r.type, genres: r.genres || [], tags: r.tags || [], description: r.description || "", poster: r.poster || "", watchUrl: r.watchurl || "", updated_at: r.updated_at, })); } function buildTitlePage(t, template) { const canonical = `${SITE}/seo/titles/${t.slug}.html`; const titleTag = `${t.name} (${t.year || ""}) – Watch on WatchVIM`; const firstTag = (t.tags && t.tags[0]) ? `${t.tags[0]} ` : ""; const meta = `Stream ${t.name}, a ${firstTag}` + `title on WatchVIM — a Black-owned streaming platform for diverse and independent content.`; const pills = [] .concat(t.type ? [String(t.type).toUpperCase()] : []) .concat(t.genres || []) .concat(t.tags || []) .filter(Boolean) .slice(0, 10) .map((x) => `${escHtml(x)}`) .join(""); const schema = { "@context": "https://schema.org", "@type": t.type === "series" ? "TVSeries" : "Movie", name: t.name, datePublished: String(t.year || ""), description: t.description || "", image: t.poster || "", url: canonical, publisher: { "@type": "Organization", name: "WatchVIM", url: SITE, }, potentialAction: { "@type": "WatchAction", target: t.watchUrl || SITE, }, }; return template .replaceAll("JAY (2025) – Watch on WatchVIM", escHtml(titleTag)) .replaceAll("Stream JAY, a Black cinema title on WatchVIM — a Black-owned streaming platform for diverse and independent content.", escHtml(meta)) .replaceAll("https://watchvim.com/seo/titles/jay.html", escHtml(canonical)) .replaceAll("JAY (2025) – Watch on WatchVIM", escHtml(titleTag)) .replaceAll("Stream JAY, a Black cinema title on WatchVIM — a Black-owned streaming platform for diverse and independent content.", escHtml(meta)) .replaceAll("https://YOUR_CDN/posters/jay.jpg", escHtml(t.poster || `${SITE}/og-default.png`)) .replaceAll("{"@context":"https://schema.org","@type":"Movie","name":"JAY","datePublished":"2025","description":"A short film highlighting intimate moments and emotional truth.","image":"https://YOUR_CDN/posters/jay.jpg","url":"https://watchvim.com/seo/titles/jay.html","publisher":{"@type":"Organization","name":"WatchVIM","url":"https://watchvim.com"},"potentialAction":{"@type":"WatchAction","target":"https://watchvim.com/#/watch/title_mf6jbph3awcmjah8mkt?kind=content"}}", JSON.stringify(schema)) .replaceAll("https://YOUR_CDN/posters/jay.jpg", escHtml(t.poster || "")) .replaceAll("JAY", escHtml(t.name || "")) .replaceAll("JAY (2025)", escHtml(`${t.name || ""}${t.year ? ` (${t.year})` : ""}`)) .replaceAll("SHORTDramaIndieBlack cinemaDiverse filmmakers", pills) .replaceAll("A short film highlighting intimate moments and emotional truth.", escHtml(t.description || "")) .replaceAll("https://watchvim.com/#/watch/title_mf6jbph3awcmjah8mkt?kind=content", escHtml(t.watchUrl || SITE)); } function buildCategoryPage({ slug, h1, intro, filterFn }, titles, template) { const canonical = `${SITE}/seo/${slug}.html`; const list = titles.filter(filterFn); const itemsHtml = list .slice(0, 60) .map((t) => { const url = `/seo/titles/${t.slug}.html`; return `
  • ${escHtml(t.name)}${t.year ? ` (${escHtml(t.year)})` : ""}
    ${escHtml((t.genres || []).join(" · "))}
  • `; }) .join(""); const schema = { "@context": "https://schema.org", "@type": "CollectionPage", name: h1, url: canonical, isPartOf: { "@type": "WebSite", name: "WatchVIM", url: SITE }, }; return template .replaceAll("JAY (2025) – Watch on WatchVIM", escHtml(`${h1} | WatchVIM`)) .replaceAll("Stream JAY, a Black cinema title on WatchVIM — a Black-owned streaming platform for diverse and independent content.", escHtml(intro)) .replaceAll("https://watchvim.com/seo/titles/jay.html", escHtml(canonical)) .replaceAll("JAY (2025)", escHtml(h1)) .replaceAll("{{INTRO}}", escHtml(intro)) .replaceAll("{{ITEMS}}", itemsHtml) .replaceAll("{"@context":"https://schema.org","@type":"Movie","name":"JAY","datePublished":"2025","description":"A short film highlighting intimate moments and emotional truth.","image":"https://YOUR_CDN/posters/jay.jpg","url":"https://watchvim.com/seo/titles/jay.html","publisher":{"@type":"Organization","name":"WatchVIM","url":"https://watchvim.com"},"potentialAction":{"@type":"WatchAction","target":"https://watchvim.com/#/watch/title_mf6jbph3awcmjah8mkt?kind=content"}}", JSON.stringify(schema)); } function buildSitemap(urls) { const now = new Date().toISOString(); return ` ${urls.map((u) => ` ${u}${now}`).join("\n")} `; } function buildRobotsTxt() { return `User-agent: * Allow: / Sitemap: ${SITE}/sitemap.xml `; } (async function main() { // 1) Load titles (Supabase preferred) let titles = null; try { titles = await fetchTitlesFromSupabase(); } catch (e) { console.error("Supabase fetch failed, falling back to data/titles.json"); console.error(e); titles = null; } if (!titles) { const titlesPath = path.join(process.cwd(), "data", "titles.json"); titles = loadJSON(titlesPath); } // Normalize / validate basics titles = (titles || []).filter((t) => t && t.slug && t.name); // 2) Load templates (SINGULAR file name: title.html) const titleTpl = read(path.join(process.cwd(), "seo", "_templates", "title.html")); const catTpl = read(path.join(process.cwd(), "seo", "_templates", "category.html")); // 3) Generate title pages const titleUrls = []; for (const t of titles) { const html = buildTitlePage(t, titleTpl); const out = path.join(process.cwd(), "seo", "titles", `${t.slug}.html`); write(out, html); titleUrls.push(`${SITE}/seo/titles/${t.slug}.html`); } // 4) Category pages const categories = [ { slug: "black-owned-streaming-platform", h1: "WatchVIM – A Black-Owned Streaming Platform for Diverse Stories", intro: "WatchVIM is a Black-owned streaming platform featuring diverse, independent films and series by underrepresented creators.", filterFn: () => true, }, { slug: "diverse-streaming", h1: "Diverse Streaming on WatchVIM", intro: "Stream diverse and inclusive films and series curated to elevate underrepresented voices.", filterFn: (t) => (t.tags || []).some((x) => /diverse|representation|inclusive/i.test(x)), }, { slug: "black-indie-films", h1: "Black Indie Films Streaming on WatchVIM", intro: "Discover Black independent films and original stories streaming on WatchVIM.", filterFn: (t) => (t.tags || []).some((x) => /black/i.test(x)) || (t.genres || []).some((x) => /indie/i.test(x)), }, ]; const catUrls = []; for (const c of categories) { const html = buildCategoryPage(c, titles, catTpl); const out = path.join(process.cwd(), "seo", `${c.slug}.html`); write(out, html); catUrls.push(`${SITE}/seo/${c.slug}.html`); } // 5) Sitemap const staticUrls = [`${SITE}/`]; const sitemap = buildSitemap([...new Set([...staticUrls, ...catUrls, ...titleUrls])]); write(path.join(process.cwd(), "sitemap.xml"), sitemap); // 6) robots.txt write(path.join(process.cwd(), "robots.txt"), buildRobotsTxt()); console.log( `Generated: ${titleUrls.length} title pages, ${catUrls.length} category pages, sitemap.xml, robots.txt` ); })();