Skip to content
Draft
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
8 changes: 8 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,16 @@ jobs:
- name: 'Install dependencies'
run: bun install --frozen-lockfile

- name: 'Check content (MDX that could run code)'
run: bun run check:content

- name: 'Test the content check'
run: bun run test:content

- name: 'Typecheck'
run: bun run typecheck

# prebuild runs check:content again (Netlify builds run it there);
# postbuild runs check:paths on out/.
- name: 'Build'
run: bun run build
81 changes: 81 additions & 0 deletions .github/workflows/preview-build.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
# Publishes this branch's built /docs as static files for the Hub's preview deployment
# (hub-preview.pelican.dev), so the preview shows the whole one-site pelican.dev: the Hub's
# pages and these docs under /docs.
#
# The branch preview/docs-under-docs holds exactly what goes in the Hub's public/docs/:
# out/docs/*, plus out/_next/ as _next/ (pages load /docs/_next/... because of assetPrefix,
# while the export writes those files to out/_next/), plus BUILD.json naming the commit.
# The Hub installs it on the preview host (App\Services\Site\DocsPreviewService); the last
# step asks the preview to check for the new build right away.
name: 'Preview build'
on:
workflow_dispatch:
push:
branches:
- 'feat/docs-under-docs'

concurrency:
group: preview-build
cancel-in-progress: true

permissions:
contents: write

jobs:
publish:
name: 'Build and publish /docs for the Hub preview'
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
uses: actions/checkout@v7

- name: 'Setup Bun'
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json

- name: 'Setup Node'
uses: actions/setup-node@v7
with:
node-version: 22

- name: 'Cache Bun packages'
uses: actions/cache@v6
with:
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

- name: 'Install dependencies'
run: bun install --frozen-lockfile

# prebuild checks the content, postbuild checks every file lands where it should.
- name: 'Build'
run: bun run build

- name: 'Assemble the /docs folder'
run: |
rm -rf preview
mkdir preview
cp -R out/docs/. preview/
cp -R out/_next preview/_next
test -f preview/index.html
test -d preview/_next/static
printf '{"sha":"%s","branch":"%s","built_at":"%s"}\n' \
"$GITHUB_SHA" "$GITHUB_REF_NAME" "$(date -u +%Y-%m-%dT%H:%M:%SZ)" > preview/BUILD.json

- name: 'Publish to preview/docs-under-docs'
working-directory: preview
env:
GH_TOKEN: ${{ github.token }}
run: |
git init -q -b preview
git config user.name 'github-actions[bot]'
git config user.email '41898282+github-actions[bot]@users.noreply.github.com'
git add -A
git commit -q -m "Docs preview build of ${GITHUB_SHA::10} (${GITHUB_REF_NAME})"
git push -q -f "https://x-access-token:${GH_TOKEN}@github.com/${GITHUB_REPOSITORY}.git" HEAD:refs/heads/preview/docs-under-docs

# Not fatal: the preview also checks on its own every 10 minutes and on the first visit.
- name: 'Ask the Hub preview to install it'
run: curl -fsS --max-time 60 https://hub-preview.pelican.dev/docs/_refresh || echo "The preview will pick it up on its next check."
6 changes: 5 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# Pelican Docs

The website and documentation at [pelican.dev](https://pelican.dev), covering [Pelican Panel](https://github.com/pelican/panel) and [Wings](https://github.com/pelican/wings).
The source for the documentation at [pelican.dev/docs](https://pelican.dev/docs), covering [Pelican Panel](https://github.com/pelican/panel) and [Wings](https://github.com/pelican/wings).

Built with [Next.js](https://nextjs.org) and [Fumadocs](https://fumadocs.dev) and served by Netlify as a static site.

## Running it locally
Expand All @@ -23,6 +24,9 @@ bun run build # the production site, written to out/
| `app/(home)/page.tsx` | The homepage. |
| `public/` | Files served as they are, including the `update-panel.sh` and `migrate-*.sh` scripts. |
| `netlify.toml` | Build settings and redirects such as `/discord`. |
| `ui/` | Design tokens (`tokens.css`) and site nav (`nav.json`) shared with the Hub; published at `/docs/ui/v1/`. |
| `router/` | `dev-server.mjs`: pelican.dev as one site locally (docs from `out/`, the rest from the Hub). See its README. |
| `scripts/check-*.mjs` | `check-content` (parses every page as MDX and fails on anything that could run code: `import`/`export`, `{expressions}`, components outside `mdx-components.tsx`, handlers, unsafe URLs; runs before the build, tested by `bun run test:content`) and `check-paths` (everything under `/docs/`, runs after it). |

## Writing a page

Expand Down
63 changes: 4 additions & 59 deletions app/(landing)/home.css
Original file line number Diff line number Diff line change
Expand Up @@ -8,37 +8,8 @@
* ====================================================================== */

.pl {
--ink: #04121c;
--sky: #38bdf8;
--sky-deep: #0b79ad;
--white: #ffffff;
--paper: #f3f7f9;
--text: #1f3644;
--on-ink-soft: #b9ccd6;
--line-on-ink: #2e4756;
--line: 3px solid var(--ink);
--pad: clamp(16px, 4cqi, 64px);
--section: clamp(72px, 9cqi, 120px);
--font-display: var(--pl-font-display), 'Helvetica Neue', sans-serif;
--font-text: var(--pl-font-text), 'Helvetica Neue', system-ui, sans-serif;
--font-code: var(--font-jetbrains), ui-monospace, Menlo, monospace;
--btn-shadow: var(--ink);
/* Theme roles. --ink and --white flip in dark mode (see "Dark theme" at the
end); these stay put or carry the role the light values had. */
--navy: #04121c; /* always navy: text on sky, chrome that is dark in both themes */
--snow: #ffffff; /* always white: text on navy */
--shadow: var(--ink); /* hard offset shadow on cards */
--muted: #33505f;
--faint: #8aa3b0;
--rule: #d5e3ea;
--rule-soft: #e3edf2;
--rule-ink: rgba(4, 18, 28, 0.12);
--chip: #e3edf2;
--hover: #e8f6fd;
--hover-strong: #cdeefd;
--on-sky-soft: #0b2a3b;
--kbd-bg: rgba(255, 255, 255, 0.5);

/* The colours, fonts and sizes (--ink, --sky, --line, --pad, ...) are in
ui/tokens.css, loaded on every page by app/global.css. */
container: page / inline-size;
flex: 1;
display: flex;
Expand Down Expand Up @@ -4556,27 +4527,7 @@ html:has(.pl-ed[open]) {
* keep their light values and only the bands around them change.
* ====================================================================== */
.dark .pl {
--ground: #04121c;
--deep: #071926;
--code: #000b12;
--ink: #ffffff;
--white: #0a1e2b;
--card: #0a1e2b; /* the surface, also inside .on-ink where --white is white again */
--paper: #071926;
--text: #d4e3ea;
--muted: #b9ccd6;
--faint: #7f99a6;
--rule: #2e4756;
--rule-soft: #1f3a4a;
--rule-ink: rgba(185, 204, 214, 0.18);
--chip: #14304a;
--hover: #0f2c3d;
--hover-strong: #123a52;
--on-sky-soft: #b9ccd6;
--kbd-bg: rgba(255, 255, 255, 0.1);
--sky-deep: #38bdf8;
--shadow: var(--sky);
--btn-shadow: var(--sky);
/* The dark tokens are in ui/tokens.css. */
background: var(--ground);
}

Expand All @@ -4585,13 +4536,7 @@ html:has(.pl-ed[open]) {
color: var(--navy);
}

/* Navy sections were already dark: give them back their light values. */
.dark .pl .on-ink {
--ink: #04121c;
--white: #ffffff;
--line: 3px solid var(--line-on-ink);
--btn-shadow: var(--snow);
}
/* Navy (.on-ink) sections get their light values back in ui/tokens.css. */

/* ---- Full-width bands: navy grounds with slate edges, no bright blocks ---- */
.dark .pl :is(.pl-features, .pl-activity, .pl-sup-faq) {
Expand Down
4 changes: 2 additions & 2 deletions app/md/[...slug]/route.ts → app/docs/md/[...slug]/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { notFound } from 'next/navigation';
import { markdownParams, pageFromMarkdownSlug, pageMarkdown } from '@/lib/llms';

// One docs page as Markdown. Builds to out/md/<page>.md; netlify.toml serves
// One docs page as Markdown. Builds to out/docs/md/<page>.md; netlify.toml serves
// it at /docs/<page>.md (the docs page route owns everything under /docs).
export const dynamic = 'force-static';
export const dynamicParams = false;
Expand All @@ -10,7 +10,7 @@ export function generateStaticParams() {
return markdownParams();
}

export async function GET(_request: Request, { params }: RouteContext<'/md/[...slug]'>) {
export async function GET(_request: Request, { params }: RouteContext<'/docs/md/[...slug]'>) {
const { slug } = await params;
const page = pageFromMarkdownSlug(slug);
if (!page) notFound();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { checkedMotif } from '@/lib/og-motifs';
import { source } from '@/lib/source';

// Share images for the docs, written into out/ at build time:
// /og/docs/image.png for /docs/ and /og/docs/<slug>/image.png for every other page.
// /docs/og/image.png for /docs/ and /docs/og/<slug>/image.png for every other page.
// The last segment is the file name, so a catch-all can stay the last part of the route.
export const dynamic = 'force-static';
export const dynamicParams = false;
Expand All @@ -32,7 +32,7 @@ function placeOf(url: string): { section: DocsSection; trail: string[] } {
return section ? { section, trail } : { section: 'Project', trail: [] };
}

export async function GET(_req: Request, { params }: RouteContext<'/og/docs/[...slug]'>) {
export async function GET(_req: Request, { params }: RouteContext<'/docs/og/[...slug]'>) {
const { slug } = await params;
if (slug.at(-1) !== FILE) notFound();
const page = source.getPage(slug.slice(0, -1));
Expand Down
4 changes: 2 additions & 2 deletions app/api/search/route.ts → app/docs/search.json/route.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { createFromSource } from 'fumadocs-core/search/server';
import { source } from '@/lib/source';

// Built once at build time into a static file; the search box downloads it
// and searches in the browser, so the site needs no server.
// Built once at build time into out/docs/search.json; the search box (app/layout.tsx)
// downloads it and searches in the browser, so the site needs no server.
export const revalidate = false;
export const { staticGET: GET } = createFromSource(source, { language: 'english' });
17 changes: 17 additions & 0 deletions app/docs/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
import type { MetadataRoute } from 'next';
import { absoluteUrl } from '@/lib/og';
import { source } from '@/lib/source';

// Written once into out/docs/sitemap.xml by the static export. The Hub's
// /sitemap.xml index links here.
export const dynamic = 'force-static';

/** Every docs page, with its git last-modified date. */
export default function sitemap(): MetadataRoute.Sitemap {
return source.getPages().map((page) => ({
url: absoluteUrl(page.url),
lastModified: page.data.lastModified,
changeFrequency: 'weekly',
priority: page.slugs.length === 0 ? 0.9 : 0.7,
}));
}
11 changes: 11 additions & 0 deletions app/docs/ui/v1/nav.json/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { siteNav } from '@/lib/nav';

// /docs/ui/v1/nav.json: ui/nav.json (the site header, footer and call to action),
// for the Hub to copy. v1 is the shape of the file.
export const dynamic = 'force-static';

export function GET() {
return new Response(`${JSON.stringify(siteNav, null, 2)}\n`, {
headers: { 'Content-Type': 'application/json; charset=utf-8' },
});
}
11 changes: 11 additions & 0 deletions app/docs/ui/v1/tokens.css/route.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
import { readFile } from 'node:fs/promises';
import { join } from 'node:path';

// /docs/ui/v1/tokens.css: ui/tokens.css as it is, for the Hub to copy.
// v1 is the shape of the file; bump the folder when a change would break a copy.
export const dynamic = 'force-static';

export async function GET() {
const css = await readFile(join(process.cwd(), 'ui/tokens.css'), 'utf8');
return new Response(css, { headers: { 'Content-Type': 'text/css; charset=utf-8' } });
}
15 changes: 3 additions & 12 deletions app/global.css
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
@import 'tailwindcss';
@import 'fumadocs-ui/css/neutral.css';
@import 'fumadocs-ui/css/preset.css';
/* The V12 tokens shared with the Hub (published at /docs/ui/v1/tokens.css). */
@import '../ui/tokens.css';

/* =========================================================================
* Site-wide theme, in the V12 "B v3" style: navy #04121C, sky #38BDF8,
Expand All @@ -16,16 +18,8 @@
--font-mono: var(--font-jetbrains), ui-monospace, SFMono-Regular, Menlo, monospace;
}

/* Fumadocs' colours; the --pl-* brand colours come from ui/tokens.css. */
:root {
--pl-ink: #04121c;
--pl-sky: #38bdf8;
--pl-sky-deep: #0b79ad;
--pl-white: #ffffff;
--pl-paper: #f3f7f9;
/* the thick edge and hard shadow used on cards, code and callouts */
--pl-edge: var(--pl-ink);
--pl-shadow: var(--pl-ink);

--color-fd-background: #f3f7f9;
--color-fd-foreground: #04121c;
--color-fd-muted: #e3edf2;
Expand All @@ -51,9 +45,6 @@
}

.dark {
--pl-edge: #2e4756;
--pl-shadow: #38bdf8;

--color-fd-background: #04121c;
--color-fd-foreground: #f3f7f9;
--color-fd-muted: #0a1f2e;
Expand Down
11 changes: 6 additions & 5 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ export const metadata: Metadata = {
description:
'Pelican is a free, open-source panel for running game servers and applications in isolated Docker containers.',
applicationName: SITE_NAME,
icons: { icon: '/img/favicon.ico', apple: '/img/logo.png' },
icons: { icon: '/docs/img/favicon.ico', apple: '/docs/img/logo.png' },
// Fallbacks only: each page sets its own canonical, Open Graph and Twitter card
// through pageMetadata() in lib/og.ts.
openGraph: {
Expand All @@ -41,10 +41,11 @@ export default function RootLayout({ children }: LayoutProps<'/'>) {
<html lang="en" className={`${display.variable} ${text.variable} ${mono.variable}`} suppressHydrationWarning>
<body className="flex min-h-screen flex-col">
<RootProvider
// Follows the reader's system setting; the switch in the docs bar overrides it.
theme={{ defaultTheme: 'system' }}
// The search index is a static file built with the site (app/api/search).
search={{ options: { type: 'static' } }}
// Dark until the reader picks a theme with the switch in the docs bar (the Hub does the same).
theme={{ defaultTheme: 'dark' }}
// The search index is a static file built with the site (app/docs/search.json),
// under /docs/ like everything else the docs load.
search={{ options: { type: 'static', api: '/docs/search.json' } }}
>
{children}
<ExternalLinkGuard />
Expand Down
3 changes: 2 additions & 1 deletion app/robots.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,14 @@ import type { MetadataRoute } from 'next';
import { IS_PRODUCTION, SITE_URL } from '@/lib/og';

// Written once into out/robots.txt by the static export.
// INTERIM: after the switch the Hub serves /robots.txt (and should list /docs/sitemap.xml).
export const dynamic = 'force-static';

/** Everything is open, AI crawlers included: the owner wants the docs discoverable. */
export default function robots(): MetadataRoute.Robots {
return {
// Deploy previews are closed to crawlers; production is open to everyone.
rules: IS_PRODUCTION ? { userAgent: '*', allow: '/' } : { userAgent: '*', disallow: '/' },
sitemap: `${SITE_URL}/sitemap.xml`,
sitemap: [`${SITE_URL}/sitemap.xml`, `${SITE_URL}/docs/sitemap.xml`],
};
}
Loading
Loading