feat: add Next.js marketing + documentation website#19
Conversation
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
- Copy boop.gif to public/assets and use animated GIF in Hero mascot and H1 inline emoji - Post-process markdown HTML to rewrite relative asset URLs to GitHub raw URLs - Add Quick start CTA button in Hero, id/scroll-mt on QuickstartBlock, and Quick start links in desktop and mobile Navbar Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Greptile SummaryThis PR adds a complete Next.js 14 marketing and documentation website under
Confidence Score: 3/5Hold for the XSS and unhandled-rejection issues in the docs pipeline before merging. Two P1 findings both in
|
| Filename | Overview |
|---|---|
| website/lib/docs.ts | Fetches and renders 5 markdown files from GitHub at build time via ISR; raw HTML pass-through (allowDangerousHtml: true) and missing error handling around Promise.all are two actionable issues. |
| website/components/DocSection.tsx | Renders pre-processed HTML with dangerouslySetInnerHTML; safe only if the upstream pipeline is sanitized (currently it is not). |
| website/components/DocsLayout.tsx | Client-side docs layout with IntersectionObserver sidebar tracking and mobile tab bar; cleanup in useEffect is correct and scroll behavior is solid. |
| website/components/Hero.tsx | Landing hero section; hardcoded star/fork counts will go stale; no runtime issues. |
| website/components/MermaidDiagram.tsx | Lazy-loads Mermaid, renders a hardcoded diagram, sets innerHTML from Mermaid's own SVG output (not external input); fallback on error is clean. |
| website/app/og/route.tsx | Edge-runtime OG image via next/og with static content; correct dimensions (1200×630). |
| website/app/page.tsx | Landing page server component; correctly wraps content in <main id="main-content"> enabling the skip-to-content link. |
| website/components/CopyButton.tsx | Clipboard copy with execCommand fallback; checks typeof window guard appropriately. |
Sequence Diagram
sequenceDiagram
participant Browser
participant Next.js (Edge)
participant Next.js (Server)
participant GitHub Raw
Browser->>Next.js (Edge): GET /og
Next.js (Edge)-->>Browser: 1200x630 ImageResponse
Browser->>Next.js (Server): GET /docs
Next.js (Server)->>GitHub Raw: fetch README.md (revalidate: 3600)
Next.js (Server)->>GitHub Raw: fetch ARCHITECTURE.md (revalidate: 3600)
Next.js (Server)->>GitHub Raw: fetch INTEGRATIONS.md (revalidate: 3600)
Next.js (Server)->>GitHub Raw: fetch CONTRIBUTING.md (revalidate: 3600)
Next.js (Server)->>GitHub Raw: fetch CHANGELOG.md (revalidate: 3600)
GitHub Raw-->>Next.js (Server): markdown text x5
Note over Next.js (Server): unified pipeline (remark to rehype to rehypePrettyCode) allowDangerousHtml:true, no rehypeSanitize
Next.js (Server)-->>Browser: HTML (dangerouslySetInnerHTML)
Browser->>Browser: IntersectionObserver sidebar tracking
Reviews (1): Last reviewed commit: "docs: add website implementation plan an..." | Re-trigger Greptile
| .use(remarkRehype, { allowDangerousHtml: true }) | ||
| .use(rehypePrettyCode as any, { | ||
| theme: 'github-dark', | ||
| keepBackground: true, | ||
| }) | ||
| .use(rehypeStringify, { allowDangerousHtml: true }) |
There was a problem hiding this comment.
Raw HTML pass-through enables XSS
Both remarkRehype({ allowDangerousHtml: true }) and rehypeStringify({ allowDangerousHtml: true }) are set, which allows any raw HTML embedded in the fetched markdown files to flow through the pipeline completely unsanitized and land in dangerouslySetInnerHTML. If any of the five markdown files on main ever contains a <script> tag or an event-handler attribute (via a compromised commit, a drive-by PR merge, or branch tampering), it will execute in every visitor's browser.
Adding rehype-sanitize between the code-highlighting step and rehypeStringify is the standard fix and has no visual impact on the rendered output.
import rehypeSanitize from 'rehype-sanitize'
// in parseMarkdown:
.use(rehypePrettyCode as any, { theme: 'github-dark', keepBackground: true })
.use(rehypeSanitize) // <-- add this
.use(rehypeStringify, { allowDangerousHtml: true })| export async function getAllDocs(): Promise<DocFile[]> { | ||
| const results = await Promise.all( | ||
| DOC_FILES.map(async ({ id, label, url }) => { | ||
| const res = await fetch(url, { next: { revalidate: 3600 } }) | ||
| const markdown = res.ok ? await res.text() : `# ${label}\n\n*Could not load this document.*` | ||
| const html = await parseMarkdown(markdown) | ||
| return { id, label, html } | ||
| }) | ||
| ) | ||
| return results |
There was a problem hiding this comment.
Network error crashes docs page on cold build
Promise.all is not wrapped in a try/catch. If any fetch call throws (e.g., DNS failure, GitHub rate-limit at build time, or transient network hiccup on a cold ISR start), the entire getAllDocs call rejects and the docs page shows a Next.js error boundary rather than the graceful per-section fallback that res.ok already handles for non-2xx responses.
export async function getAllDocs(): Promise<DocFile[]> {
const results = await Promise.all(
DOC_FILES.map(async ({ id, label, url }) => {
try {
const res = await fetch(url, { next: { revalidate: 3600 } })
const markdown = res.ok ? await res.text() : `# ${label}\n\n*Could not load this document.*`
const html = await parseMarkdown(markdown)
return { id, label, html }
} catch {
const html = await parseMarkdown(`# ${label}\n\n*Could not load this document.*`)
return { id, label, html }
}
})
)
return results
}| {[ | ||
| '43 stars', | ||
| '11 forks', | ||
| 'MIT License', | ||
| 'Built on Claude Agent SDK', | ||
| ].map((stat) => ( | ||
| <span | ||
| key={stat} | ||
| className="px-3 py-1 rounded-full text-xs text-text-muted border border-border bg-bg-card" | ||
| > | ||
| {stat} | ||
| </span> | ||
| ))} |
There was a problem hiding this comment.
Hardcoded star/fork counts will go stale immediately
'43 stars' and '11 forks' are static strings baked into the bundle. As soon as the PR merges, these counts will drift from reality. Consider either fetching live data from the GitHub API (https://api.github.com/repos/raroque/boop-agent) with ISR, or removing the count badges altogether and linking the pills to the GitHub repo so the real numbers are always one click away.
There was a problem hiding this comment.
Pull request overview
Adds a standalone Next.js 14 marketing + documentation site under website/, including a landing page and a /docs page that renders project markdown with syntax highlighting, plus SEO endpoints (sitemap/robots/OG).
Changes:
- Scaffolded a new Next.js 14 (App Router) site in
website/with Tailwind, TypeScript, and build scripts. - Implemented landing-page sections (hero, architecture/mermaid, features, quickstart w/ copy buttons, prerequisites) and shared layout components.
- Implemented
/docsrendering via GitHub raw markdown fetch + unified/rehype pipeline, with a client sidebar using IntersectionObserver, and added SEO routes/metadata.
Reviewed changes
Copilot reviewed 25 out of 30 changed files in this pull request and generated 7 comments.
Show a summary per file
| File | Description |
|---|---|
| website/tsconfig.json | TypeScript configuration for the Next.js site. |
| website/tailwind.config.ts | Tailwind theme and content scanning paths. |
| website/postcss.config.js | PostCSS setup for Tailwind + autoprefixer. |
| website/package.json | Website dependencies and scripts. |
| website/next.config.mjs | Next config for remote images and package import optimization. |
| website/next-env.d.ts | Next.js TypeScript environment declarations. |
| website/lib/docs.ts | Fetches and converts GitHub markdown to HTML for /docs. |
| website/components/QuickstartBlock.tsx | Quickstart steps UI with copy-to-clipboard actions. |
| website/components/PrerequisitesTable.tsx | Prerequisites table with external links. |
| website/components/Navbar.tsx | Responsive nav with mobile menu and top links. |
| website/components/MermaidDiagram.tsx | Client-side Mermaid render with fallback text diagram. |
| website/components/Hero.tsx | Landing hero section with CTAs and mascot imagery. |
| website/components/Footer.tsx | Site footer with links and attribution. |
| website/components/FeaturesGrid.tsx | Features grid of product capabilities. |
| website/components/DocsLayout.tsx | Docs page layout with active-section tracking + mobile tabs. |
| website/components/DocSection.tsx | Renders each fetched doc section into the page. |
| website/components/CopyButton.tsx | Client copy-to-clipboard button used in quickstart. |
| website/components/ArchitectureBlock.tsx | Architecture section combining Mermaid diagram + component cards. |
| website/app/sitemap.ts | Sitemap generation for / and /docs. |
| website/app/robots.ts | Robots.txt generation referencing sitemap. |
| website/app/page.tsx | Landing page composition. |
| website/app/og/route.tsx | Edge OG image generation route. |
| website/app/layout.tsx | Root layout, fonts, and global metadata. |
| website/app/globals.css | Global styles and custom prose styles for rendered docs. |
| website/app/docs/page.tsx | /docs page assembly and ISR configuration. |
| docs/superpowers/plans/2026-04-27-boop-website.md | Implementation plan documentation for building the site. |
| .gitignore | Ignores Next build output and dependencies under website/. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| .use(remarkRehype, { allowDangerousHtml: true }) | ||
| .use(rehypePrettyCode as any, { |
There was a problem hiding this comment.
The markdown pipeline enables allowDangerousHtml, which preserves raw HTML from the fetched markdown and can lead to XSS when combined with dangerouslySetInnerHTML on the docs page. Consider stripping raw HTML or sanitizing the output (e.g., rehype-sanitize with an allowlist for img, a, etc.).
| <div | ||
| className="prose-custom" | ||
| dangerouslySetInnerHTML={{ __html: doc.html }} | ||
| /> |
There was a problem hiding this comment.
This renders doc.html via dangerouslySetInnerHTML. Because the HTML is produced from remotely fetched markdown and the parser currently preserves raw HTML, this can allow XSS in the docs page. Please ensure doc.html is sanitized/escaped before injection (or change the markdown pipeline to strip/sanitize raw HTML).
|
|
||
| const scrollTo = (id: string) => { | ||
| const el = document.getElementById(id) | ||
| el?.scrollIntoView({ behavior: 'smooth', block: 'start' }) |
There was a problem hiding this comment.
scrollIntoView({ behavior: 'smooth' }) forces smooth scrolling even when the user has prefers-reduced-motion: reduce. Consider honoring reduced-motion by switching to behavior: 'auto' when that media query matches.
| el?.scrollIntoView({ behavior: 'smooth', block: 'start' }) | |
| const prefersReducedMotion = window.matchMedia( | |
| '(prefers-reduced-motion: reduce)' | |
| ).matches | |
| el?.scrollIntoView({ | |
| behavior: prefersReducedMotion ? 'auto' : 'smooth', | |
| block: 'start', | |
| }) |
| lastModified: new Date(), | ||
| changeFrequency: 'weekly', | ||
| priority: 1, | ||
| }, | ||
| { | ||
| url: 'https://boop-agent.vercel.app/docs', | ||
| lastModified: new Date(), |
There was a problem hiding this comment.
The sitemap entries set lastModified: new Date(), which will change on every sitemap generation and may mislead crawlers. Consider omitting lastModified or deriving it from real content/build metadata so it only changes when the content changes.
| lastModified: new Date(), | |
| changeFrequency: 'weekly', | |
| priority: 1, | |
| }, | |
| { | |
| url: 'https://boop-agent.vercel.app/docs', | |
| lastModified: new Date(), | |
| changeFrequency: 'weekly', | |
| priority: 1, | |
| }, | |
| { | |
| url: 'https://boop-agent.vercel.app/docs', |
| metadataBase: new URL('https://boop-agent.vercel.app'), | ||
| title: { default: 'Boop Agent', template: '%s | Boop Agent' }, | ||
| description: |
There was a problem hiding this comment.
metadataBase (and related OG URLs) are hard-coded to https://boop-agent.vercel.app, which will be incorrect for preview deployments, forks, or custom domains. Consider deriving the site URL from an environment variable (with a safe default) and using it consistently.
| ``` | ||
| website/ | ||
| package.json ← Next.js project deps (separate from root) | ||
| next.config.ts ← image domains, no extra config needed |
There was a problem hiding this comment.
The plan doc lists next.config.ts, but the actual implementation in this PR uses website/next.config.mjs. Updating the file map here will prevent confusion for future readers following the plan.
| next.config.ts ← image domains, no extra config needed | |
| next.config.mjs ← image domains, no extra config needed |
|
|
||
| 5. **`'use client'` boundary:** `DocsLayout`, `Navbar`, and `CopyButton` are client components. `DocSection`, all section components, and the page files themselves are server components. Never import a server component from a client component. | ||
|
|
||
| 6. **Images in markdown:** The README contains `<img>` tags with `assets/` relative paths that won't resolve on the docs page (they point to the GitHub repo). This is expected behavior — images in the rendered markdown will 404. Only the `boop.png` in `public/` is served by Next.js. |
There was a problem hiding this comment.
This note says markdown images using assets/ paths will 404 on the docs page, but the implementation rewrites src="assets/..." to GitHub raw URLs in website/lib/docs.ts. Please update/remove this edge case so the plan doc reflects current behavior.
| 6. **Images in markdown:** The README contains `<img>` tags with `assets/` relative paths that won't resolve on the docs page (they point to the GitHub repo). This is expected behavior — images in the rendered markdown will 404. Only the `boop.png` in `public/` is served by Next.js. | |
| 6. **Images in markdown:** The docs processing rewrites README `<img>` tags that use `assets/` relative paths to GitHub raw URLs, so those images should render correctly on the docs page. Only site-owned static assets placed in `public/` are served directly by Next.js. |
Summary
website/— landing page (/) and combined docs page (/docs)rehype-pretty-codesyntax highlighting and a fixed sidebar with IntersectionObserver active-section trackingTech stack
Next.js 14 App Router · TypeScript · Tailwind CSS v3 (custom dark/orange theme) · unified/remark/rehype · Mermaid.js · lucide-react · next/font (Inter + JetBrains Mono)
Test plan
cd website && npm run buildcompletes with zero errorsnpm run dev→ verify landing page athttp://localhost:3000/docs— all 5 sections render with syntax-highlighted code blockshttp://localhost:3000/ogreturns 1200×630 OG imagehttp://localhost:3000/sitemap.xmland/robots.txtreturn correctly🤖 Generated with Claude Code