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
2 changes: 2 additions & 0 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -128,6 +128,8 @@ The community-tier signup page (`/community`) reuses `LanguageSwitcher.astro` an
| `/docs/examples` | Code snippets (Python, Node.js, curl) |
| `/docs/api` | OpenAI-compatible API reference (endpoints, auth, rate limits) |

Sidebar navigation comes from each guide's frontmatter (`src/content.config.ts`): `group` picks the section, `order` the position, and an optional `parent` (the entry id of another guide, identical in EN and ES) lists the page as an indented sub-item right under that guide, in the parent's group. The library supports any depth, but content is kept to one level (enforced in `DocsShell.test.ts`), and a child's `order` must be the slot right after its parent so the flat order (`/api/docs` manifest) matches the sidebar. URLs stay flat (`/docs/<id>`); the breadcrumb and prev/next follow the sidebar's visual order. Logic and tests: `src/lib/docsNav.ts`; markup: `src/components/docs/DocsSidebarNav.astro` + `DocsNavList.astro` (rendered in tests with the Astro Container API, `*.render.test.ts`, see `vitest.render.config.ts`).

### API Endpoints

#### `POST /api/waitlist`
Expand Down
45 changes: 45 additions & 0 deletions src/components/docs/DocsNavList.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
---
/**
* One level of the docs sidebar: a list of pages, each with the pages nested
* under it (frontmatter `parent`) rendered as an indented sub-list right below.
*
* It recurses through Astro.self, so nesting depth is whatever the data says.
* Sub-items are always shown, not behind a toggle: with a handful of children
* a disclosure button would add a click and a focus stop for no gain, and a
* plain nested <ul> is what screen readers announce as "list, level 2".
*
* `here` is the current path. The current page gets `aria-current="page"`; the
* pages above it (its parent chain) get `is-trail`, so the reader sees where
* the open page sits in the tree.
*/
import { isActiveDocPath, isActiveDocTrail, type DocsNavNode } from '../../lib/docsNav';

interface Props {
items: DocsNavNode[];
here: string;
depth?: number;
}

const { items, here, depth = 0 } = Astro.props;
---

<ul class:list={[{ 'docs-nav-sub': depth > 0 }]} data-depth={depth}>
{
items.map((item) => {
const active = isActiveDocPath(here, item.slug);
const trail = !active && isActiveDocTrail(here, item);
return (
<li class:list={[{ 'has-children': item.children.length > 0 }]}>
<a
href={item.slug}
class:list={['docs-nav-link', { 'is-active': active, 'is-trail': trail }]}
aria-current={active ? 'page' : undefined}
>
{item.label}
</a>
{item.children.length > 0 && <Astro.self items={item.children} here={here} depth={depth + 1} />}
</li>
);
})
}
</ul>
27 changes: 27 additions & 0 deletions src/components/docs/DocsSidebarNav.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
---
/**
* The docs sidebar's page list: one block per group, each a (possibly nested)
* list of pages. Split out of Docs.astro so it can be rendered on its own in
* tests; the layout itself drags in the content collections and Cloudflare.
*/
import DocsNavList from './DocsNavList.astro';
import type { DocsNavGroup } from '../../lib/docsNav';

interface Props {
groups: DocsNavGroup[];
here: string;
}

const { groups, here } = Astro.props;
---

<nav class="docs-nav" aria-label="Docs sections">
{
groups.map((g) => (
<div class="docs-nav-group">
<p class="docs-nav-label">{g.group}</p>
<DocsNavList items={g.items} here={here} />
</div>
))
}
</nav>
11 changes: 11 additions & 0 deletions src/content.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,17 @@ const docsSchema = z.object({
* second list to maintain either.
*/
group: z.string().default('Guides'),
/*
* The entry id of the page this one is listed under in the sidebar (e.g.
* `automations`). The page then shows as an indented sub-item right below
* its parent, inside the parent's group, and prev/next follow that order.
* Its URL stays flat (/docs/<id>): nesting is a nav concern only, so links
* and the /api/docs manifest are unaffected. Same value in both languages.
*/
parent: z
.string()
.regex(/^[a-z0-9-]+$/)
.optional(),
locale: z.string().default('es'),
});

Expand Down
3 changes: 2 additions & 1 deletion src/content/docs-es/pr-gatekeeper.mdx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
title: PR gatekeeper
description: Monta, paso a paso, una automatización que comprueba cada pull request contra el mínimo de tu equipo y deja el veredicto en un solo comentario.
order: 22
order: 21
group: Guías
parent: automations
---

import Details from '../../components/docs/Details.astro';
Expand Down
2 changes: 1 addition & 1 deletion src/content/docs-es/webhooks.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Webhooks y programaciones
description: Lanza tus automatizaciones desde GitHub, Stripe o tus propios sistemas con webhooks firmados, o con una programación.
order: 21
order: 22
group: Guías
---

Expand Down
3 changes: 2 additions & 1 deletion src/content/docs/pr-gatekeeper.mdx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
---
title: PR gatekeeper
description: Build, step by step, an automation that checks every pull request against your team's minimum bar and keeps the verdict in one comment.
order: 22
order: 21
group: Guides
parent: automations
---

import Details from '../../components/docs/Details.astro';
Expand Down
2 changes: 1 addition & 1 deletion src/content/docs/webhooks.mdx
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
---
title: Webhooks and schedules
description: Start your automations from GitHub, Stripe or your own systems with signed webhooks, or on a schedule.
order: 21
order: 22
group: Guides
---

Expand Down
63 changes: 28 additions & 35 deletions src/layouts/Docs.astro
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,15 @@ import '@fontsource/jetbrains-mono/700.css';

import { getCollection, render } from 'astro:content';
import DocsTopBar from '../components/docs/DocsTopBar.astro';
import DocsSidebarNav from '../components/docs/DocsSidebarNav.astro';
import { API_DOC_META, API_DOC_SLUG } from '../lib/apiDoc';
import spec from '../data/openapi.json';
import {
apiSearchHeadings,
docSlug,
docsNavAncestors,
docsNavItemFromEntry,
flattenDocsNav,
groupDocsNav,
isActiveDocPath,
type DocsNavItem,
Expand Down Expand Up @@ -75,13 +80,7 @@ const entries = await getCollection(lang === 'es' ? 'docsEs' : 'docs');
* so the page and the manifest cannot disagree.
*/
const navItems: DocsNavItem[] = [
...entries.map((entry) => ({
slug: entry.id === 'intro' ? `${pfx}/docs` : `${pfx}/docs/${entry.id}`,
label: entry.data.title,
order: entry.data.order,
group: entry.data.group,
description: entry.data.description,
})),
...entries.map((entry) => docsNavItemFromEntry(entry, pfx)),
{
slug: `${pfx}/docs/${API_DOC_SLUG}`,
label: API_DOC_META.title,
Expand All @@ -92,6 +91,9 @@ const navItems: DocsNavItem[] = [
].sort((a, b) => a.order - b.order);

const navGroups = groupDocsNav(navItems);
// The sidebar's visual order (children right after their parent), which is
// what prev/next must follow; the raw `order` alone would not.
const navOrder = flattenDocsNav(navGroups);

const enPath = Astro.url.pathname.replace(/^\/es(?=\/|$)/, '') || '/docs';
const esPath = `/es${enPath}`;
Expand All @@ -104,7 +106,7 @@ const isActive = (slug: string) => isActiveDocPath(here, slug);
* its structure dropped them; they are restored because under /docs they are
* the only clue to where you are when you arrive from a search engine.
*/
const breadcrumb = enPath
const pathCrumbs = enPath
.split('/')
.filter(Boolean)
.map((part, i, parts) => {
Expand All @@ -118,11 +120,20 @@ const breadcrumb = enPath
isCurrent: path === here,
};
});

const currentIndex = navItems.findIndex((item) => isActive(item.slug));
const prevPage = currentIndex > 0 ? navItems[currentIndex - 1] : null;
// A page nested in the nav (frontmatter `parent`) has a flat URL, so its
// parents are not in the path: they are slotted in before the current crumb so
// the breadcrumb tells the same story as the sidebar.
const parentCrumbs = docsNavAncestors(navItems, here).map((p) => ({
label: p.label,
path: p.slug,
isCurrent: false,
}));
const breadcrumb = [...pathCrumbs.slice(0, -1), ...parentCrumbs, ...pathCrumbs.slice(-1)];

const currentIndex = navOrder.findIndex((item) => isActive(item.slug));
const prevPage = currentIndex > 0 ? navOrder[currentIndex - 1] : null;
const nextPage =
currentIndex >= 0 && currentIndex < navItems.length - 1 ? navItems[currentIndex + 1] : null;
currentIndex >= 0 && currentIndex < navOrder.length - 1 ? navOrder[currentIndex + 1] : null;

/*
* Search index.
Expand All @@ -137,7 +148,7 @@ const collectionIndex = await Promise.all(
const { headings } = await render(entry);
return {
title: entry.data.title,
href: entry.id === 'intro' ? `${pfx}/docs` : `${pfx}/docs/${entry.id}`,
href: docSlug(entry.id, pfx),
description: entry.data.description,
headings: headings
.filter((h) => h.depth >= 2 && h.depth <= 3)
Expand Down Expand Up @@ -235,28 +246,7 @@ const searchData = [
<div id="docs-search-results" class="ds-results" hidden></div>
</div>

<nav class="docs-nav" aria-label="Docs sections">
{
navGroups.map((g) => (
<div class="docs-nav-group">
<p class="docs-nav-label">{g.group}</p>
<ul>
{g.items.map((item) => (
<li>
<a
href={item.slug}
class:list={['docs-nav-link', { 'is-active': isActive(item.slug) }]}
aria-current={isActive(item.slug) ? 'page' : undefined}
>
{item.label}
</a>
</li>
))}
</ul>
</div>
))
}
</nav>
<DocsSidebarNav groups={navGroups} here={here} />
</aside>

<div class="docs-pane">
Expand Down Expand Up @@ -398,6 +388,9 @@ const searchData = [
backdrop.hidden = !open;
menuBtn.setAttribute('aria-expanded', String(open));
document.body.style.overflow = open ? 'hidden' : '';
// A nested or late page can sit below the fold of a short phone:
// bring the current link into view so the reader sees where they are.
if (open) side.querySelector('[aria-current="page"]')?.scrollIntoView({ block: 'center' });
};
menuBtn.addEventListener('click', () => setOpen(!side.classList.contains('open')));
backdrop.addEventListener('click', () => setOpen(false));
Expand Down
Loading
Loading