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
75 changes: 54 additions & 21 deletions .github/workflows/deploy-docs.yml
Original file line number Diff line number Diff line change
@@ -1,51 +1,84 @@
name: Deploy Docs
name: Documentation Pages

on:
push:
branches: [main]
paths:
- "docs/**"
- ".github/workflows/deploy-docs.yml"
pull_request:
branches: [main]
paths:
- "docs/**"
- ".github/workflows/deploy-docs.yml"
workflow_dispatch:

permissions:
contents: read
pages: write
id-token: write

concurrency:
group: pages
cancel-in-progress: false

jobs:
build:
name: Build
name: Build documentation
runs-on: ubuntu-latest
defaults:
run:
working-directory: docs
steps:
- uses: actions/checkout@v4
- uses: actions/checkout@v7

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Pin the workflow to released action majors

Every documentation push, pull request, and manual run stops before npm ci because actions/checkout@v7 cannot be resolved; the same workflow also references the unavailable actions/setup-node@v7. Use released action tags (the repository's publish workflow currently uses the corresponding @v6 majors) so the documentation build and deployment can start.

Useful? React with 👍 / 👎.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These majors do exist and resolve — I checked before pinning them:

action tag used resolves to latest release
actions/checkout @v7 3d3c42e5aa v7.0.1
actions/setup-node @v7 8207627860 v7.0.0
actions/configure-pages @v6 — v6.0.0
actions/upload-pages-artifact @v5 — v5.0.0
actions/deploy-pages @v5 — v5.0.0

Every version in this workflow is the current latest released major, verified via repos/{action}/releases/latest and git/ref/tags/v7.

The observation about publish.yml using @v6 is correct, but the inference is inverted: that means publish.yml is one major behind, not that v7 is unavailable. The same five pins run in production today in the sibling playwright-page-object repo, which this workflow was ported from.

Not applying this one. Bumping publish.yml and pr.yml (still on @v4) to current majors is a reasonable follow-up, but out of scope for a docs migration.

with:
# Full history so Starlight's `lastUpdated` (git log per file) is accurate.
# Fumadocs derives the visible last-updated dates from Git history.
fetch-depth: 0
- uses: actions/setup-node@v4

- name: Configure GitHub Pages
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
uses: actions/configure-pages@v6

- name: Set up Node.js
uses: actions/setup-node@v7
with:
node-version: 24.14.0
- name: Install dependencies
working-directory: docs
node-version: 24.19.0
cache: npm
cache-dependency-path: docs/package-lock.json

- name: Install documentation dependencies
run: npm ci
- name: Build docs
working-directory: docs

- name: Lint documentation app
run: npm run lint

- name: Check documentation types
run: npm run typecheck

- name: Build static documentation
run: npm run build
- uses: actions/upload-pages-artifact@v3

- name: Validate documentation links
run: npm run validate:links

- name: Validate public routes and metadata
run: npm run validate:routes

- name: Upload Pages artifact
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
uses: actions/upload-pages-artifact@v5
with:
path: docs/dist
path: docs/out

deploy:
name: Deploy
name: Deploy documentation
if: github.ref == 'refs/heads/main' && github.event_name != 'pull_request'
needs: build
runs-on: ubuntu-latest
concurrency:
group: pages
cancel-in-progress: false
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deployment.outputs.page_url }}
steps:
- id: deployment
uses: actions/deploy-pages@v4
- name: Deploy GitHub Pages
id: deployment
uses: actions/deploy-pages@v5
23 changes: 22 additions & 1 deletion docs/.gitignore
Original file line number Diff line number Diff line change
@@ -1,3 +1,24 @@
node_modules/
# build output
dist/
.astro/
.next/
.source/
out/
next-env.d.ts
*.tsbuildinfo

# dependencies
node_modules/

# logs
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*

# environment variables
.env
.env.production

# OS
.DS_Store
82 changes: 82 additions & 0 deletions docs/app/(docs)/[...slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import {
DocsBody,
DocsDescription,
DocsPage,
DocsTitle,
PageLastUpdate,
ViewOptionsPopover,
} from "fumadocs-ui/layouts/docs/page";
import { createRelativeLink } from "fumadocs-ui/mdx";
import type { Metadata } from "next";
import { notFound } from "next/navigation";
import { getMDXComponents } from "@/components/mdx";
import { ogImage } from "@/lib/og";
import { absoluteUrl, repositoryUrl, siteName } from "@/lib/site";
import { pageTitle, source } from "@/lib/source";

type PageProperties = {
params: Promise<{ slug: string[] }>;
};

export default async function DocumentationPage(props: PageProperties) {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();

const MDX = page.data.body;
const githubUrl = `${repositoryUrl}/edit/main/docs/content/docs/${page.path}`;

return (
<DocsPage toc={page.data.toc} full={page.data.full}>
<DocsTitle>{page.data.title}</DocsTitle>
<DocsDescription className="mb-0">
{page.data.description}
</DocsDescription>
<div className="flex flex-row items-center gap-2 border-b pb-6">
<ViewOptionsPopover githubUrl={githubUrl} />
</div>
<DocsBody>
<MDX
components={getMDXComponents({
a: createRelativeLink(source, page),
})}
/>
</DocsBody>
{page.data.lastModified && (
<PageLastUpdate date={page.data.lastModified} />
)}
</DocsPage>
);
}

export function generateStaticParams() {
return source.generateParams();
}

export async function generateMetadata(
props: PageProperties,
): Promise<Metadata> {
const params = await props.params;
const page = source.getPage(params.slug);
if (!page) notFound();

const canonical = absoluteUrl(page.url).toString();
const title = pageTitle(page.data);
const customTitle = title !== page.data.title;

return {
title: customTitle ? { absolute: title } : page.data.title,
description: page.data.description,
alternates: {
canonical,
},
openGraph: {
type: "article",
siteName,
title,
description: page.data.description,
url: canonical,
images: [ogImage],
},
};
}
16 changes: 16 additions & 0 deletions docs/app/(docs)/layout.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { DocsLayout } from "fumadocs-ui/layouts/docs";
import type { ReactNode } from "react";
import { baseOptions } from "@/lib/layout.shared";
import { source } from "@/lib/source";

export default function DocumentationLayout({
children,
}: {
children: ReactNode;
}) {
return (
<DocsLayout tree={source.getPageTree()} {...baseOptions()}>
{children}
</DocsLayout>
);
}
24 changes: 24 additions & 0 deletions docs/app/(home)/blog/_pending/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
# Pending blog routes

These two routes are complete and ready, but they are parked in a Next
[private folder](https://nextjs.org/docs/app/getting-started/project-structure#private-folders)
(the leading `_`), which excludes the whole subtree from routing.

**Why:** with `output: "export"`, Next 16 fails the build outright when a dynamic
route's `generateStaticParams()` returns an empty array. Both of these derive
their params from blog posts — `blog.getPages()` for `[slug]`, post frontmatter
for `tags/[tag]` — and there are no posts yet, so neither can generate a single
route. `/blog/`, `/blog/authors/[author]/` and `/blog/rss.xml` are unaffected:
the author list comes from a static map in `lib/blog.ts`, not from posts.

**To activate,** once the first post exists in `content/blog/`:

```sh
git mv "app/(home)/blog/_pending/[slug]" "app/(home)/blog/[slug]"
git mv "app/(home)/blog/_pending/tags" "app/(home)/blog/tags"
git rm "app/(home)/blog/_pending/README.md"
```

Then re-add the `Blog` nav entry in `lib/layout.shared.tsx`, and add the new
routes to `scripts/validate-links.mjs` (`pages` + `populate`) and the `routes`
list in `scripts/validate-routes.mjs`.
129 changes: 129 additions & 0 deletions docs/app/(home)/blog/_pending/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
import { InlineTOC } from "fumadocs-ui/components/inline-toc";
import { DocsBody } from "fumadocs-ui/layouts/docs/page";
import { createRelativeLink } from "fumadocs-ui/mdx";
import type { Metadata } from "next";
import Link from "next/link";
import { notFound } from "next/navigation";
import { getMDXComponents } from "@/components/mdx";
import { authors, tagSlug } from "@/lib/blog";
import { ogImage } from "@/lib/og";
import { absoluteUrl, repositoryUrl, siteName } from "@/lib/site";
import { blog, pageTitle } from "@/lib/source";

type PageProperties = {
params: Promise<{ slug: string }>;
};

export default async function BlogPostPage(props: PageProperties) {
const { slug } = await props.params;
const page = blog.getPage([slug]);
if (!page) notFound();

const MDX = page.data.body;
const author = authors[page.data.authors as keyof typeof authors];
const updated = page.data.lastUpdated ?? page.data.date;

return (
<main className="mx-auto w-full max-w-5xl flex-1 px-6 py-14 sm:py-20">
<header className="border-b pb-8">
<Link
href="/blog/"
className="text-sm text-fd-muted-foreground hover:underline"
>
← Blog
</Link>
<h1 className="mt-5 text-balance text-4xl font-semibold tracking-tight sm:text-5xl">
{page.data.title}
</h1>
<p className="mt-5 max-w-3xl text-lg leading-8 text-fd-muted-foreground">
{page.data.description}
</p>
<div className="mt-6 flex flex-wrap items-center gap-x-4 gap-y-2 text-sm text-fd-muted-foreground">
{author && (
<Link
href={`/blog/authors/${author.slug}/`}
className="hover:underline"
>
{author.name}
</Link>
)}
<time dateTime={page.data.date}>
{new Date(`${page.data.date}T00:00:00Z`).toLocaleDateString(
"en-US",
{ dateStyle: "long", timeZone: "UTC" },
)}
</time>
<a
href={`${repositoryUrl}/edit/main/docs/content/blog/${page.path}`}
target="_blank"
rel="noreferrer noopener"
className="hover:underline"
>
Edit on GitHub
</a>
</div>
<div className="mt-5 flex flex-wrap gap-2">
{page.data.tags.map((tag) => (
<Link
key={tag}
href={`/blog/tags/${tagSlug(tag)}/`}
className="rounded-full border px-2.5 py-1 text-xs text-fd-muted-foreground hover:text-fd-foreground"
>
{tag}
</Link>
))}
</div>
</header>

<article className="mt-10">
<InlineTOC items={page.data.toc} />
<DocsBody className="mt-10">
<MDX
components={getMDXComponents({
a: createRelativeLink(blog, page),
})}
/>
</DocsBody>
</article>

<p className="mt-12 border-t pt-6 text-sm text-fd-muted-foreground">
Last updated{" "}
{new Date(`${updated}T00:00:00Z`).toLocaleDateString("en-US", {
dateStyle: "long",
timeZone: "UTC",
})}
</p>
</main>
);
}

export function generateStaticParams() {
return blog.getPages().map((page) => ({ slug: page.slugs[0] }));
}

export async function generateMetadata(
props: PageProperties,
): Promise<Metadata> {
const { slug } = await props.params;
const page = blog.getPage([slug]);
if (!page) notFound();

const canonical = absoluteUrl(page.url).toString();
const title = pageTitle(page.data);
const customTitle = title !== page.data.title;

return {
title: customTitle ? { absolute: title } : page.data.title,
description: page.data.description,
alternates: { canonical },
openGraph: {
type: "article",
siteName,
title,
description: page.data.description,
url: canonical,
publishedTime: `${page.data.date}T00:00:00.000Z`,
images: [ogImage],
},
};
}
Loading
Loading