From 3014ce642faa9330382cd545b055d07eaa728429 Mon Sep 17 00:00:00 2001 From: "Alexandru Vlad (Scai)" <59282365+alexevladgabriel@users.noreply.github.com> Date: Wed, 4 Nov 2026 22:48:29 +0200 Subject: [PATCH 1/9] feat: load everything the docs need from /docs/ pelican.dev will route only /docs/*, /llms*.txt and the install scripts to this build; the Hub serves every other path. - assetPrefix '/docs': pages load /docs/_next/...; the static export still writes out/_next/, so netlify.toml rewrites /docs/_next/* to it - search index: app/api/search -> /docs/search.json (search client points there) - Markdown copies: app/md -> app/docs/md (still served at /docs/.md) - docs share images: /og/docs/... -> /docs/og/... - logo and favicon: /img/ -> /docs/img/ - /docs/sitemap.xml for the docs; /sitemap.xml keeps the landing pages (interim) and robots.txt lists both - 301s from the old paths that other sites may link --- app/{ => docs}/md/[...slug]/route.ts | 4 +- app/{og/docs => docs/og}/[...slug]/route.tsx | 4 +- app/{api/search => docs/search.json}/route.ts | 4 +- app/docs/sitemap.ts | 17 ++++++ app/layout.tsx | 7 ++- app/robots.ts | 3 +- app/sitemap.ts | 18 ++---- components/docs/docs-top-bar.tsx | 2 +- components/home/feature-panel.tsx | 2 +- components/home/sections.tsx | 4 +- components/home/site-header.tsx | 2 +- lib/layout.shared.tsx | 2 +- lib/llms.ts | 7 ++- lib/og-image.tsx | 2 +- lib/og.ts | 13 +++-- netlify.toml | 53 ++++++++++++++++-- next.config.mjs | 6 ++ public/{ => docs}/img/favicon.ico | Bin public/{ => docs}/img/logo.png | Bin 19 files changed, 108 insertions(+), 42 deletions(-) rename app/{ => docs}/md/[...slug]/route.ts (84%) rename app/{og/docs => docs/og}/[...slug]/route.tsx (96%) rename app/{api/search => docs/search.json}/route.ts (57%) create mode 100644 app/docs/sitemap.ts rename public/{ => docs}/img/favicon.ico (100%) rename public/{ => docs}/img/logo.png (100%) diff --git a/app/md/[...slug]/route.ts b/app/docs/md/[...slug]/route.ts similarity index 84% rename from app/md/[...slug]/route.ts rename to app/docs/md/[...slug]/route.ts index 9aaf3c3b..1899cff5 100644 --- a/app/md/[...slug]/route.ts +++ b/app/docs/md/[...slug]/route.ts @@ -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/.md; netlify.toml serves +// One docs page as Markdown. Builds to out/docs/md/.md; netlify.toml serves // it at /docs/.md (the docs page route owns everything under /docs). export const dynamic = 'force-static'; export const dynamicParams = false; @@ -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(); diff --git a/app/og/docs/[...slug]/route.tsx b/app/docs/og/[...slug]/route.tsx similarity index 96% rename from app/og/docs/[...slug]/route.tsx rename to app/docs/og/[...slug]/route.tsx index ee7a51aa..14165e55 100644 --- a/app/og/docs/[...slug]/route.tsx +++ b/app/docs/og/[...slug]/route.tsx @@ -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//image.png for every other page. +// /docs/og/image.png for /docs/ and /docs/og//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; @@ -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)); diff --git a/app/api/search/route.ts b/app/docs/search.json/route.ts similarity index 57% rename from app/api/search/route.ts rename to app/docs/search.json/route.ts index c1d933fb..b89a81e3 100644 --- a/app/api/search/route.ts +++ b/app/docs/search.json/route.ts @@ -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' }); diff --git a/app/docs/sitemap.ts b/app/docs/sitemap.ts new file mode 100644 index 00000000..f90d8fff --- /dev/null +++ b/app/docs/sitemap.ts @@ -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, + })); +} diff --git a/app/layout.tsx b/app/layout.tsx index 8ff22cbd..c5cc6537 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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: { @@ -43,8 +43,9 @@ export default function RootLayout({ children }: LayoutProps<'/'>) { {children} diff --git a/app/robots.ts b/app/robots.ts index 88e272d9..56de5be1 100644 --- a/app/robots.ts +++ b/app/robots.ts @@ -2,6 +2,7 @@ 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. */ @@ -9,6 +10,6 @@ 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`], }; } diff --git a/app/sitemap.ts b/app/sitemap.ts index b66ef176..addda019 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -1,13 +1,16 @@ import type { MetadataRoute } from 'next'; import { absoluteUrl } from '@/lib/og'; -import { pagesSource, source } from '@/lib/source'; +import { pagesSource } from '@/lib/source'; // Written once into out/sitemap.xml by the static export. +// INTERIM: the landing pages only (remove after the switch, with them). The docs +// have their own /docs/sitemap.xml (app/docs/sitemap.ts); robots.txt lists both, +// and the Hub will serve /sitemap.xml as an index that points at the docs one. export const dynamic = 'force-static'; -/** Every landing page and every docs page, with the docs' git last-modified dates. */ +/** The landing pages (/, /eggs, /support, /faq). */ export default function sitemap(): MetadataRoute.Sitemap { - const landing: MetadataRoute.Sitemap = [ + return [ { url: absoluteUrl('/'), changeFrequency: 'weekly', priority: 1 }, { url: absoluteUrl('/eggs/'), changeFrequency: 'daily', priority: 0.8 }, { url: absoluteUrl('/support/'), changeFrequency: 'monthly', priority: 0.6 }, @@ -18,13 +21,4 @@ export default function sitemap(): MetadataRoute.Sitemap { priority: 0.6, })), ]; - - const docs: MetadataRoute.Sitemap = source.getPages().map((page) => ({ - url: absoluteUrl(page.url), - lastModified: page.data.lastModified, - changeFrequency: 'weekly', - priority: page.slugs.length === 0 ? 0.9 : 0.7, - })); - - return [...landing, ...docs]; } diff --git a/components/docs/docs-top-bar.tsx b/components/docs/docs-top-bar.tsx index b8de1759..38e8b262 100644 --- a/components/docs/docs-top-bar.tsx +++ b/components/docs/docs-top-bar.tsx @@ -14,7 +14,7 @@ export function DocsTopBar() {
{/* eslint-disable-next-line @next/next/no-img-element */} - + Pelican Docs diff --git a/components/home/feature-panel.tsx b/components/home/feature-panel.tsx index ee96f2d2..6d20e1d9 100644 --- a/components/home/feature-panel.tsx +++ b/components/home/feature-panel.tsx @@ -63,7 +63,7 @@ export function FeaturePanel() {
{/* eslint-disable-next-line @next/next/no-img-element */} - + Pelican / Servers / Minecraft SMP diff --git a/components/home/sections.tsx b/components/home/sections.tsx index 60dccacd..922b8bcc 100644 --- a/components/home/sections.tsx +++ b/components/home/sections.tsx @@ -195,7 +195,7 @@ export function FeaturesSection() {
{/* eslint-disable-next-line @next/next/no-img-element */} - + Panel Web UI + API @@ -480,7 +480,7 @@ export function SiteFooterNav() {
{/* eslint-disable-next-line @next/next/no-img-element */} - + Pelican

A free, open-source game server panel, built by its community.

diff --git a/components/home/site-header.tsx b/components/home/site-header.tsx index 163bd068..99516c0c 100644 --- a/components/home/site-header.tsx +++ b/components/home/site-header.tsx @@ -22,7 +22,7 @@ export function SiteHeader() {
{/* eslint-disable-next-line @next/next/no-img-element */} - + Pelican