Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions app/api/generate/[framework]/[fileName]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import path from 'path'

import type { WizardResponses } from '@/types/wizard'
import { getTemplateConfig, type TemplateKey } from '@/lib/template-config'
import { getStackGuidance } from '@/lib/stack-guidance'

function mapOutputFileToTemplateType(outputFile: string): string {
const mapping: Record<string, string> = {
Expand Down Expand Up @@ -111,8 +112,23 @@ export async function POST(
replaceVariable('logging')
replaceVariable('commitStyle')
replaceVariable('prRules')
const replaceStaticPlaceholder = (placeholderKey: string, value: string) => {
const placeholder = `{{${placeholderKey}}}`

if (!generatedContent.includes(placeholder)) {
return
}

const replacement = isJsonTemplate ? escapeForJson(value) : value
generatedContent = generatedContent.replace(placeholder, replacement)
}

replaceVariable('outputFile')

const stackGuidanceSlug = responses.stackSelection || frameworkFromPath
const stackGuidance = getStackGuidance(stackGuidanceSlug)
replaceStaticPlaceholder('stackGuidance', stackGuidance)

return NextResponse.json({
content: generatedContent,
fileName: templateConfig.outputFileName,
Expand Down
9 changes: 8 additions & 1 deletion app/new/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { track } from "@/lib/mixpanel"
import type { DataQuestionSource, FileOutputConfig } from "@/types/wizard"
import { Github } from "lucide-react"
import Link from "next/link"
import { useSearchParams } from "next/navigation"

import filesData from "@/data/files.json"
import { buildFileOptionsFromQuestion } from "@/lib/wizard-utils"
Expand All @@ -20,10 +21,12 @@ const fileQuestion = fileQuestionSet[0] ?? null
const fileOptionsFromData = buildFileOptionsFromQuestion(fileQuestion)

export default function NewInstructionsPage() {
const searchParams = useSearchParams()
const [showWizard, setShowWizard] = useState(false)
const [selectedFileId, setSelectedFileId] = useState<string | null>(null)

const fileOptions = useMemo(() => fileOptionsFromData, [])
const preferredStackId = searchParams.get("stack")?.toLowerCase() ?? null

const handleFileCtaClick = (file: FileOutputConfig) => {
setSelectedFileId(file.id)
Expand Down Expand Up @@ -72,7 +75,11 @@ export default function NewInstructionsPage() {
{/* Hero Section */}
<main className={getHomeMainClasses(showWizard)}>
{showWizard && selectedFileId ? (
<InstructionsWizard selectedFileId={selectedFileId} onClose={handleWizardClose} />
<InstructionsWizard
selectedFileId={selectedFileId}
onClose={handleWizardClose}
initialStackId={preferredStackId}
/>
) : (
<>
<div className="space-y-6">
Expand Down
3 changes: 3 additions & 0 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,9 @@ export default function LandingPage() {
<div className="relative z-10 flex min-h-screen flex-col">
<header className="flex items-center justify-end px-6 py-6 lg:px-12 lg:py-8">
<div className="flex items-center gap-3">
<Button asChild variant="ghost" size="sm" className="hidden sm:inline-flex">
<Link href="/stacks">Browse stacks</Link>
</Button>
<Button asChild variant="ghost" size="sm" className="hidden sm:inline-flex">
<Link href="/new">Launch wizard</Link>
</Button>
Expand Down
189 changes: 189 additions & 0 deletions app/stacks/[stack]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,189 @@
import Link from "next/link"
import type { Metadata } from "next"
import { notFound } from "next/navigation"

import stacksData from "@/data/stacks.json"
import type { DataQuestionSource } from "@/types/wizard"

const stackQuestionSet = stacksData as DataQuestionSource[]
const stackQuestion = stackQuestionSet[0]
const stackAnswers = stackQuestion?.answers ?? []

const STACK_PAGE_DETAILS: Record<string, { title: string; description: string; highlights: string[]; docsNote?: string }> = {
react: {
title: "React instructions file",
description:
"Ship consistent React instructions that cover hooks usage, component structures, and testing expectations before you open the wizard.",
highlights: [
"Bundle instructions for hooks, components, and state tooling",
"Document styling decisions across Tailwind or CSS Modules",
"Share testing policies with Jest and React Testing Library",
],
},
nextjs: {
title: "Next.js instructions file",
description: "Clarify App Router patterns, rendering modes, and data fetching strategies for your Next.js project.",
highlights: [
"Capture SSR, SSG, or ISR defaults for routes",
"Explain data fetching via server actions or route handlers",
"Align styling and component conventions across the monorepo",
],
},
angular: {
title: "Angular instructions file",
description: "Outline Angular module structure, RxJS usage, and testing defaults for teams shipping with Angular.",
highlights: [
"Define standalone components vs. NgModule structure",
"Document reactive forms, signals, and service patterns",
"Capture Jest or Karma coverage expectations",
],
},
vue: {
title: "Vue instructions file",
description: "Guide your team on Composition API, Pinia state, and the Vue testing stack before exporting guidance.",
highlights: [
"Document whether components use the Composition or Options API",
"Agree on Pinia, Vuex, or lightweight store helpers",
"Share styling choices from SFC scoped CSS to Tailwind",
],
},
nuxt: {
title: "Nuxt instructions file",
description: "Capture Nuxt-specific rendering, data fetching, and deployment strategies to feed into AI assistants.",
highlights: [
"Set expectations for SSR, SSG, or hybrid rendering",
"List how you use runtime config, server routes, and Nitro",
"Clarify deployment adapters whether Vercel, Netlify, or custom",
],
},
svelte: {
title: "Svelte instructions file",
description: "Align on SvelteKit tooling, store patterns, and styling so generated instructions match your stack.",
highlights: [
"Capture whether you build with SvelteKit or bare Vite",
"Explain store usage and when to reach for external libs",
"Define styling guidance across scoped CSS or Tailwind",
],
},
astro: {
title: "Astro instructions file",
description: "Describe islands architecture, content collections, and deployment workflows for Astro projects.",
highlights: [
"Identify which integration (React, Vue, Svelte) powers islands",
"Explain your rendering defaults and revalidation windows",
"Document CMS or content-collection structure for writers",
],
},
remix: {
title: "Remix instructions file",
description: "Share data loader patterns, runtime choices, and styling so AI agents stay true to your Remix app.",
highlights: [
"Capture whether loaders, resource routes, or client fetch power data",
"Document hosting choices like Vercel, Fly.io, or Express",
"Signal styling conventions across Tailwind or CSS Modules",
],
},
python: {
title: "Python instructions file",
description: "Detail your Python framework, typing policy, and packaging so Copilot and agents stay in sync.",
highlights: [
"State whether FastAPI, Django, or Flask powers the API",
"Explain typing expectations and lint tooling like Ruff",
"Document package and testing workflows across Poetry or pytest",
],
docsNote: "Need a framework not listed? Pick Python, then document it in the first question.",
},
}

export function generateStaticParams() {
return stackAnswers
.filter((answer) => typeof answer.value === "string" && answer.value.length > 0)
.map((answer) => ({ stack: answer.value }))
}

export function generateMetadata({ params }: { params: { stack: string } }): Metadata {
const slug = params.stack.toLowerCase()
const stackEntry = stackAnswers.find((answer) => answer.value === slug)

if (!stackEntry) {
return {
title: "DevContext instructions",
description: "Generate clear AI assistant instructions tailored to your framework.",
}
}

const details = STACK_PAGE_DETAILS[slug]
const title = details ? `${details.title} | DevContext` : `${stackEntry.label} instructions file | DevContext`
const description = details?.description ?? `Create an instructions file for ${stackEntry.label} with DevContext.`

return {
title,
description,
alternates: {
canonical: `https://devcontext.xyz/stacks/${slug}`,
},
}
}

export default function StackLandingPage({ params }: { params: { stack: string } }) {
const slug = params.stack.toLowerCase()
const stackEntry = stackAnswers.find((answer) => answer.value === slug)

if (!stackEntry) {
notFound()
}

const details = STACK_PAGE_DETAILS[slug]
const highlights = details?.highlights ?? []
const pageTitle = details?.title ?? `${stackEntry.label} instructions file`
const description = details?.description ?? `Start a ${stackEntry.label} instructions wizard with DevContext.`
const targetUrl = `/new?stack=${slug}`

return (
<main className="mx-auto flex min-h-screen max-w-3xl flex-col gap-10 px-6 py-16 text-foreground">
<header className="space-y-4">
<p className="text-xs font-semibold uppercase tracking-wide text-muted-foreground">Stack presets</p>
<h1 className="text-4xl font-semibold tracking-tight md:text-5xl">{pageTitle}</h1>
<p className="text-base text-muted-foreground md:text-lg">{description}</p>
{stackEntry.docs ? (
<p className="text-sm text-muted-foreground">
Source docs: {" "}
<a href={stackEntry.docs} target="_blank" rel="noreferrer" className="underline-offset-4 hover:underline">
{stackEntry.label} documentation
</a>
</p>
) : null}
{details?.docsNote ? (
<p className="text-sm text-muted-foreground">{details.docsNote}</p>
) : null}
</header>

{highlights.length > 0 ? (
<section className="space-y-3">
<h2 className="text-2xl font-semibold tracking-tight">What this preset covers</h2>
<ul className="list-disc space-y-2 pl-6 text-sm text-muted-foreground">
{highlights.map((bullet) => (
<li key={bullet}>{bullet}</li>
))}
</ul>
</section>
) : null}

<div className="flex flex-col gap-4 rounded-2xl border border-border/70 bg-card/95 p-6 shadow-sm">
<h2 className="text-xl font-semibold">Ready to build your instructions?</h2>
<p className="text-sm text-muted-foreground">
Launch the DevContext wizard with {stackEntry.label} pre-selected. You can review every question, accept defaults, and export a
stack-aware instructions file when each section is complete.
</p>
<div>
<Link
href={targetUrl}
className="inline-flex items-center justify-center rounded-lg border border-border/80 bg-primary px-5 py-2 text-sm font-semibold text-primary-foreground shadow-sm transition hover:translate-y-[1px] hover:bg-primary/90"
>
Start the {stackEntry.label} wizard
</Link>
</div>
</div>
</main>
)
}
59 changes: 59 additions & 0 deletions app/stacks/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
import Link from "next/link"
import type { Metadata } from "next"

import stacksData from "@/data/stacks.json"
import type { DataQuestionSource } from "@/types/wizard"

const stackQuestionSet = stacksData as DataQuestionSource[]
const stackQuestion = stackQuestionSet[0]
const stackAnswers = stackQuestion?.answers ?? []

export const metadata: Metadata = {
title: "Choose Your Stack | DevContext",
description: "Explore framework-specific instructions flows for React, Vue, Svelte, Python, and more.",
}

export default function StacksIndexPage() {
return (
<main className="mx-auto flex min-h-screen max-w-4xl flex-col gap-10 px-6 py-16 text-foreground">
<header className="space-y-4 text-center">
<h1 className="text-4xl font-semibold tracking-tight md:text-5xl">Framework instructions presets</h1>
<p className="mx-auto max-w-2xl text-base text-muted-foreground md:text-lg">
Jump straight into the DevContext wizard with copy tailored to your stack. Each page outlines what we cover and links directly
into the guided flow.
</p>
</header>

<section className="grid gap-6 md:grid-cols-2">
{stackAnswers.map((answer) => {
const href = answer.value ? `/stacks/${answer.value}` : "/new"
return (
<article
key={answer.value}
className="flex flex-col gap-4 rounded-2xl border border-border/70 bg-card/95 p-6 shadow-sm transition hover:-translate-y-1 hover:shadow-lg"
>
<div className="space-y-2">
<h2 className="text-2xl font-semibold tracking-tight">{answer.label}</h2>
{answer.docs ? (
<p className="text-sm text-muted-foreground">
<a href={answer.docs} target="_blank" rel="noreferrer" className="underline-offset-4 hover:underline">
Official documentation
</a>
</p>
) : null}
</div>
<div className="mt-auto">
<Link
href={href}
className="inline-flex items-center justify-center rounded-lg border border-border/80 bg-background/80 px-4 py-2 text-sm font-semibold transition hover:border-primary/40 hover:text-primary"
>
View {answer.label} instructions
</Link>
</div>
</article>
)
})}
</section>
</main>
)
}
Loading