Skip to content

Commit 2617328

Browse files
committed
feat(seo): Add breadcrumbs, canonical URLs, and FAQ schema
- Add BreadcrumbList JSON-LD to agent detail and publisher pages (Home → Store → Publisher → Agent) - Add canonical URLs to store, publisher, and agent pages to prevent duplicate content - Add FAQPage JSON-LD schema to FAQ docs page with 10 Q&A pairs - Add docs breadcrumb navigation schema - Expand sitemap with docs pages and pricing page
1 parent 74e8bab commit 2617328

File tree

6 files changed

+293
-96
lines changed

6 files changed

+293
-96
lines changed

sdk/src/agents/load-agents.ts

Lines changed: 4 additions & 78 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,4 @@
1-
import { createHash } from 'crypto'
21
import fs from 'fs'
3-
import { builtinModules } from 'module'
42
import os from 'os'
53
import path from 'path'
64
import { pathToFileURL } from 'url'
@@ -149,7 +147,7 @@ const getDefaultAgentDirs = () => {
149147
* - `{homedir}/.agents`
150148
*
151149
* Agent files can be `.ts`, `.tsx`, `.js`, `.mjs`, or `.cjs`.
152-
* TypeScript files are automatically transpiled.
150+
* TypeScript files are loaded natively by Bun's runtime.
153151
*
154152
* @param options.agentsPath - Optional path to a specific agents directory
155153
* @param options.verbose - Whether to log errors during loading
@@ -219,7 +217,7 @@ export async function loadLocalAgents({
219217

220218
for (const fullPath of allAgentFiles) {
221219
try {
222-
const agentModule = await importAgentModule(fullPath, verbose)
220+
const agentModule = await importAgentModule(fullPath)
223221
if (!agentModule) {
224222
continue
225223
}
@@ -312,80 +310,8 @@ export async function loadLocalAgents({
312310
return agents
313311
}
314312

315-
async function importAgentModule(
316-
fullPath: string,
317-
verbose: boolean,
318-
): Promise<any | null> {
319-
const extension = path.extname(fullPath).toLowerCase()
313+
async function importAgentModule(fullPath: string): Promise<any | null> {
314+
// Cache-bust to ensure fresh imports when agent files change
320315
const urlVersion = `?update=${Date.now()}`
321-
322-
if (extension === '.ts' || extension === '.tsx') {
323-
const compiledPath = await transpileAgent(fullPath, verbose)
324-
if (!compiledPath) {
325-
return null
326-
}
327-
return import(`${pathToFileURL(compiledPath).href}${urlVersion}`)
328-
}
329-
330316
return import(`${pathToFileURL(fullPath).href}${urlVersion}`)
331317
}
332-
333-
async function transpileAgent(
334-
fullPath: string,
335-
verbose: boolean,
336-
): Promise<string | null> {
337-
const canUseBunBuild =
338-
typeof Bun !== 'undefined' && typeof Bun.build === 'function'
339-
340-
if (!canUseBunBuild) {
341-
if (verbose) {
342-
console.error(`Cannot transpile ${fullPath}: Bun.build not available`)
343-
}
344-
return null
345-
}
346-
347-
const hash = createHash('sha1').update(fullPath).digest('hex')
348-
// Store compiled agents inside the current project so node module resolution
349-
// can find dependencies (e.g. lodash, zod/v4) via parent node_modules.
350-
const tempDir = path.join(process.cwd(), '.codebuff', 'agents')
351-
const compiledPath = path.join(tempDir, `${hash}.mjs`)
352-
353-
const result = await Bun.build({
354-
entrypoints: [fullPath],
355-
outdir: tempDir,
356-
target: 'node',
357-
format: 'esm',
358-
sourcemap: 'inline',
359-
splitting: false,
360-
minify: false,
361-
root: process.cwd(),
362-
packages: 'external',
363-
external: [
364-
...builtinModules,
365-
...builtinModules.map((mod) => `node:${mod}`),
366-
],
367-
throw: false,
368-
})
369-
370-
if (!result.success) {
371-
if (verbose) {
372-
console.error(`Bun.build failed for agent: ${fullPath}`)
373-
}
374-
return null
375-
}
376-
377-
const entryOutput =
378-
result.outputs.find((output) => output.kind === 'entry-point') ??
379-
result.outputs[0]
380-
const jsText = entryOutput ? await entryOutput.text() : null
381-
if (!jsText) {
382-
if (verbose) {
383-
console.error(`Failed to transpile agent (no output): ${fullPath}`)
384-
}
385-
return null
386-
}
387-
388-
await fs.promises.mkdir(tempDir, { recursive: true })
389-
await fs.promises.writeFile(compiledPath, jsText, 'utf8')
390-
return compiledPath
391-
}

web/src/app/docs/[category]/[slug]/page.tsx

Lines changed: 145 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
'use client'
22

3+
import { env } from '@codebuff/common/env'
34
import dynamic from 'next/dynamic'
45
import NextLink from 'next/link'
56
import { notFound, useParams } from 'next/navigation'
@@ -11,6 +12,126 @@ import { Mdx } from '@/components/docs/mdx/mdx-components'
1112
import { getDocsByCategory } from '@/lib/docs'
1213
import { allDocs } from '.contentlayer/generated'
1314

15+
// FAQ structured data for SEO - parsed from the FAQ MDX content
16+
const FAQ_ITEMS = [
17+
{
18+
question: 'What can Codebuff be used for?',
19+
answer:
20+
'Software development: Writing features, tests, and scripts across common languages and frameworks. It can also run CLI commands, adjust build configs, review code, and answer questions about your repo.',
21+
},
22+
{
23+
question: 'What model does Codebuff use?',
24+
answer:
25+
'Multiple. The orchestrator ("Buffy") uses Claude Opus 4.5 in Default and Max modes, or Grok 4.1 Fast in Lite mode. Subagents are matched to their tasks: GPT-5.1 and Claude Opus 4.5 for code editing, Gemini 2.5 Pro for deep reasoning, Grok 4 Fast for terminal commands and research, and Relace AI for fast file rewrites.',
26+
},
27+
{
28+
question: 'Is Codebuff open source?',
29+
answer:
30+
"Yes. It's Apache 2.0 at github.com/CodebuffAI/codebuff.",
31+
},
32+
{
33+
question: 'Do you store my data?',
34+
answer:
35+
"We don't store your codebase. The server forwards requests to model providers. We keep small slices of chat logs for debugging.",
36+
},
37+
{
38+
question:
39+
'Do you use model providers that train on my codebase or chat data?',
40+
answer:
41+
"No, we don't choose providers that will train on your data in our standard modes.",
42+
},
43+
{
44+
question: 'Can I trust Codebuff with full access to my terminal?',
45+
answer:
46+
'If you want isolation, use the Dockerfile to run Codebuff against a scoped copy of your codebase.',
47+
},
48+
{
49+
question: 'Can I specify custom instructions for Codebuff?',
50+
answer:
51+
"Yes. Add knowledge.md files to describe patterns, constraints, and commands. Codebuff also reads AGENTS.md and CLAUDE.md if present. Per directory, it picks one: knowledge.md first, then AGENTS.md, then CLAUDE.md. Codebuff updates existing knowledge files but won't create them unless you ask.",
52+
},
53+
{
54+
question: 'Can I tell Codebuff to ignore certain files?',
55+
answer:
56+
'Codebuff by default will not read files that are specified in your .gitignore. You can also create a .codebuffignore file to specify additional files or folders to ignore.',
57+
},
58+
{
59+
question: 'How does Codebuff work?',
60+
answer:
61+
'Codebuff runs specialized models in parallel: one finds files, another reasons through the problem, another writes code, another reviews. A selector picks the best output. In Max mode, multiple implementations compete.',
62+
},
63+
{
64+
question: 'How does Codebuff compare to Claude Code?',
65+
answer:
66+
'Codebuff is faster, cheaper, and handles large codebases better. See the detailed comparison in our documentation.',
67+
},
68+
]
69+
70+
function FAQJsonLd() {
71+
const jsonLd = {
72+
'@context': 'https://schema.org',
73+
'@type': 'FAQPage',
74+
mainEntity: FAQ_ITEMS.map((item) => ({
75+
'@type': 'Question',
76+
name: item.question,
77+
acceptedAnswer: {
78+
'@type': 'Answer',
79+
text: item.answer,
80+
},
81+
})),
82+
}
83+
84+
return (
85+
<script
86+
type="application/ld+json"
87+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
88+
/>
89+
)
90+
}
91+
92+
// Breadcrumb JSON-LD for docs pages
93+
function DocsBreadcrumbJsonLd({
94+
category,
95+
title,
96+
slug,
97+
}: {
98+
category: string
99+
title: string
100+
slug: string
101+
}) {
102+
const jsonLd = {
103+
'@context': 'https://schema.org',
104+
'@type': 'BreadcrumbList',
105+
itemListElement: [
106+
{
107+
'@type': 'ListItem',
108+
position: 1,
109+
name: 'Documentation',
110+
item: `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/docs`,
111+
},
112+
{
113+
'@type': 'ListItem',
114+
position: 2,
115+
name: category.charAt(0).toUpperCase() + category.slice(1),
116+
item: `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/docs/${category}`,
117+
},
118+
{
119+
'@type': 'ListItem',
120+
position: 3,
121+
name: title,
122+
item: `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/docs/${category}/${slug}`,
123+
},
124+
],
125+
}
126+
127+
return (
128+
<script
129+
type="application/ld+json"
130+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
131+
/>
132+
)
133+
}
134+
14135
const DocNavigation = ({
15136
sortedDocs,
16137
category,
@@ -84,25 +205,31 @@ export default function DocPage() {
84205

85206
const sortedDocs = [...docs].sort((a, b) => (a.order ?? 0) - (b.order ?? 0))
86207

208+
const isFaqPage = slug === 'faq'
209+
87210
return (
88-
<div className="max-w-3xl mx-auto">
89-
<article className="prose dark:prose-invert prose-compact max-w-none overflow-x-auto">
90-
<Mdx code={doc.body.code} />
91-
92-
{React.createElement(
93-
dynamic(() =>
94-
import(`@/content/${doc.category}/_cta.mdx`).catch(
95-
() => () => null,
211+
<>
212+
<DocsBreadcrumbJsonLd category={category} title={doc.title} slug={slug} />
213+
{isFaqPage && <FAQJsonLd />}
214+
<div className="max-w-3xl mx-auto">
215+
<article className="prose dark:prose-invert prose-compact max-w-none overflow-x-auto">
216+
<Mdx code={doc.body.code} />
217+
218+
{React.createElement(
219+
dynamic(() =>
220+
import(`@/content/${doc.category}/_cta.mdx`).catch(
221+
() => () => null,
222+
),
96223
),
97-
),
98-
)}
99-
</article>
100-
101-
<DocNavigation
102-
sortedDocs={sortedDocs}
103-
category={category}
104-
currentSlug={slug}
105-
/>
106-
</div>
224+
)}
225+
</article>
226+
227+
<DocNavigation
228+
sortedDocs={sortedDocs}
229+
category={category}
230+
currentSlug={slug}
231+
/>
232+
</div>
233+
</>
107234
)
108235
}

web/src/app/publishers/[id]/agents/[agentId]/[version]/page.tsx

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,14 @@ export async function generateMetadata({ params }: AgentDetailPageProps) {
7474
agentData.description ||
7575
`View details for ${agentName} version ${agent[0].version}`
7676
const ogImages = (pub?.[0]?.avatar_url ? [pub[0].avatar_url] : []) as string[]
77+
const canonicalUrl = `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/publishers/${id}/agents/${agentId}/${version}`
7778

7879
return {
7980
title,
8081
description,
82+
alternates: {
83+
canonical: canonicalUrl,
84+
},
8185
openGraph: {
8286
title,
8387
description,
@@ -142,6 +146,59 @@ function AgentJsonLd({
142146
)
143147
}
144148

149+
// Breadcrumb JSON-LD for navigation hierarchy
150+
function BreadcrumbJsonLd({
151+
publisherId,
152+
publisherName,
153+
agentName,
154+
agentId,
155+
version,
156+
}: {
157+
publisherId: string
158+
publisherName: string
159+
agentName: string
160+
agentId: string
161+
version: string
162+
}) {
163+
const jsonLd = {
164+
'@context': 'https://schema.org',
165+
'@type': 'BreadcrumbList',
166+
itemListElement: [
167+
{
168+
'@type': 'ListItem',
169+
position: 1,
170+
name: 'Home',
171+
item: env.NEXT_PUBLIC_CODEBUFF_APP_URL,
172+
},
173+
{
174+
'@type': 'ListItem',
175+
position: 2,
176+
name: 'Agent Store',
177+
item: `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/store`,
178+
},
179+
{
180+
'@type': 'ListItem',
181+
position: 3,
182+
name: publisherName,
183+
item: `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/publishers/${publisherId}`,
184+
},
185+
{
186+
'@type': 'ListItem',
187+
position: 4,
188+
name: `${agentName} v${version}`,
189+
item: `${env.NEXT_PUBLIC_CODEBUFF_APP_URL}/publishers/${publisherId}/agents/${agentId}/${version}`,
190+
},
191+
],
192+
}
193+
194+
return (
195+
<script
196+
type="application/ld+json"
197+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
198+
/>
199+
)
200+
}
201+
145202
const AgentDetailPage = async ({ params }: AgentDetailPageProps) => {
146203
const { id, agentId, version } = await params
147204
// Get publisher info
@@ -215,6 +272,13 @@ const AgentDetailPage = async ({ params }: AgentDetailPageProps) => {
215272
publisherName={publisherData.name}
216273
createdAt={new Date(agent[0].created_at)}
217274
/>
275+
<BreadcrumbJsonLd
276+
publisherId={id}
277+
publisherName={publisherData.name}
278+
agentName={agentName}
279+
agentId={agentId}
280+
version={version}
281+
/>
218282
<div className="container mx-auto py-6 px-4">
219283
<div className="max-w-4xl mx-auto">
220284
{' '}

0 commit comments

Comments
 (0)