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
95 changes: 79 additions & 16 deletions apps/docs/app/layout.tsx
Original file line number Diff line number Diff line change
@@ -1,25 +1,71 @@
/**
* Root layout for @gonext/docs.
*
* Renders the persistent shell: top nav (Docs / ADRs / API Reference /
* Proposals), the section sidebar (slot via `children`), and the search
* bar. The sidebar is itself rendered by individual section layouts so
* that pages outside the doc tree (landing, API reference) can render
* full-bleed.
* Renders the persistent shell for every docs route: the topbar with
* brand wordmark, primary nav, and global search; everything below the
* topbar is owned by the route — sidebars, articles, and TOC come from
* the per-section layouts under `app/docs/` and `app/adr/`.
*
* Brand fonts (Living-Systems handoff): Archivo for display headlines,
* Geist for UI/body, Geist Mono for code, Instrument Serif for the
* signature italic accents that swap in inside <em> tags. They load via
* `next/font/google` so the URLs are self-hosted, layout shift is
* suppressed by next/font's reserved-space metric, and the CSS
* variables surface for both Tailwind utilities and raw selectors in
* `styles/docs.css` / `src/styles/tokens.css`.
*/
import type { Metadata } from 'next';
import type { ReactElement, ReactNode } from 'react';
import Link from 'next/link';
import {
Archivo,
Geist,
Geist_Mono,
Instrument_Serif,
} from 'next/font/google';
import { SearchBar } from '@/components/SearchBar';
import { buildSearchIndex } from '@/lib/content';
import '@/styles/docs.css';

const archivo = Archivo({
subsets: ['latin'],
weight: ['500', '600', '700', '800', '900'],
variable: '--font-display',
display: 'swap',
});

const geist = Geist({
subsets: ['latin'],
weight: ['400', '500', '600', '700'],
variable: '--font-sans',
display: 'swap',
});

const geistMono = Geist_Mono({
subsets: ['latin'],
weight: ['400', '500'],
variable: '--font-mono',
display: 'swap',
});

const instrumentSerif = Instrument_Serif({
subsets: ['latin'],
weight: ['400'],
style: ['normal', 'italic'],
variable: '--font-serif',
display: 'swap',
});

export const metadata: Metadata = {
title: {
default: 'GoNext Documentation',
template: '%s · GoNext Docs',
},
description: 'Documentation, ADRs, and architectural references for the GoNext platform.',
description:
'Documentation, ADRs, and architectural references for the GoNext platform — a modular CMS built on Go and Next.js.',
icons: {
icon: '/favicon.svg',
},
};

export default async function RootLayout({
Expand All @@ -30,18 +76,35 @@ export default async function RootLayout({
// The search index is built once per build and inlined into the layout
// chunk. Keeps the index out of `_next/data` round-trips.
const entries = await buildSearchIndex();
const fontVariables = [
archivo.variable,
geist.variable,
geistMono.variable,
instrumentSerif.variable,
].join(' ');
return (
<html lang="en">
<html lang="en" className={fontVariables}>
<body>
<header className="docs-shell__header">
<Link href="/" className="docs-shell__brand">GoNext</Link>
<nav className="docs-shell__nav" aria-label="Primary">
<Link href="/docs" className="docs-shell__nav-link">Docs</Link>
<Link href="/adr" className="docs-shell__nav-link">ADRs</Link>
<Link href="/api" className="docs-shell__nav-link">API Reference</Link>
<Link href="/docs/proposals/14-proposals" className="docs-shell__nav-link">Proposals</Link>
</nav>
<SearchBar entries={entries} />
<header className="docs-topbar">
<div className="docs-topbar__left">
<Link href="/" className="docs-topbar__brand" aria-label="GoNext Docs home">
<span className="wordmark">
<span className="wm-go">Go</span>
<span className="wm-next">Next</span>
</span>
<span className="docs-topbar__brand-tag">Docs</span>
</Link>
<nav className="docs-topbar__nav" aria-label="Primary">
<Link href="/docs" className="docs-topbar__nav-link">Docs</Link>
<Link href="/adr" className="docs-topbar__nav-link">ADRs</Link>
<Link href="/api" className="docs-topbar__nav-link">API Reference</Link>
<Link href="/docs/proposals/14-proposals" className="docs-topbar__nav-link">Proposals</Link>
</nav>
</div>
<div className="docs-topbar__right">
<SearchBar entries={entries} />
<span className="docs-topbar__version">v1.0</span>
</div>
</header>
{children}
</body>
Expand Down
65 changes: 46 additions & 19 deletions apps/docs/app/page.tsx
Original file line number Diff line number Diff line change
@@ -1,15 +1,25 @@
/**
* Landing page.
* Landing page — the docs site front door.
*
* Three pieces, ordered top-down on the page:
* 1. A hero with the project name, one-line value prop, and a Getting
* Started CTA that links into the first doc.
* 2. A feature grid summarising what the docs cover.
* Three sections, ordered top-down:
* 1. A hero with a giant Archivo headline ("Docs that *grow* with you.")
* where the italic emerald `grow` ties to the brand's
* italic-accent rule. Beneath it sits a Geist subtitle and two
* prominent CTAs — emerald primary (Read the docs) and a cream
* secondary (API reference).
* 2. A forest "alive band" with organic radial-glow gradients in
* emerald + lavender. Inside the band lives the feature grid of
* subsystem entry points (each card jumps into a specific doc).
* 3. A direct path into the ADR list for readers who came here looking
* for design decisions, not how-to.
*
* The italic emphasis (`<em>grow</em>`) follows HANDOFF.md "The italic
* accent rule": one italic word per headline, max two. It is emphasis
* not decoration.
*/
import Link from 'next/link';
import type { ReactElement } from 'react';
import { ArrowRight, BookOpen } from 'lucide-react';

const FEATURES = [
{
Expand Down Expand Up @@ -48,33 +58,50 @@ export default function LandingPage(): ReactElement {
return (
<main className="landing">
<section className="landing__hero">
<h1 className="landing__title">GoNext Documentation</h1>
<span className="landing__eyebrow">Living documentation</span>
<h1 className="landing__title">
Docs that <em>grow</em> with you.
</h1>
<p className="landing__subtitle">
A modern, modular CMS built on Go and Next.js. Familiar mental model,
modern stack, plugin and theme ecosystems engineered for safety.
Subsystem guides, architectural decisions, and the API reference for the
GoNext platform. One coherent surface for the whole stack — Go backend,
Next.js frontend, plugin and theme ecosystems.
</p>
<Link href="/docs/00-architecture-overview" className="landing__cta">
Start with the architecture overview &rarr;
</Link>
<div className="landing__ctas">
<Link href="/docs/00-architecture-overview" className="landing__cta landing__cta--primary">
<BookOpen className="landing__cta-icon" aria-hidden="true" />
Read the docs
</Link>
<Link href="/api" className="landing__cta landing__cta--secondary">
API reference
<ArrowRight className="landing__cta-icon" aria-hidden="true" />
</Link>
</div>
</section>

<section aria-label="Documentation areas">
<section className="landing__band" aria-label="Documentation areas">
<div className="landing__band-eyebrow">By subsystem</div>
<h2 className="landing__band-title">
One product for everything you used <em>five</em> for.
</h2>
<p className="landing__band-sub">
Each guide is self-contained — the architecture overview is the entry
point, but you can drop into any subsystem on its own.
</p>
<div className="feature-grid">
{FEATURES.map((f) => (
<Link key={f.href} href={f.href} className="feature-card">
<h2 className="feature-card__title">{f.title}</h2>
<h3 className="feature-card__title">{f.title}</h3>
<p className="feature-card__body">{f.body}</p>
</Link>
))}
</div>
</section>

<section style={{ marginTop: '48px', textAlign: 'center' }}>
<p style={{ color: 'var(--color-text-muted)', fontSize: '14px' }}>
Looking for design decisions? Read the{' '}
<Link href="/adr">Architecture Decision Records</Link>.
</p>
</section>
<p className="landing__footnote">
Looking for design decisions? Read the{' '}
<Link href="/adr">Architecture Decision Records</Link>.
</p>
</main>
);
}
5 changes: 5 additions & 0 deletions apps/docs/components/Callout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,11 @@
* Used by first-party MDX pages. Filesystem-sourced markdown gets folded
* into <blockquote> by the renderer; we may later promote `> [!NOTE]`
* patterns into this component, but for now the two layers stay separate.
*
* Visual: emerald-soft tint for note/tip, warning-soft for warning,
* danger-soft for danger. Each carries a circular ink-on-cream icon
* (Archivo glyph) with an uppercase emerald-deep title. Mirrors the
* .callout treatment in docs/design/ui_kits/docs/index.html.
*/
import type { ReactElement, ReactNode } from 'react';

Expand Down
30 changes: 20 additions & 10 deletions apps/docs/components/SearchBar.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,19 @@
* no body text) so we don't have to ship the entire docs corpus to the
* client. Fuse.js handles the fuzzy match; results land in a small dropdown.
*
* Visual: paper-3 pill with a leading Lucide `search` icon and a trailing
* ⌘K keycap. On focus the field swaps to paper background with an
* emerald border + emerald-tinted focus halo (`--sh-focus`). Matches
* the .search-mini pill in docs/design/ui_kits/docs/index.html.
*
* No analytics, no remote calls — this is a deliberate non-feature: the
* search index is part of the static bundle and works offline.
*/
'use client';

import Link from 'next/link';
import Fuse from 'fuse.js';
import { Search } from 'lucide-react';
import { useMemo, useState, type ReactElement } from 'react';
import type { SearchEntry } from '@/lib/content';

Expand All @@ -37,16 +43,20 @@ export function SearchBar({ entries }: { entries: SearchEntry[] }): ReactElement

return (
<div className="search-bar" role="search">
<input
type="search"
placeholder="Search docs..."
className="search-bar__input"
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setOpen(true)}
onBlur={() => setTimeout(() => setOpen(false), 150)}
aria-label="Search documentation"
/>
<div className="search-bar__field">
<Search className="search-bar__icon" aria-hidden="true" />
<input
type="search"
placeholder="Search docs…"
className="search-bar__input"
value={query}
onChange={(e) => setQuery(e.target.value)}
onFocus={() => setOpen(true)}
onBlur={() => setTimeout(() => setOpen(false), 150)}
aria-label="Search documentation"
/>
<span className="search-bar__kbd" aria-hidden="true">⌘K</span>
</div>
{open && results.length > 0 && (
<ul className="search-bar__results" role="listbox">
{results.map((r) => {
Expand Down
4 changes: 4 additions & 0 deletions apps/docs/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@
"@next/mdx": "^15.0.0",
"fuse.js": "^7.0.0",
"gray-matter": "^4.0.3",
"lucide-react": "^0.469.0",
"next": "^15.0.0",
"react": "^19.0.0",
"react-dom": "^19.0.0",
Expand All @@ -38,9 +39,12 @@
"@types/react": "^19.0.0",
"@types/react-dom": "^19.0.0",
"@vitest/coverage-v8": "^1.6.0",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.0",
"eslint-config-next": "^15.0.0",
"jsdom": "^24.0.0",
"postcss": "^8.4.49",
"tailwindcss": "^3.4.17",
"typescript": "^5.6.0",
"vitest": "^1.6.0"
}
Expand Down
17 changes: 17 additions & 0 deletions apps/docs/postcss.config.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* PostCSS config — required for Next.js to pick up Tailwind v3.
*
* Mirrors `apps/admin/postcss.config.mjs`. Next.js auto-detects this
* file at the app root and runs the listed plugins over every imported
* `.css` file. We import the global stylesheet (which lives at
* `styles/docs.css`, kept under that name for backwards-compatible
* import paths) from the root layout; without the postcss config the
* `@tailwind` directives at the top of that file would be left as
* literal text.
*/
export default {
plugins: {
tailwindcss: {},
autoprefixer: {},
},
};
Binary file added apps/docs/public/favicon.ico
Binary file not shown.
4 changes: 4 additions & 0 deletions apps/docs/public/favicon.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
6 changes: 6 additions & 0 deletions apps/docs/public/logo-mark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
7 changes: 7 additions & 0 deletions apps/docs/public/logo-wordmark.svg
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Loading