docs: add per-template setup instructions (supabase, trigger.dev)

Adds instructions.md files for the Supabase and Trigger.dev templates and
renders them in the template browser as a new Instructions tab in the
template dialog (fetched from blueprints/<id>/instructions.md, rendered
with a small dependency-free markdown component).

Also removes the placeholder blueprints/ackee/instructions.md so it does
not surface as an empty Instructions tab.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
Mauricio Siu
2026-07-14 12:04:30 -06:00
parent 17efd705a4
commit 03a60bb417
6 changed files with 349 additions and 11 deletions

View File

@@ -14,6 +14,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from "./ui/tabs";
import { Label } from "./ui/label";
import { Clipboard } from "lucide-react";
import { Input } from "./ui/input";
import Markdown from "./ui/markdown";
interface Template {
id: string;
@@ -32,6 +33,7 @@ interface Template {
interface TemplateFiles {
dockerCompose: string | null;
config: string | null;
instructions: string | null;
}
interface TemplateDialogProps {
@@ -169,8 +171,23 @@ const TemplateDialog: React.FC<TemplateDialogProps> = ({
</div>
</div>
)}
<Tabs defaultValue="docker-compose" className="w-full">
<Tabs
defaultValue={
templateFiles?.instructions
? "instructions"
: "docker-compose"
}
className="w-full"
>
<TabsList className="w-full justify-start">
{templateFiles?.instructions && (
<TabsTrigger
value="instructions"
className="data-[state=active]:font-bold"
>
Instructions
</TabsTrigger>
)}
<TabsTrigger
value="docker-compose"
className="data-[state=active]:font-bold"
@@ -184,6 +201,24 @@ const TemplateDialog: React.FC<TemplateDialogProps> = ({
Configuration
</TabsTrigger>
</TabsList>
{templateFiles?.instructions && (
<TabsContent value="instructions" className="mt-4">
<div className="space-y-2">
<Label className="flex flex-col items-start w-fit justify-start gap-1">
<span className="leading-tight text-xl font-semibold">
Setup Instructions
</span>
<span className="leading-tight text-sm text-gray-500">
instructions.md
</span>
</Label>
<div className="rounded-md border p-4">
<Markdown content={templateFiles.instructions} />
</div>
</div>
</TabsContent>
)}
<TabsContent value="docker-compose" className="mt-4">
{templateFiles?.dockerCompose && (
<div className="space-y-2">

View File

@@ -28,6 +28,7 @@ interface Template {
interface TemplateFiles {
dockerCompose: string | null;
config: string | null;
instructions: string | null;
}
interface TemplateGridProps {
@@ -80,20 +81,34 @@ const TemplateGrid: React.FC<TemplateGridProps> = ({ view }) => {
const fetchTemplateFiles = async (templateId: string) => {
setModalLoading(true);
try {
const [dockerComposeRes, configRes] = await Promise.all([
fetch(`/blueprints/${templateId}/docker-compose.yml`),
fetch(`/blueprints/${templateId}/template.toml`),
]);
const [dockerComposeRes, configRes, instructionsRes] = await Promise.all(
[
fetch(`/blueprints/${templateId}/docker-compose.yml`),
fetch(`/blueprints/${templateId}/template.toml`),
fetch(`/blueprints/${templateId}/instructions.md`),
]
);
const dockerCompose = dockerComposeRes.ok
? await dockerComposeRes.text()
: null;
const config = configRes.ok ? await configRes.text() : null;
setTemplateFiles({ dockerCompose, config });
// Guard against SPA fallbacks that return index.html with a 200 status
// for templates that don't have an instructions.md file.
const instructionsIsMarkdown =
instructionsRes.ok &&
!(instructionsRes.headers.get("content-type") || "").includes(
"text/html"
);
const instructions = instructionsIsMarkdown
? await instructionsRes.text()
: null;
setTemplateFiles({ dockerCompose, config, instructions });
} catch (err) {
console.error("Error fetching template files:", err);
setTemplateFiles({ dockerCompose: null, config: null });
setTemplateFiles({ dockerCompose: null, config: null, instructions: null });
} finally {
setModalLoading(false);
}

View File

@@ -0,0 +1,177 @@
import React from "react";
// Minimal markdown renderer (no external dependencies) for the per-template
// instructions.md files. Supports headings, paragraphs, fenced code blocks,
// ordered/unordered lists, and inline code / bold / links.
const INLINE_PATTERN =
/(`[^`]+`)|(\*\*[^*]+\*\*)|(\[[^\]]+\]\([^)\s]+\))/g;
const renderInline = (text: string): React.ReactNode[] => {
const nodes: React.ReactNode[] = [];
let lastIndex = 0;
let key = 0;
let match: RegExpExecArray | null;
INLINE_PATTERN.lastIndex = 0;
while ((match = INLINE_PATTERN.exec(text)) !== null) {
if (match.index > lastIndex) {
nodes.push(text.slice(lastIndex, match.index));
}
const token = match[0];
if (token.startsWith("`")) {
nodes.push(
<code
key={key++}
className="px-1.5 py-0.5 rounded bg-muted font-mono text-[0.85em]"
>
{token.slice(1, -1)}
</code>
);
} else if (token.startsWith("**")) {
nodes.push(<strong key={key++}>{token.slice(2, -2)}</strong>);
} else {
const link = /^\[([^\]]+)\]\(([^)\s]+)\)$/.exec(token);
if (link) {
nodes.push(
<a
key={key++}
href={link[2]}
target="_blank"
rel="noopener noreferrer"
className="text-blue-600 dark:text-blue-400 underline underline-offset-2"
>
{link[1]}
</a>
);
} else {
nodes.push(token);
}
}
lastIndex = match.index + token.length;
}
if (lastIndex < text.length) {
nodes.push(text.slice(lastIndex));
}
return nodes;
};
const HEADING_CLASSES: Record<number, string> = {
1: "text-2xl font-bold mt-2",
2: "text-xl font-semibold mt-6 border-b pb-1",
3: "text-lg font-semibold mt-4",
4: "text-base font-semibold mt-3",
};
interface MarkdownProps {
content: string;
}
const Markdown: React.FC<MarkdownProps> = ({ content }) => {
const lines = content.replace(/\r\n/g, "\n").split("\n");
const blocks: React.ReactNode[] = [];
let key = 0;
let i = 0;
while (i < lines.length) {
const line = lines[i];
// Blank line
if (line.trim() === "") {
i++;
continue;
}
// Fenced code block
if (line.trim().startsWith("```")) {
const code: string[] = [];
i++;
while (i < lines.length && !lines[i].trim().startsWith("```")) {
code.push(lines[i]);
i++;
}
i++; // skip closing fence
blocks.push(
<pre
key={key++}
className="bg-muted rounded-md p-4 overflow-x-auto text-sm font-mono whitespace-pre"
>
<code>{code.join("\n")}</code>
</pre>
);
continue;
}
// Heading
const heading = /^(#{1,4})\s+(.*)$/.exec(line);
if (heading) {
const level = heading[1].length;
const Tag = `h${Math.min(level + 1, 6)}` as keyof React.JSX.IntrinsicElements;
blocks.push(
<Tag key={key++} className={HEADING_CLASSES[level]}>
{renderInline(heading[2])}
</Tag>
);
i++;
continue;
}
// Unordered list
if (/^[-*]\s+/.test(line)) {
const items: string[] = [];
while (i < lines.length && /^[-*]\s+/.test(lines[i])) {
items.push(lines[i].replace(/^[-*]\s+/, ""));
i++;
}
blocks.push(
<ul key={key++} className="list-disc pl-6 space-y-1">
{items.map((item, idx) => (
<li key={idx}>{renderInline(item)}</li>
))}
</ul>
);
continue;
}
// Ordered list
if (/^\d+\.\s+/.test(line)) {
const items: string[] = [];
while (i < lines.length && /^\d+\.\s+/.test(lines[i])) {
items.push(lines[i].replace(/^\d+\.\s+/, ""));
i++;
}
blocks.push(
<ol key={key++} className="list-decimal pl-6 space-y-1">
{items.map((item, idx) => (
<li key={idx}>{renderInline(item)}</li>
))}
</ol>
);
continue;
}
// Paragraph: consume consecutive plain lines
const paragraph: string[] = [line];
i++;
while (
i < lines.length &&
lines[i].trim() !== "" &&
!/^(#{1,4})\s+/.test(lines[i]) &&
!/^[-*]\s+/.test(lines[i]) &&
!/^\d+\.\s+/.test(lines[i]) &&
!lines[i].trim().startsWith("```")
) {
paragraph.push(lines[i]);
i++;
}
blocks.push(
<p key={key++} className="leading-relaxed">
{renderInline(paragraph.join(" "))}
</p>
);
}
return <div className="space-y-3 text-sm">{blocks}</div>;
};
export default Markdown;