diff --git a/apps/roadmap/app/api/md/[...slug]/route.ts b/apps/roadmap/app/api/md/[...slug]/route.ts index abf7632b..3aff36b7 100644 --- a/apps/roadmap/app/api/md/[...slug]/route.ts +++ b/apps/roadmap/app/api/md/[...slug]/route.ts @@ -12,7 +12,11 @@ import { renderLaneMarkdown, renderPersonMarkdown, renderTicketMarkdown, + renderHomeMarkdown, + renderAboutMarkdown, + renderExperimentsMarkdown, } from "@/lib/markdown" +import { EXPERIMENTS } from "@/lib/experiments" const HEADERS = { "Content-Type": "text/markdown; charset=utf-8", @@ -75,6 +79,25 @@ export async function GET( }) } + case "index": { + const features = getAllFeatures() + return new Response(renderHomeMarkdown(features, EXPERIMENTS), { + headers: HEADERS, + }) + } + + case "about": { + return new Response(renderAboutMarkdown(EXPERIMENTS), { + headers: HEADERS, + }) + } + + case "experiments": { + return new Response(renderExperimentsMarkdown(EXPERIMENTS), { + headers: HEADERS, + }) + } + default: return notFound(`Unknown path: /${slug.join("/")}`) } diff --git a/apps/roadmap/app/llms.txt/route.ts b/apps/roadmap/app/llms.txt/route.ts index 9f345f7e..e0ec452d 100644 --- a/apps/roadmap/app/llms.txt/route.ts +++ b/apps/roadmap/app/llms.txt/route.ts @@ -18,9 +18,12 @@ export async function GET() { "", `This site provides a read-only view of the JesusFilm project roadmap. Every page is available as markdown by appending \`.md\` to the URL.`, "", - `## Overview`, + `## Pages`, "", + `- [Home](/index.md): Overview with progress stats, recently shipped features, and live experiments`, `- [Full Roadmap](/roadmap.md): All features with status, priority, owner, and timeline`, + `- [About](/about.md): Mission, team, focus areas, guardrails, and timeline`, + `- [Experiments](/experiments.md): Active project demos with links and team info`, "", `## Lanes`, "", diff --git a/apps/roadmap/lib/markdown.ts b/apps/roadmap/lib/markdown.ts index 7d4cad29..b5ef376b 100644 --- a/apps/roadmap/lib/markdown.ts +++ b/apps/roadmap/lib/markdown.ts @@ -1,5 +1,6 @@ import type { Feature, FeatureStatus, Lane } from "./features" import { getStatusCounts, getLaneLabel } from "./features" +import type { Experiment } from "./experiments" function statusEmoji(status: FeatureStatus): string { switch (status) { @@ -106,3 +107,185 @@ export function renderTicketMarkdown(feature: Feature): string { ] return lines.join("\n") } + +export function renderHomeMarkdown( + features: Feature[], + experiments: Experiment[], +): string { + const counts = getStatusCounts(features) + + const recentlyCompleted = features + .filter((f) => f.status === "complete" && f.start_date) + .sort((a, b) => { + const endA = new Date( + new Date(a.start_date + "T00:00:00").getTime() + + (a.duration - 1) * 86400000, + ) + const endB = new Date( + new Date(b.start_date + "T00:00:00").getTime() + + (b.duration - 1) * 86400000, + ) + return endB.getTime() - endA.getTime() + }) + .slice(0, 5) + + const lines: string[] = [ + `# JesusFilm Digital Strategies Roadmap`, + "", + `> Reaching every person, in every language, through the power of AI.`, + "", + `The Digital Strategies Department is building trusted AI capabilities that help people discover gospel content, engage meaningfully with Scripture, and take faithful next steps.`, + "", + `## Progress at a Glance`, + "", + `- **Features Shipped:** ${counts.complete}`, + `- **In Progress:** ${counts["in-progress"]}`, + `- **Total Planned:** ${features.length}`, + "", + `[Full Roadmap](/roadmap.md) | [About](/about.md) | [Experiments](/experiments.md)`, + ] + + if (recentlyCompleted.length > 0) { + lines.push("", `## Recently Shipped`, "") + for (const f of recentlyCompleted) { + lines.push(`- [${f.title}](/ticket/${f.id}.md) — ${getLaneLabel(f.lane)}`) + } + } + + if (experiments.length > 0) { + lines.push("", `## Live Experiments`, "") + for (const exp of experiments) { + const link = + exp.links[0] && !exp.comingSoon + ? ` — [${exp.links[0].label}](${exp.links[0].href})` + : exp.comingSoon + ? " — Coming soon" + : "" + lines.push(`- **${exp.title}:** ${exp.description}${link}`) + } + } + + return lines.join("\n") +} + +export function renderAboutMarkdown(experiments: Experiment[]): string { + const team = [ + { name: "Tataihono", role: "Architect" }, + { name: "Vlad", role: "Product Owner & Manager Builder" }, + { name: "Ekkasit", role: "AI Experience Generation" }, + { name: "Nisal", role: "Backend" }, + { name: "Urim", role: "Frontend (Web & Mobile)" }, + ] + + const principles = [ + { + title: "Theological fidelity", + description: + "AI assists, humans verify. No speculative doctrine. Every generated piece of content is grounded in trusted Scripture and reviewed before publication.", + }, + { + title: "Human oversight", + description: + "Generated content starts as drafts, published only after human review. The AI proposes; the ministry team decides.", + }, + { + title: "Safe experimentation", + description: + "Practical outcomes over full automation. We ship incremental value, measure impact, and iterate with care.", + }, + { + title: "Ministry first", + description: + "Technology serves the mission, not the other way around. Every capability we build is measured by lives reached, not models deployed.", + }, + ] + + const quarters = [ + { label: "Sept – Nov 2025", title: "Foundation" }, + { label: "Dec 2025 – Feb 2026", title: "Infrastructure & Data" }, + { label: "March – May 2026", title: "Search, Topics, Audio (current)" }, + { + label: "June – Aug 2026", + title: "Personalization, Publishing, Video AI", + }, + ] + + const lines: string[] = [ + `# About — JesusFilm Digital Strategies`, + "", + `> Reaching every person, in every language, through the power of AI.`, + "", + `Build trusted, scalable AI capabilities that help people discover gospel content, engage meaningfully with Scripture, and take faithful next steps, while maintaining strong theological and ministry guardrails.`, + "", + `## The Opportunity`, + "", + `Billions of people across hundreds of languages are searching for hope, meaning, and truth. The Jesus Film Project has decades of gospel media (films, short videos, Scripture resources) but connecting the right content to the right person at the right moment remains an enormous challenge. AI changes the equation — not by replacing human ministry, but by making it possible to structure, discover, and deliver content at a scale that was previously impossible.`, + "", + `## Three Focus Areas`, + "", + `1. **Content Discovery & Recommendation** — Structure and tag media so people and AI systems can discover related content. Semantic search, embeddings, and intelligent recommendations.`, + `2. **Topic Pages & Guided Journeys** — Use clustered content and AI assistance to generate clear, public-facing topic pages. Tens of thousands of pages, each a doorway to the gospel.`, + `3. **AI-Assisted Media Creation** — Reduce the cost and effort of creating media through AI-assisted subtitles, audio, and video. Break language barriers at scale.`, + "", + `## Year 1 Timeline`, + "", + ...quarters.map((q) => `- **${q.label}:** ${q.title}`), + "", + `## The Team`, + "", + `| Name | Role |`, + `|------|------|`, + ...team.map((m) => `| ${m.name} | ${m.role} |`), + "", + `## Our Guardrails`, + "", + ...principles.map((p) => `- **${p.title}:** ${p.description}`), + ] + + if (experiments.length > 0) { + lines.push("", `## Live Experiments`, "") + for (const exp of experiments) { + const link = + exp.links[0] && !exp.comingSoon + ? ` — [${exp.links[0].label}](${exp.links[0].href})` + : exp.comingSoon + ? " — Coming soon" + : "" + lines.push(`- **${exp.title}:** ${exp.description}${link}`) + } + } + + return lines.join("\n") +} + +export function renderExperimentsMarkdown(experiments: Experiment[]): string { + const lines: string[] = [ + `# Experiments`, + "", + `Active projects demonstrating what we're building. Each explores a different way to use technology for ministry, from hand-crafted experiences to AI-generated content to mobile delivery.`, + ] + + for (const exp of experiments) { + lines.push( + "", + `## ${exp.number}. ${exp.title}`, + "", + exp.description, + "", + `- **Team:** ${exp.team.map((k) => k.charAt(0).toUpperCase() + k.slice(1)).join(", ")}`, + ) + + if (exp.comingSoon) { + lines.push(`- **Status:** Coming soon`) + } else if (exp.links.length > 0) { + for (const link of exp.links) { + lines.push(`- **${link.label}:** ${link.href}`) + } + if (exp.loginRequired) { + lines.push(`- *Login required*`) + } + } + } + + return lines.join("\n") +} diff --git a/apps/roadmap/next.config.ts b/apps/roadmap/next.config.ts index 36e9d00c..753f6ac1 100644 --- a/apps/roadmap/next.config.ts +++ b/apps/roadmap/next.config.ts @@ -3,6 +3,10 @@ import type { NextConfig } from "next" const nextConfig: NextConfig = { async rewrites() { return [ + { + source: "/index.md", + destination: "/api/md/index", + }, { source: "/:path*.md", destination: "/api/md/:path*",