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
50 changes: 21 additions & 29 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -1,33 +1,23 @@
name: Deploy docs to Pages

# Builds the MkDocs Material site under docs/ and publishes it to
# https://mountainowl.github.io/bubo/. The repo's homepage
# URL in About points at that origin.
# Builds the Nextra docs site under docs/ and publishes it to
# https://mountainowl.github.io/bubo/. It is a GitHub Pages project site, so the
# build sets basePath=/bubo (NEXT_PUBLIC_BASE_PATH) and exports a static bundle.
#
# Triggers:
# * push to main when docs/, mkdocs.yml, or this workflow change
# * manual run via the Actions tab (workflow_dispatch) for re-deploys
# without a content change
#
# The Pages "GitHub Actions" source MUST be enabled in repo Settings
# → Pages → Source = "GitHub Actions" for the deploy step to bind
# its artifact to the live site.
# Requires repo Settings -> Pages -> Source = "GitHub Actions".

on:
push:
branches: [main]
paths:
- docs/**
- mkdocs.yml
- .github/workflows/deploy-docs.yml
- pyproject.toml
workflow_dispatch:

permissions:
contents: read

# Allow only one concurrent deploy. If a newer push lands while a build
# is mid-flight, cancel the older one — Pages only serves the latest.
# One deploy at a time; a newer push cancels an in-flight build.
concurrency:
group: pages-deploy
cancel-in-progress: true
Expand All @@ -36,40 +26,42 @@ jobs:
build:
name: Build site
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs
steps:
- name: Check out repository
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0

- name: Set up Python
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0
- name: Set up Node
uses: actions/setup-node@39370e3970a6d050c480ffad4ff0ed4d3fdee5af # v4.1.0
with:
python-version: "3.14"
node-version: "20"
cache: npm
cache-dependency-path: docs/package-lock.json

- name: Install docs dependencies
run: uv sync --locked --group docs
- name: Install dependencies
run: npm ci

- name: Build the static site
# --strict fails on any warning (broken link, missing nav entry,
# malformed YAML) so a busted PR can't ship a broken site.
run: uv run mkdocs build --strict
env:
NEXT_PUBLIC_BASE_PATH: /bubo
run: npm run build

- name: Upload artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: site
path: docs/out

deploy:
name: Deploy to Pages
needs: build
runs-on: ubuntu-latest
permissions:
pages: write # publish to the Pages environment
id-token: write # OIDC for the deploy-pages action
pages: write # publish to the Pages environment
id-token: write # OIDC for the deploy-pages action
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
Expand Down
11 changes: 11 additions & 0 deletions docs/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
.next
out
node_modules

# IDE / build artifacts
.idea/
.DS_Store
*.tsbuildinfo

# local agent / preview config
.claude/
23 changes: 23 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
# Nextra Docs Template

This is a template for creating documentation with [Nextra](https://nextra.site).

[**Live Demo →**](https://nextra-docs-template.vercel.app)

[![](.github/screenshot.png)](https://nextra-docs-template.vercel.app)

## Quick Start

Click the button to clone this repository and deploy it on Vercel:

[![](https://vercel.com/button)](https://vercel.com/new/clone?s=https%3A%2F%2Fgithub.com%2Fshuding%2Fnextra-docs-template&showOptionalTeamCreation=false)

## Local Development

First, run `pnpm i` to install the dependencies.

Then, run `pnpm dev` to start the development server and visit localhost:3000.

## License

This project is licensed under the MIT License.
13 changes: 13 additions & 0 deletions docs/components/Badge.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import type { ReactNode } from 'react'
import styles from './badge.module.css'

export function Badge({ children, href }: { children: ReactNode; href?: string }) {
if (href) {
return (
<a href={href} className={styles.badge}>
{children}
</a>
)
}
return <span className={styles.badge}>{children}</span>
}
6 changes: 6 additions & 0 deletions docs/components/Eyebrow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import type { ReactNode } from 'react'
import styles from './eyebrow.module.css'

export function Eyebrow({ children }: { children: ReactNode }) {
return <p className={styles.eyebrow}>{children}</p>
}
10 changes: 10 additions & 0 deletions docs/components/Footnote.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import type { ReactNode } from 'react'
import styles from './footnote.module.css'

export function Footnote({ id, children }: { id?: string; children: ReactNode }) {
return (
<div id={id} className={styles.note}>
{children}
</div>
)
}
228 changes: 228 additions & 0 deletions docs/components/Landing.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import type { ReactNode } from 'react'
import Link from 'next/link'
import styles from './landing.module.css'
import { SiteFooter } from './SiteFooter'

const ACCENT = '#1d4ed8'
const SYMBOLS: [string, number, number, number, number][] = [
['</>', 50, 50, -15, 24],
['{}', 150, 100, 10, 20],
['=>', 250, 80, -5, 18],
['[]', 100, 200, 15, 22],
['<>', 300, 180, -10, 20],
['()', 200, 250, 5, 24],
['::', 50, 320, -8, 18],
['==', 350, 300, 12, 22],
['++', 150, 350, -15, 20],
[';', 250, 370, 8, 24],
]

const ABOUT =
"Bubo stays quiet until it has evidence, and speaks only when something's worth your time. Your LLM, self-hosted. No filler. No praise. No generic summaries."

const CAPABILITIES = [
'Bring-your-own-LLM',
'Self-hosted',
'GitLab + GitHub',
'Signal over noise',
'Review moods',
'Dispute-driven learning',
'Verify before posting',
'Governance & provenance',
'OpenTelemetry',
'MCP',
'GitHub Action CI',
]

const FEATURES: { group: string; items: [string, ReactNode][] }[] = [
{
group: 'Reviews worth reading',
items: [
['Only what matters', 'What’s wrong, why, and how to fix it. No praise, no summaries.'],
['Dry run', 'Runs a real review and enumerates findings, but posts nothing.'],
['Pick the tone', 'Terse, collaborative, socratic, formal, or casual.'],
],
},
{
group: 'Use the AI you already trust',
items: [
['Bring your own model', 'Codex, Claude, or any CLI-driven model. No lock-in.'],
['SCM', 'Works with GitLab and GitHub.'],
],
},
{
group: 'Decide what gets flagged',
items: [
['Gate or collaborate', 'Merge-blockers only, or everything — suggestions and questions too.'],
['Confidence calibration', 'Post only what it’s sure of; raise the bar per issue type.'],
['Learns your taste', 'Keeps flagging what you dismiss? It stops.'],
['Double-checks first', 'An optional second model drops findings that don’t hold up.'],
],
},
{
group: 'Fits how you work',
items: [
['MCP', 'Connect your editor to trigger an interactive review on demand.'],
['Reviews in CI', 'A GitHub Action that comments on PRs in your pipeline.'],
['A simple dashboard', 'Read-only view of recent reviews, health, and reports.'],
],
},
{
group: 'Governance, compliance & ROI',
items: [
['AI-code provenance', 'Identifies AI-assisted and sensitive changes for closer review; it does not block merges by itself.'],
['Audit trail', 'A write-once, chronological record of every decision and its lifecycle.'],
['Self-hosted control', 'Repository data stays within the infrastructure and model path you configure.'],
['ROI reporting', 'Accept/dispute rate, bugs caught, noise, and cost.'],
[
'Redaction and signed releases',
<>
Credentials scrubbed before the model sees the diff. Signed releases,{' '}
<Link href="/overview#security" className={styles.featureLink}>
SBOM
</Link>
.
</>,
],
],
},
]

export function Landing() {
return (
<div className={styles.page}>
{/* HERO */}
<section className={styles.hero}>
<div className={styles.gradient} aria-hidden="true" />
<svg aria-hidden="true" className={styles.pattern}>
<defs>
<pattern id="grid" x="50%" y={-1} width={200} height={200} patternUnits="userSpaceOnUse">
<path d="M.5 200V.5H200" fill="none" />
</pattern>
<pattern id="symbols" x="0" y="0" width={400} height={400} patternUnits="userSpaceOnUse">
{SYMBOLS.map(([t, x, y, r, s], i) => (
<text
key={i}
x={x}
y={y}
fill={ACCENT}
fontFamily="monospace"
fontSize={s}
transform={`rotate(${r} ${x} ${y})`}
>
{t}
</text>
))}
</pattern>
</defs>
<rect fill="url(#symbols)" width="100%" height="100%" opacity="0.2" />
<rect fill="url(#grid)" width="100%" height="100%" strokeWidth={0} />
</svg>

<div className={styles.heroInner}>
<h2 className={styles.hello}>Hoo-hoo-hooooo… hoo-hoo 👋</h2>
<h1 className={styles.name}>
I&apos;m <span style={{ color: ACCENT }}>Bubo</span> 🦉
</h1>
<p className={styles.tagline}>
Self-hosted, <span className={styles.signalKey}>high signal-density</span>{' '}
agentic AI code reviewer.
</p>
<div className={styles.actions}>
<Link href="/recipes" className={styles.cta}>
Quickstart <span aria-hidden="true">→</span>
</Link>
<Link href="/overview" className={styles.ctaGhost}>
Read the documentation
</Link>
</div>
</div>

<div className={styles.social}>
<a
href="https://github.com/mountainowl/bubo"
target="_blank"
rel="noreferrer"
aria-label="GitHub"
>
<svg
xmlns="http://www.w3.org/2000/svg"
width="28"
height="28"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path stroke="none" d="M0 0h24v24H0z" fill="none" />
<path d="M9 19c-4.3 1.4 -4.3 -2.5 -6 -3m12 5v-3.5c0 -1 .1 -1.4 -.5 -2c2.8 -.3 5.5 -1.4 5.5 -6a4.6 4.6 0 0 0 -1.3 -3.2a4.2 4.2 0 0 0 -.1 -3.2s-1.1 -.3 -3.5 1.3a12.3 12.3 0 0 0 -6.2 0c-2.4 -1.6 -3.5 -1.3 -3.5 -1.3a4.2 4.2 0 0 0 -.1 3.2a4.6 4.6 0 0 0 -1.3 3.2c0 4.6 2.7 5.7 5.5 6c-.6 .6 -.6 1.2 -.5 2v3.5" />
</svg>
</a>
</div>
</section>

{/* ABOUT */}
<section className={styles.section}>
<div className={styles.grid}>
<div className={styles.head}>
<h2 className={styles.title}>About</h2>
<div className={styles.bar} />
</div>
<div className={styles.body}>
<p className={styles.about}>{ABOUT}</p>
</div>
</div>
</section>

{/* CAPABILITIES */}
<section className={styles.section}>
<div className={styles.grid}>
<div className={styles.head}>
<h2 className={styles.title}>Capabilities</h2>
<div className={styles.bar} />
</div>
<div className={styles.body}>
<div className={styles.pills}>
{CAPABILITIES.map((c) => (
<span key={c} className={styles.pill}>
{c}
</span>
))}
</div>
</div>
</div>
</section>

{/* FEATURES */}
<section className={styles.section}>
<div className={styles.grid}>
<div className={styles.head}>
<h2 className={styles.title}>Features</h2>
<div className={styles.bar} />
</div>
<div className={styles.body}>
<div className={styles.featureGroups}>
{FEATURES.map((g) => (
<div key={g.group} className={styles.featureGroup}>
<h3 className={styles.featureGroupTitle}>{g.group}</h3>
<ul className={styles.featureList}>
{g.items.map(([name, desc]) => (
<li key={name} className={styles.featureItem}>
<span className={styles.featureName}>{name}</span>
<span className={styles.featureDesc}>{desc}</span>
</li>
))}
</ul>
</div>
))}
</div>
</div>
</div>
</section>

<SiteFooter />
</div>
)
}
Loading