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
12 changes: 11 additions & 1 deletion frontmatter.json
Original file line number Diff line number Diff line change
Expand Up @@ -71,6 +71,16 @@
"title": "navOrder",
"name": "navOrder",
"type": "number"
},
{
"title": "area",
"name": "area",
"type": "choice",
"choices": [
"docs",
"api"
],
"required": false
}
]
}
Expand Down Expand Up @@ -132,4 +142,4 @@
]
}
}
}
}
7 changes: 4 additions & 3 deletions src/components/ApiNavigation.astro
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,15 @@ stats.start();

type Props = {
lang: string;
headings: { depth: number; slug: string; text: string }[];
// Left off by a layout that lists this page's headings somewhere else, which
// leaves the nav as the flat list of pages with nothing under the current one.
headings?: { depth: number; slug: string; text: string }[];
// One entry per endpoint, in heading order, left on the frontmatter by
// plugins/satteri-api-examples.js from the `:endpoint` directive under each
// heading. Sections without one — and pages that never reach that plugin —
// simply have none.
apiMethods?:
| { text: string; method: string | null; deprecated?: boolean }[]
| null;
{ text: string; method: string | null; deprecated?: boolean }[] | null;
};
const { lang, headings, apiMethods } = Astro.props satisfies Props;

Expand Down
38 changes: 38 additions & 0 deletions src/components/AreaNavigation.astro
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
---
import type { Area } from '@lib/areas';

// The nav for the docs area
import Navigation from '@components/Navigation.astro';
// The nav for the API reference
import ApiNavigation from './ApiNavigation.astro';

// The area's nav, picked by area rather than by layout, so a page can be laid
// out one way and navigated another. Every layout renders this and none of them
// names a nav component.
//
// Adding an area means adding a line at the bottom of this file, and the rest
// of the checklist is in lib/areas.ts - this is step 3 of four.
type Props = {
area: Area;
lang: string;
// The API nav lists the current page's endpoints under it, and reads both of
// these to do it. A layout that shows this page's headings elsewhere — the
// table of contents in the side column — leaves them off, and the nav is
// then the flat page list on its own.
headings?: { depth: number; slug: string; text: string }[];
apiMethods?: { text: string; method: string | null }[] | null;
};
const { area, lang, headings, apiMethods } = Astro.props satisfies Props;
---

{
/* One line per area, each handed only the props its own nav takes. An area
with no line here renders no nav at all, which is louder than quietly showing
it the wrong one. */
}
{area === 'docs' && <Navigation lang={lang} />}
{
area === 'api' && (
<ApiNavigation lang={lang} headings={headings} apiMethods={apiMethods} />
)
}
25 changes: 17 additions & 8 deletions src/layouts/Api.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import { accelerator } from '@lib/accelerator';
import { PostFiltering } from 'astro-accelerator-utils';
import type { Frontmatter as OriginalFrontmatter } from 'astro-accelerator-utils/types/Frontmatter';
import { SITE } from '@config';
import { buildApiCrumbs } from '@lib/apiNavigation';
import { buildAreaCrumbs, resolveArea, type Area } from '@lib/areas';
import type { Crumb } from '@util/breadcrumbs';

// Theme components
Expand All @@ -14,7 +14,7 @@ import Authors from '@components/Authors.astro';
import Taxonomy from '@components/Taxonomy.astro';

// Custom components
import ApiNavigation from '../components/ApiNavigation.astro';
import AreaNavigation from '../components/AreaNavigation.astro';
import ArticleHeader from '../components/ArticleHeader.astro';
import Feedback from '../components/Feedback.astro';
import TopNav from '../components/TopNav.astro';
Expand All @@ -23,19 +23,23 @@ import Footer from 'src/components/Footer.astro';
import DocsSearch from '../components/DocsSearch.astro';

type Props = {
// `apiMethods` is not written by hand: plugins/satteri-api-examples.js leaves
// it on the frontmatter for the left nav, an entry per endpoint.
frontmatter: OriginalFrontmatter & {
// The nav this page appears in. Optional, and normally left alone: these
// pages live under /docs/api, which is the API area already.
area?: Area;
// `apiMethods` is not written by hand: plugins/satteri-api-examples.js
// leaves it on the frontmatter for the left nav, an entry per endpoint.
apiMethods?: { text: string; method: string | null }[];
};
headings: { depth: number; slug: string; text: string }[];
breadcrumbs?: Crumb[] | null;
};
const { frontmatter, headings, breadcrumbs } = Astro.props satisfies Props;

// buildApiCrumbs, not buildCrumbs: the section has no page of its own at
// /docs/api for the generic walk to find, so it splices the crumb in.
const crumbs = buildApiCrumbs(Astro.url, breadcrumbs);
const area = resolveArea(frontmatter.area, Astro.url.pathname);
// The API section has no page of its own at /docs/api for the generic crumb
// walk to find, so buildAreaCrumbs splices the crumb in for it.
const crumbs = buildAreaCrumbs(Astro.url, area, breadcrumbs);

const lang = frontmatter.lang ?? SITE.default.lang;
const textDirection = frontmatter.dir ?? SITE.default.dir;
Expand Down Expand Up @@ -100,7 +104,12 @@ const lastUpdated = frontmatter.modDate ?? frontmatter.pubDate ?? null;
</article>
<Feedback frontmatter={frontmatter} lang={lang} />
</main>
<ApiNavigation
{
/* The nav belongs to the area, not to this layout. This column is the
only nav on the page, so the headings go with it. */
}
<AreaNavigation
area={area}
headings={headings}
lang={lang}
apiMethods={frontmatter.apiMethods}
Expand Down
19 changes: 14 additions & 5 deletions src/layouts/Default.astro
Original file line number Diff line number Diff line change
Expand Up @@ -3,17 +3,18 @@ import { accelerator } from '@lib/accelerator';
import { PostFiltering } from 'astro-accelerator-utils';
import type { Frontmatter as OriginalFrontmatter } from 'astro-accelerator-utils/types/Frontmatter';
import { SITE } from '@config';
import { buildCrumbs, type Crumb } from '@util/breadcrumbs';
import type { Crumb } from '@util/breadcrumbs';
import { buildAreaCrumbs, resolveArea, type Area } from '@lib/areas';

// Theme components
import Head from '@components/HtmlHead.astro';
import SkipLinks from '@components/SkipLinks.astro';
import Breadcrumbs from '@components/Breadcrumbs.astro';
import Navigation from '@components/Navigation.astro';
import Authors from '@components/Authors.astro';
import Taxonomy from '@components/Taxonomy.astro';

// Custom components
import AreaNavigation from '../components/AreaNavigation.astro';
import ArticleHeader from '../components/ArticleHeader.astro';
import ArticleNav from '../components/ArticleNav.astro';
import Feedback from '../components/Feedback.astro';
Expand All @@ -27,13 +28,17 @@ import Footer from 'src/components/Footer.astro';
import DocsSearch from '../components/DocsSearch.astro';

type Props = {
frontmatter: OriginalFrontmatter;
// `area` is the nav this page appears in, and it is optional: the section the
// page lives in supplies it (see lib/areas.ts). It is here for the page that
// wants this layout's middle column while belonging to another area's nav.
frontmatter: OriginalFrontmatter & { area?: Area };
headings: { depth: number; slug: string; text: string }[];
breadcrumbs?: Crumb[] | null;
};
const { frontmatter, headings, breadcrumbs } = Astro.props satisfies Props;

const crumbs = buildCrumbs(Astro.url, breadcrumbs);
const area = resolveArea(frontmatter.area, Astro.url.pathname);
const crumbs = buildAreaCrumbs(Astro.url, area, breadcrumbs);

const lang = frontmatter.lang ?? SITE.default.lang;
const textDirection = frontmatter.dir ?? SITE.default.dir;
Expand Down Expand Up @@ -99,7 +104,11 @@ const lastUpdated = frontmatter.modDate ?? frontmatter.pubDate ?? null;
</div>
</article>
</main>
<Navigation lang={lang} />
{
/* The nav belongs to the area, not to this layout. The page's own
headings are not passed: the side column below lists them. */
}
<AreaNavigation area={area} lang={lang} />
<div class="side-nav">
<!-- <TableOfContents
expanded={930}
Expand Down
67 changes: 14 additions & 53 deletions src/lib/apiNavigation.ts
Original file line number Diff line number Diff line change
@@ -1,23 +1,19 @@
import type { MarkdownInstance } from 'astro-accelerator-utils/types/Astro';
import { accelerator } from './accelerator';
import { SITE } from '@config';
import { buildCrumbs, type Crumb } from '@util/breadcrumbs';
import { pageArea } from './areas';

// The API reference carries its own left nav, built here from the pages that
// opt into src/layouts/Api.astro. Those same pages are pruned out of the main
// site nav in navigationTree.ts, so the two trees never overlap.
// The API reference carries its own left nav, built here from the pages in the
// API area (see lib/areas.ts — the section they live in, not the layout they
// render with). Those same pages are pruned out of the main site nav in
// navigationTree.ts, so the two trees never overlap.
//
// Unlike the site nav, this one is flat: an entry per page, and under the page
// the reader is on, an entry per endpoint. The endpoints come from the layout's
// own headings rather than from here — Posts.all() reads the page set back from
// a JSON cache, which leaves the frontmatter intact but drops getHeadings().

const API_LAYOUT = '/Api.astro';

const PAGES_ROOT = '/src/pages';

const API_SECTION_TITLE = 'Api';

function urlFromSourcePath(path: string): string {
return path
.slice(PAGES_ROOT.length)
Expand Down Expand Up @@ -57,8 +53,8 @@ export type ApiNavPage = {
order: number;
};

export function isApiPage(post: MarkdownInstance): boolean {
return (post?.frontmatter?.layout ?? '').includes(API_LAYOUT);
export function isApiPage(post: MarkdownInstance, urlHint?: string): boolean {
return pageArea(post, urlHint) === 'api';
}

// Same deal as menuTemplate(): the list is identical for every page in the
Expand All @@ -74,54 +70,19 @@ export function apiMenu(): ApiNavPage[] {

const menu = accelerator.posts
.all()
.filter(isApiPage)
.map((post) => ({
// The recovered url is what decides the area for a page Astro gave none, so
// it is worked out before the filter rather than inside the map.
.map((post) => ({ post, url: post.url ?? urls.get(post.file) ?? '/' }))
.filter(({ post, url }) => isApiPage(post, url))
.map(({ post, url }) => ({
title: post.frontmatter.navTitle ?? post.frontmatter.title,
url: accelerator.urlFormatter.addSlashToAddress(
post.url ?? urls.get(post.file) ?? '/'
),
url: accelerator.urlFormatter.addSlashToAddress(url),
order: post.frontmatter.navOrder ?? Number.MAX_SAFE_INTEGER,
}))
// The pages are generated one per API area and carry no navOrder, so the
// The pages are generated one per API resource and carry no navOrder, so the
// fallback is the alphabetical order the index page already lists them in.
.sort((a, b) => a.order - b.order || a.title.localeCompare(b.title));

if (import.meta.env.PROD) pages = menu;
return menu;
}

/**
* Every API page URL, trailing slash trimmed, for the site nav to prune
* against.
*/
export function apiPageUrls(): Set<string> {
return new Set(apiMenu().map((page) => page.url.replace(/\/$/, '')));
}

// The API reference has no page of its own at /docs/api yet - `_index.md` is
// underscore-prefixed so Astro does not route it - so the generic breadcrumb
// walk, which builds a crumb per path segment that resolves to a page, skips
// straight from Docs to the endpoint page. Splice the section in by hand so an
// API page reads "Docs / Api / Feeds".
//
// The crumb carries no url on purpose: there is nothing to link to until the
// landing page lands.
export function buildApiCrumbs(
currentUrl: URL,
extraCrumbs?: ReadonlyArray<Crumb> | null
): Crumb[] {
const crumbs = buildCrumbs(currentUrl, extraCrumbs);
const sectionPath = SITE.subfolder + '/api';

if (!currentUrl.pathname.startsWith(sectionPath)) return crumbs;
if (crumbs.some((crumb) => crumb.title === API_SECTION_TITLE)) return crumbs;

// After the /docs crumb, which is the only one the walk finds above us.
const insertAt = crumbs.findIndex(
(crumb) => crumb.url.replace(/\/$/, '') === SITE.subfolder
);

const section: Crumb = { url: '', title: API_SECTION_TITLE };
crumbs.splice(insertAt + 1, 0, section);
return crumbs;
}
Loading