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
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
39 changes: 27 additions & 12 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,27 +2,42 @@ name: 'Build'
on:
workflow_dispatch:
workflow_call:
pull_request:
push:
branches:
- 'main'

jobs:
deploy:
name: 'Deploy'
build:
name: 'Build'
runs-on: ubuntu-latest
steps:
- name: 'Checkout'
uses: actions/checkout@v4
uses: actions/checkout@v7

# Bun installs and runs the scripts; Next.js itself still builds on Node.
- name: 'Setup Bun'
uses: oven-sh/setup-bun@v2
with:
bun-version-file: package.json

- name: 'Setup Node'
uses: actions/setup-node@v4
uses: actions/setup-node@v7
with:
node-version: 20
- uses: pnpm/action-setup@v3
name: Install pnpm
node-version: 22

- name: 'Cache Bun packages'
uses: actions/cache@v6
with:
version: 10
run_install: true # pnpm will install dependencies recursively.
path: ~/.bun/install/cache
key: bun-${{ runner.os }}-${{ hashFiles('bun.lock') }}
restore-keys: bun-${{ runner.os }}-

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

- name: 'Typecheck'
run: bun run typecheck

- name: 'Build'
run: |
pnpm build
- name: 'Build'
run: bun run build
14 changes: 10 additions & 4 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
# Dependencies
/node_modules

# Production
/build
# Production (static export)
/out

# Generated files
.docusaurus
.cache-loader
/.next
/.source
next-env.d.ts
*.tsbuildinfo
.netlify

# Misc
.DS_Store
Expand All @@ -22,3 +25,6 @@ yarn-error.log*
/.idea

package-lock.json

# design skill working files
.taste-skill
9 changes: 9 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
<!-- BEGIN:nextjs-agent-rules -->

## This is NOT the Next.js you know

This version has breaking changes — APIs, conventions, and file structure may all differ from your training data. Read the relevant guide in `node_modules/next/dist/docs/` (resolved from this file's directory; in monorepos the `next` package may not be visible from the repo root) before writing any code. Heed deprecation notices.

This block is written and re-added by `next dev` — verify at `node_modules/next/dist/server/lib/generate-agent-files.js`. Removing it from a diff only re-creates the uncommitted change; committing it with your work keeps the tree clean.

<!-- END:nextjs-agent-rules -->
97 changes: 90 additions & 7 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,97 @@
# Pelican Docs

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).
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).
Built with [Next.js](https://nextjs.org) and [Fumadocs](https://fumadocs.dev) and served by Netlify as a static site.

Pelican docs are created with [Docusaurus](https://docusaurus.io).
## Running it locally

## Building
You need Node.js 22 or newer (Next.js builds on Node) and [Bun](https://bun.com) 1.3 or newer (installs and runs the scripts).

To test changes locally install the dependencies and run the server using pnpm.
```sh
bun install
bun run dev # http://localhost:3000, reloads as you edit
bun run build # the production site, written to out/
```

## Where things are

| Path | What it is |
| --- | --- |
| `content/docs/` | The documentation. One `.mdx` file per page; the URL follows the folder. |
| `content/docs/**/meta.json` | Sidebar order and section names for that folder. |
| `content/pages/` | Stand-alone pages: `/support` and `/faq`. |
| `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`. |

## Writing a page

Create `content/docs/<section>/<name>.mdx` and add `<name>` to that folder's `meta.json`
so it shows up in the sidebar.

```mdx
---
title: Installing Wings
description: One sentence shown under the title and in search results.
---

Your content, in Markdown.
```

The title goes in the frontmatter, not as a `# Heading`.

### Callouts

```mdx
:::tip
Short tips work like this.
:::

<Admonition type="warning" title="Back up first">
Or like this, when you want a title. Types: note, info, tip, warning, danger.
</Admonition>
```

```shell
pnpm i
pnpm run start
### Tabs

```mdx
<Tabs groupId="webserver">
<TabItem value="nginx" label="NGINX" default>…</TabItem>
<TabItem value="apache" label="Apache">…</TabItem>
<TabItem value="caddy" label="Caddy">…</TabItem>
</Tabs>
```

Tabs with the same `groupId` switch together on every page and remember the reader's choice.
Use `groupId="webserver"` with the values `nginx`, `apache` and `caddy`, and `groupId="ssl"`
with `https` and `http`, so they also follow the reader's setup (below).

### Code blocks

````mdx
```nginx title="/etc/nginx/sites-available/pelican.conf" {3,5}
````

`title` shows a file name above the block and `{3,5}` highlights lines 3 and 5.

### Filling in the reader's setup

Put a setup box on a page:

```mdx
<SetupPanel fields={['domain', 'webserver', 'ssl', 'php', 'path']} />
```

Then add `setup` to any code block that should use the reader's values:

````mdx
```nginx setup title="/etc/nginx/sites-available/pelican.conf"
server_name <domain>;
root /var/www/pelican/public;
fastcgi_pass unix:/run/php/php8.5-fpm.sock;
```
````

In blocks marked `setup`, `<domain>`, `/var/www/pelican` and the version in `php8.5` are replaced
with what the reader entered. Their choices are saved in their browser, so other pages pick them up too.
Blocks without `setup` are never changed.
85 changes: 85 additions & 0 deletions app/(landing)/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
import { notFound } from 'next/navigation';
import type { Metadata } from 'next';
import { readFileSync } from 'node:fs';
import { join } from 'node:path';
import { pagesSource } from '@/lib/source';
import { extractFaq, faqPageJsonLd, jsonLdScript, ogImage, pageMetadata } from '@/lib/og';
import { getMDXComponents } from '@/mdx-components';
import { SiteHeader } from '@/components/home/site-header';
import { SiteFooterNav } from '@/components/home/sections';

export const dynamicParams = false;

// Short lines under each page's title, in the same voice as the homepage. The
// frontmatter description is the longer one used for search results.
const LEDES: Record<string, string> = {
faq: 'Straight answers about Pelican, Wings, licensing and plugins.',
};

// Social card per page; every content/pages page needs one in the /og contract.
const OG_IMAGES: Record<string, string> = {
faq: ogImage.faq,
};

/** FAQPage structured data from the page's own <details> questions, so it matches what readers see. */
function faqJsonLd(slug: string, path: string) {
if (slug !== 'faq') return null;
const entries = extractFaq(readFileSync(join(process.cwd(), 'content/pages', path), 'utf8'), `/${slug}/`);
return entries.length > 0 ? jsonLdScript(faqPageJsonLd(entries, `/${slug}/`)) : null;
}

/**
* Stand-alone MDX pages from content/pages (today only /faq) in the homepage's design.
* /support is its own route (app/(landing)/support/page.tsx), not MDX.
*/
export default async function Page(props: PageProps<'/[slug]'>) {
const { slug } = await props.params;
const page = pagesSource.getPage([slug]);
if (!page) notFound();

const MDX = page.data.body;
const jsonLd = faqJsonLd(slug, page.path);

return (
<>
{jsonLd && <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: jsonLd }} />}
<SiteHeader />
<main id="main">
<section className="pl-page__hero" aria-labelledby="page-title">
<div className="pl-wrap">
<h1 id="page-title" className="d pl-page__title">
{page.data.title}
</h1>
{(LEDES[slug] ?? page.data.description) && (
<p className="pl-page__lede">{LEDES[slug] ?? page.data.description}</p>
)}
</div>
</section>
<section className="pl-page__body">
<div className="pl-wrap">
<div className="pl-mdx prose">
<MDX components={getMDXComponents()} />
</div>
</div>
</section>
</main>
<SiteFooterNav />
</>
);
}

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

export async function generateMetadata(props: PageProps<'/[slug]'>): Promise<Metadata> {
const { slug } = await props.params;
const page = pagesSource.getPage([slug]);
if (!page) notFound();
return pageMetadata({
title: page.data.title,
description: page.data.description ?? LEDES[slug],
path: `/${slug}/`,
image: OG_IMAGES[slug] ?? ogImage.home,
});
}
81 changes: 81 additions & 0 deletions app/(landing)/eggs/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
import type { Metadata } from 'next';
import { SiteHeader } from '@/components/home/site-header';
import { SiteFooterNav } from '@/components/home/sections';
import { EggIndexList, type IndexEgg, type IndexGame } from '@/components/home/egg-index';
import { eggCount, eggIndex, gameCount } from '@/lib/eggs';
import { ogImage, pageMetadata } from '@/lib/og';

export const metadata: Metadata = pageMetadata({
title: 'Eggs: every game and app Pelican can run',
description: `${eggCount} eggs for Pelican, from Minecraft and Rust to Discord bots and databases. Copy an egg's URL and import it into your panel.`,
path: '/eggs/',
image: ogImage.eggs,
});

const dateFormat = new Intl.DateTimeFormat('en-GB', { day: 'numeric', month: 'long', year: 'numeric', timeZone: 'UTC' });
const updated = dateFormat.format(new Date(eggIndex.generatedAt));
const hubUpdated = eggIndex.hubFetchedAt ? dateFormat.format(new Date(eggIndex.hubFetchedAt)) : null;

// Only what the list shows, searches or filters on goes to the browser: no
// square icons (they're for the homepage), no egg icons, no empty fields.
// An egg's details (images, commands, variables, install script) load from
// /data/eggs/<id>.json when someone opens it.
const indexGames: IndexGame[] = eggIndex.games.map(({ slug, name, category, steamAppId, art, eggs }) => ({
slug,
name,
category,
steamAppId,
art,
eggs: eggs.map((e) => {
const egg: IndexEgg = { id: e.id, name: e.name, description: e.description, url: e.url, image: e.image };
if (e.imageNote) egg.imageNote = e.imageNote;
if (e.features.length) egg.features = e.features;
if (e.vars.length) egg.vars = e.vars.join(' ');
if (e.hubUrl) egg.hubUrl = e.hubUrl;
if (e.installs) egg.installs = e.installs;
if (e.updatedAt) egg.updatedAt = e.updatedAt.slice(0, 10);
return egg;
}),
}));

export default function EggsPage() {
return (
<>
<SiteHeader />
<main id="main" className="pl-ei">
<section className="pl-ei__hero" aria-labelledby="eggs-title">
<div className="pl-wrap">
<h1 id="eggs-title" className="d pl-ei__title">
Every egg, in one place.
</h1>
<p className="pl-ei__lede">
An egg tells Pelican how to install and start a game or app. Here are all {eggCount} community eggs for{' '}
{gameCount} games and plenty more besides. Open one to see its images, variables and install script, or
copy its URL and import it from your panel&apos;s Eggs page.
</p>
</div>
</section>
<section className="pl-ei__list" aria-label="Eggs">
<div className="pl-wrap">
<EggIndexList games={indexGames} categories={eggIndex.categories} />
<p className="pl-fine pl-ei__source">
From the <a href="https://pelican-eggs.github.io/pelican">pelican-eggs index</a>, which is rebuilt every day
from the <a href="https://github.com/pelican-eggs">pelican-eggs repositories</a> (MIT licence); each
egg&apos;s details are read from its own egg file. This copy is from {updated}.
{hubUpdated && (
<>
{' '}
Install counts and update dates come from <a href="https://hub.pelican.dev/eggs">Pelican Hub</a>, as of{' '}
{hubUpdated}.
</>
)}{' '}
Game artwork comes from each game&apos;s Steam store page and belongs to its publisher; the Minecraft block is
our own drawing.
</p>
</div>
</section>
</main>
<SiteFooterNav />
</>
);
}
Loading
Loading