From 9181a65dcef5e4b0038990943eecc8e806a10b1f Mon Sep 17 00:00:00 2001 From: shenlan Date: Mon, 15 Sep 2025 17:33:40 +0800 Subject: [PATCH] docs: refine routing plan and dl components (#221) --- docs/Frontend-Routing-design.md | 103 +++++++++++++++++++++++++++++++ scripts/fetch-dl-index.ts | 44 +++++++++++++ types/download.ts | 13 ++++ ui/dl/components/Breadcrumbs.tsx | 17 +++-- ui/dl/components/CardGrid.tsx | 83 +++++++++++-------------- ui/dl/components/FileTable.tsx | 79 ++++++++++++++++-------- ui/dl/package.json | 2 + 7 files changed, 264 insertions(+), 77 deletions(-) create mode 100644 docs/Frontend-Routing-design.md create mode 100644 scripts/fetch-dl-index.ts create mode 100644 types/download.ts diff --git a/docs/Frontend-Routing-design.md b/docs/Frontend-Routing-design.md new file mode 100644 index 0000000..39120b1 --- /dev/null +++ b/docs/Frontend-Routing-design.md @@ -0,0 +1,103 @@ +# svc.plus Frontend Routing Design + +This document outlines the routing plan and page skeleton for the **svc.plus** site +implemented with **Next.js 14 App Router** and exported as static HTML (`output: 'export'`). +The site uses **TypeScript** and **Tailwind CSS**. All pages are statically generated at +build time; runtime servers are not required. + +## Routes Overview + +| Route | Description | Page File | Components | Data Source | +|-------|-------------|-----------|------------|-------------| +| `/` | Site home with entry cards to Downloads and Docs | `app/page.tsx` | custom `Card` components | static | +| `/download/` | Downloads home displaying top‑level folders | `app/download/page.tsx` | `CardGrid` | `dl.svc.plus` top‑level JSON fetched at build time | +| `/download//[...path]` | File listing for any nested folder | `app/download/[name]/[[...path]]/page.tsx` | `FileTable` | per‑folder JSON fetched at build time | +| `/docs/` | Docs home listing available ebooks | `app/docs/page.tsx` | optional `DocCard` grid | local `content/` processed by Contentlayer | +| `/docs/` | Reading page for a single ebook | `app/docs/[name]/page.tsx` | reader layout with side TOC | `content//**` Markdown files | + +## Directory Structure + +``` +app/ + page.tsx # Home + download/ + page.tsx # Downloads home + [name]/ + [[...path]]/ + page.tsx # File listings for arbitrary depth + docs/ + page.tsx # Docs home + [name]/ + page.tsx # Ebook reader +content/ # Markdown sources for docs +ui/ + dl/ + components/ + CardGrid.tsx + FileTable.tsx + docs/ + components/ # (optional) shared doc components +``` + +## Static Generation Requirements + +### Downloads +1. `scripts/fetch-dl-index.ts` recursively crawls `https://dl.svc.plus/` (overridable by + `DL_BASE`) and writes `public/dl-index/top.json` and `public/dl-index/all.json`. + `top.json` feeds the downloads home while `all.json` contains a `DirListing[]` with + every directory and its `DirEntry` children. The script runs automatically via the + `prebuild` npm script. +2. `types/download.ts` defines shared types: + ```ts + interface DirEntry { name; href; type: 'file'|'dir'; size?; lastModified?; sha256? } + interface DirListing { path; entries: DirEntry[] } + ``` +3. Use `all.json` to enumerate every `//path/...` combination. +4. Implement `generateStaticParams` in + `app/download/[name]/[[...path]]/page.tsx` to return `{ name, path?: string[] }` for + each known directory including the empty path. +5. Implement `generateMetadata` to set titles and OpenGraph info for each folder. +6. Render a `FileTable` with breadcrumb navigation and sorting by name/size/time. + +### Docs +1. Store Markdown under `content//.md`. +2. Configure **Contentlayer** to parse metadata (title, cover, order, etc.). +3. `app/docs/page.tsx` queries the Contentlayer output to list available docs. +4. `app/docs/[name]/page.tsx` loads chapters for the given doc and renders a reader with + a sidebar table of contents, progress indicator, and intra‑page anchors for navigation. +5. Implement `generateStaticParams` for `/docs/[name]` based on `allDocs` from + Contentlayer, and `generateMetadata` for SEO and OpenGraph. + +## CodeX Automation Prompt + +Use the following step‑by‑step tasks for automated implementation. + +1. **Initialize Next.js project** + - Create a Next.js 14 project in `ui/` with `output: 'export'` and Tailwind CSS. + - Acceptance: running `npm run build` produces static `out/` directory. + +2. **Home Page** + - Implement `app/page.tsx` with two cards linking to `/download/` and `/docs/`. + - Acceptance: cards render with Tailwind styling. + +3. **Downloads Sub‑site** + - Add `CardGrid` and `FileTable` components under `ui/dl/components/`. + - Use `types/download.ts` and run `pnpm prebuild` to fetch directory JSON during + build. `CardGrid` props: `sections: { key; title; href; lastModified?; count? }[]`. + `FileTable` props: `listing: DirListing`, `breadcrumb: { label; href }[]`. + - Implement `generateStaticParams` and `generateMetadata` for + `app/download/[name]/[[...path]]/page.tsx`. + - Acceptance: visiting any known folder in exported build shows correct listing. + +4. **Docs Sub‑site** + - Configure Contentlayer for `content/` Markdown. + - Create `app/docs/page.tsx` that lists docs and `app/docs/[name]/page.tsx` that renders + a reader with sidebar TOC and navigation anchors. + - Acceptance: static export contains pages for all docs with working navigation. + +5. **Build Validation** + - Run `pnpm i && pnpm build && pnpm export` to ensure a full static export. + - Acceptance: the `out/` directory contains working `/download/` and `/docs/` pages, + e.g. `/download/offline-package/k3s/` renders a file table and `/docs/` + retains reading progress. + diff --git a/scripts/fetch-dl-index.ts b/scripts/fetch-dl-index.ts new file mode 100644 index 0000000..abe1738 --- /dev/null +++ b/scripts/fetch-dl-index.ts @@ -0,0 +1,44 @@ +import fs from 'fs/promises' +import path from 'path' +import { DirEntry, DirListing } from '../types/download' + +const BASE = (process.env.DL_BASE || 'https://dl.svc.plus/').replace(/\/+$/, '/') + +async function crawl(rel: string): Promise { + const url = BASE + rel + const res = await fetch(url + 'index.json') + if (!res.ok) throw new Error(`failed to fetch ${url}: ${res.status}`) + const entries = (await res.json()) as DirEntry[] + const listing: DirListing = { path: rel, entries } + const all: DirListing[] = [listing] + for (const e of entries) { + if (e.type === 'dir') { + const childRel = rel + e.name + '/' + const child = await crawl(childRel) + all.push(...child) + } + } + return all +} + +async function main() { + const listings = await crawl('') + const top = listings.find(l => l.path === '') + const sections = top ? top.entries.filter(e => e.type === 'dir').map(e => ({ + key: e.name, + title: e.name, + href: '/' + e.name + '/', + lastModified: e.lastModified, + count: undefined + })) : [] + + const outDir = path.join(process.cwd(), 'public', 'dl-index') + await fs.mkdir(outDir, { recursive: true }) + await fs.writeFile(path.join(outDir, 'all.json'), JSON.stringify(listings, null, 2)) + await fs.writeFile(path.join(outDir, 'top.json'), JSON.stringify(sections, null, 2)) +} + +main().catch(err => { + console.error(err) + process.exit(1) +}) diff --git a/types/download.ts b/types/download.ts new file mode 100644 index 0000000..ce0680c --- /dev/null +++ b/types/download.ts @@ -0,0 +1,13 @@ +export interface DirEntry { + name: string; + href: string; + type: 'file' | 'dir'; + size?: number; + lastModified?: string; + sha256?: string; +} + +export interface DirListing { + path: string; + entries: DirEntry[]; +} diff --git a/ui/dl/components/Breadcrumbs.tsx b/ui/dl/components/Breadcrumbs.tsx index 98c4787..a49604a 100644 --- a/ui/dl/components/Breadcrumbs.tsx +++ b/ui/dl/components/Breadcrumbs.tsx @@ -1,15 +1,20 @@ import Link from 'next/link' -export default function Breadcrumbs({ segments }: { segments: string[] }) { - const paths = segments.map((_, i) => '/' + segments.slice(0, i + 1).join('/') + '/') +export interface Crumb { + label: string + href: string +} + +export default function Breadcrumbs({ items }: { items: Crumb[] }) { return (