diff --git a/.pages.yml b/.pages.yml index 07d39532d..9094a8419 100644 --- a/.pages.yml +++ b/.pages.yml @@ -1,5 +1,3 @@ -# Browser editor configuration: https://pagescms.org/docs/configuration/ -# Keep category choices aligned with docs/pages frontmatter. media: - name: images label: Images @@ -8,134 +6,720 @@ media: categories: - image content: - - name: pages - label: Wiki pages - type: collection - path: docs/pages - format: yaml-frontmatter - subfolders: false - filename: - template: "{primary}.md" - field: create - operations: - rename: false - delete: false - view: - primary: title - fields: - - title - - tags - search: - - title - - aliases - sort: - - title - default: - sort: title - order: asc - fields: - - name: title - label: Page title - type: string - required: true - - name: description - label: Short description - type: text - description: Optional one-sentence summary for search engines. - - name: aliases - label: Other names for this page - type: string - list: true - - name: tags - label: Categories - type: select - options: - multiple: true - values: - - Arms items - - Basics - - Belts - - Boats - - Body items - - Charging weapons - - Cheat Sheet - - Cities - - Classes - - Cleaving weapons - - Cleric spells - - Clients - - Cloaks - - Cloth equipment - - Commands - - Concussion weapons - - Consumables - - Containers - - Ents - - Equipment - - Equipment by slot - - FAQS - - Feet items - - Guides - - Guildmasters - - Hands items - - Head items - - Help files - - Help_files - - History - - Inns - - Items - - Keys - - Leather equipment - - Legend homes - - Legend_homes - - Legs items - - Links - - Locations - - Lore - - Mage spells - - Mail equipment - - Mechanics - - Metal equipment - - Miscellaneous equipment - - Missile weapons - - Mobiles - - Mobiles with unknown level - - Neck items - - Newbie - - Newbie Help - - Piercing weapons - - Places - - Plants - - Playable races - - Quest items - - Races - - Ranger skills - - Rings - - Shaman spells - - Shields - - Shopkeepers - - Shops - - Skills - - Slashing weapons - - Smiting weapons - - Software - - Spells - - Stabbing weapons - - Statistics - - Thief skills - - Uncategorized items - - Warrior skills - - Weapons - - Wrist items - - name: autolink - label: Automatically link names of other wiki pages - type: boolean - default: true - - name: body - label: Page content (Markdown) - type: text - description: "Share mortal knowledge only. Hide spoilers inside ::: details - Spoiler blocks. Keep existing includes and components intact. Check your changes before saving to main; a successful site build publishes them directly." + - name: software + label: Software + type: group + items: + - name: software-c-z + label: C-Z + type: collection + path: docs/pages/Software/C-Z + format: yaml-frontmatter + subfolders: false + filename: &a1 + template: "{primary}.md" + field: create + operations: &a2 + rename: false + delete: false + view: &a3 + primary: title + fields: + - title + - tags + search: + - title + - aliases + sort: + - title + default: + sort: title + order: asc + fields: &a4 + - name: title + label: Page title + type: string + required: true + - name: description + label: Short description + type: text + description: Optional one-sentence summary for search engines. + - name: aliases + label: Other names for this page + type: string + list: true + - name: tags + label: Categories + type: select + options: + multiple: true + values: + - Arms items + - Basics + - Belts + - Boats + - Body items + - Charging weapons + - Cheat Sheet + - Cities + - Classes + - Cleaving weapons + - Cleric spells + - Clients + - Cloaks + - Cloth equipment + - Commands + - Concussion weapons + - Consumables + - Containers + - Ents + - Equipment + - Equipment by slot + - FAQS + - Feet items + - Guides + - Guildmasters + - Hands items + - Head items + - Help files + - Help_files + - History + - Inns + - Items + - Keys + - Leather equipment + - Legend homes + - Legend_homes + - Legs items + - Links + - Locations + - Lore + - Mage spells + - Mail equipment + - Mechanics + - Metal equipment + - Miscellaneous equipment + - Missile weapons + - Mobiles + - Mobiles with unknown level + - Neck items + - Newbie + - Newbie Help + - Piercing weapons + - Places + - Plants + - Playable races + - Quest items + - Races + - Ranger skills + - Rings + - Shaman spells + - Shields + - Shopkeepers + - Shops + - Skills + - Slashing weapons + - Smiting weapons + - Software + - Spells + - Stabbing weapons + - Statistics + - Thief skills + - Uncategorized items + - Warrior skills + - Weapons + - Wrist items + - name: autolink + label: Automatically link names of other wiki pages + type: boolean + default: true + - name: body + label: Page content + type: rich-text + options: + format: markdown + switcher: true + media: images + description: "Share mortal knowledge only. Hide spoilers inside ::: details + Spoiler blocks. Use Source mode for spoilers, includes, HTML, + scripts and components. Check your changes before saving to main; a successful + site build publishes them directly." + - name: guides + label: Guides + type: group + items: + - name: guides-c-g + label: C-G + type: collection + path: docs/pages/Guides/C-G + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: guides-h + label: H + type: collection + path: docs/pages/Guides/H + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: guides-n-1 + label: N-1 + type: collection + path: docs/pages/Guides/N-1 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: guides-n-2 + label: N-2 + type: collection + path: docs/pages/Guides/N-2 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: guides-n-3 + label: N-3 + type: collection + path: docs/pages/Guides/N-3 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: guides-n-4 + label: N-4 + type: collection + path: docs/pages/Guides/N-4 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: guides-n-5 + label: N-5 + type: collection + path: docs/pages/Guides/N-5 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: guides-n-6 + label: N-6 + type: collection + path: docs/pages/Guides/N-6 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: guides-n-7 + label: N-7 + type: collection + path: docs/pages/Guides/N-7 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: guides-n-8 + label: N-8 + type: collection + path: docs/pages/Guides/N-8 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: guides-q-t + label: Q-T + type: collection + path: docs/pages/Guides/Q-T + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: commands + label: Commands + type: group + items: + - name: commands-a-g + label: A-G + type: collection + path: docs/pages/Commands/A-G + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: commands-h-q + label: H-Q + type: collection + path: docs/pages/Commands/H-Q + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: commands-r-y + label: R-Y + type: collection + path: docs/pages/Commands/R-Y + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: spells + label: Spells + type: group + items: + - name: spells-b-n + label: B-N + type: collection + path: docs/pages/Spells/B-N + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: spells-p-w + label: P-W + type: collection + path: docs/pages/Spells/P-W + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: skills + label: Skills + type: group + items: + - name: skills-a-w + label: A-W + type: collection + path: docs/pages/Skills/A-W + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: equipment + label: Equipment + type: group + items: + - name: equipment-a-b + label: A-B + type: collection + path: docs/pages/Equipment/A-B + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: equipment-c-g + label: C-G + type: collection + path: docs/pages/Equipment/C-G + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: equipment-h-o + label: H-O + type: collection + path: docs/pages/Equipment/H-O + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: equipment-p-r + label: P-R + type: collection + path: docs/pages/Equipment/P-R + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: equipment-s-w + label: S-W + type: collection + path: docs/pages/Equipment/S-W + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: equipment-y + label: Y + type: collection + path: docs/pages/Equipment/Y + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: mobiles + label: Mobiles + type: group + items: + - name: mobiles-a-e + label: A-E + type: collection + path: docs/pages/Mobiles/A-E + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: mobiles-f-h + label: F-H + type: collection + path: docs/pages/Mobiles/F-H + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: mobiles-i-r + label: I-R + type: collection + path: docs/pages/Mobiles/I-R + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: mobiles-s-y + label: S-Y + type: collection + path: docs/pages/Mobiles/S-Y + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: locations + label: Locations + type: group + items: + - name: locations-a-i + label: A-I + type: collection + path: docs/pages/Locations/A-I + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: locations-l-w + label: L-W + type: collection + path: docs/pages/Locations/L-W + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: races + label: Races + type: group + items: + - name: races-b-z + label: B-Z + type: collection + path: docs/pages/Races/B-Z + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: classes + label: Classes + type: group + items: + - name: classes-a-z + label: A-Z + type: collection + path: docs/pages/Classes/A-Z + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + exclude: + - .gitkeep + - name: lore + label: Lore + type: group + items: + - name: lore-m-t + label: M-T + type: collection + path: docs/pages/Lore/M-T + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference + label: Reference + type: group + items: + - name: reference-a-1 + label: A-1 + type: collection + path: docs/pages/Reference/A-1 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-a-2 + label: A-2 + type: collection + path: docs/pages/Reference/A-2 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-b + label: B + type: collection + path: docs/pages/Reference/B + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-c-d + label: C-D + type: collection + path: docs/pages/Reference/C-D + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-e-f + label: E-F + type: collection + path: docs/pages/Reference/E-F + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-g-1 + label: G-1 + type: collection + path: docs/pages/Reference/G-1 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-g-2 + label: G-2 + type: collection + path: docs/pages/Reference/G-2 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-h-k + label: H-K + type: collection + path: docs/pages/Reference/H-K + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-l + label: L + type: collection + path: docs/pages/Reference/L + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-m-1 + label: M-1 + type: collection + path: docs/pages/Reference/M-1 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-m-2 + label: M-2 + type: collection + path: docs/pages/Reference/M-2 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-n + label: N + type: collection + path: docs/pages/Reference/N + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-o-q + label: O-Q + type: collection + path: docs/pages/Reference/O-Q + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-r + label: R + type: collection + path: docs/pages/Reference/R + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-s-1 + label: S-1 + type: collection + path: docs/pages/Reference/S-1 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-s-2 + label: S-2 + type: collection + path: docs/pages/Reference/S-2 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-s-3 + label: S-3 + type: collection + path: docs/pages/Reference/S-3 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-s-4 + label: S-4 + type: collection + path: docs/pages/Reference/S-4 + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-t-v + label: T-V + type: collection + path: docs/pages/Reference/T-V + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-w-x + label: W-X + type: collection + path: docs/pages/Reference/W-X + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 + - name: reference-z + label: Z + type: collection + path: docs/pages/Reference/Z + format: yaml-frontmatter + subfolders: false + filename: *a1 + operations: *a2 + view: *a3 + fields: *a4 settings: content: merge: true diff --git a/AGENTS.md b/AGENTS.md index 8da5e7292..efc2a59ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -30,11 +30,11 @@ docker run --rm -v "$(pwd):/app" -w /app node:22 npm install ## Pages CMS -[`.pages.yml`](.pages.yml) configures browser editing for `docs/pages/` and images in `docs/public/img/`. See [CONTRIBUTING.md](CONTRIBUTING.md) for app installation, editor invitations, and trusted-editor access and direct publishing to `main`. Keep its category choices aligned with existing frontmatter tags. Preserve the plain-text body editor: VitePress spoilers, includes, and components require lossless Markdown. Metadata extraction uses YAML parsing to support CMS list serialization. +[`.pages.yml`](.pages.yml) configures browser editing for `docs/pages/` and images in `docs/public/img/`. See [CONTRIBUTING.md](CONTRIBUTING.md) for app installation, editor invitations, and trusted-editor access and direct publishing to `main`. Keep its category choices aligned with existing frontmatter tags. The body uses rich-text Markdown with an Editor/Source switch. Use Source mode for VitePress spoilers, includes, HTML and components and preserve their Markdown syntax. Metadata extraction uses YAML parsing to support CMS list serialization. ## Adding or editing pages -All wiki pages live in [`docs/pages/`](docs/pages/). Each is a standard Markdown file with YAML frontmatter: +All wiki pages live in topic/alphabetic leaf folders under [`docs/pages/`](docs/pages/). Each topic has a Pages CMS group with one collection per leaf folder. Use the appropriate topic and filename range when creating a page; the public URL stays `/wiki/pages/` regardless of its source folder. Each is a standard Markdown file with YAML frontmatter: ```yaml --- @@ -68,7 +68,7 @@ Use relative Markdown links with the `.md` extension: [Back to classes](../classes.md) ``` -Do **not** use wikilink syntax `[text](Page "wikilink")` — the migration script converts those but the VitePress site uses standard links. +Do **not** use wikilink syntax `[text](Page "wikilink")` — the VitePress site uses standard Markdown links. ## Adding images @@ -128,7 +128,7 @@ Open a pull request — GitHub Actions will build your changes. Once a maintaine This file (`AGENTS.md`) is read automatically by Claude Code, OpenAI Codex, Gemini CLI, and other agents that support the `AGENTS.md` convention. Keep it up to date so agents have accurate project context. Key facts for agents: -- All wiki content is in `docs/pages/*.md` — edit there, not in `_pages/` (legacy source) +- All wiki content is in `docs/pages/**/*.md` — edit there, not in `_pages/` (legacy source) - Images live in `docs/public/img/` and are referenced as `/img/filename` - Dev server: `docker compose up dev` (port 5174) - Build command: `docker compose up --build wiki` (port 4173); or `npm run docs:build` inside the container diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9dae8f017..1ed29bac7 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,7 +37,7 @@ Secret information here... A maintainer must connect the repository first (see below). Editors can sign in with GitHub or accept a Pages CMS email invitation from a maintainer. If you do not have editor access, use the GitHub pull request route below. -Trusted members edit directly on **`main`**. Accept your invitation, open the wiki repository in Pages CMS, select `main`, and choose **Wiki pages** to edit an existing page or create one. Use **Images** to upload pictures. Check your content and spoilers before saving. +Trusted members edit directly on **`main`**. Accept your invitation, open the wiki repository in Pages CMS, select `main`, and choose the **topic → alphabetic collection** for the article to edit an existing page or create one. Use **Images** to upload pictures. Check your content and spoilers before saving. **Saving commits your changes directly to `main` and triggers the site build and deployment.** Your edit appears on the live wiki after deployment succeeds; there is no branch preparation, pull request, or per-edit maintainer approval. If saving is denied or the site does not update after deployment, contact a maintainer. See the [browser editing guide](https://docs.mume.org/wiki/pages/Contributing) for detailed steps. @@ -62,11 +62,15 @@ Local development requires [Docker](https://www.docker.com/). 2. Sign in at [Pages CMS](https://app.pagescms.org/) and install its GitHub App for this repository, following the [official quick start](https://pagescms.org/docs/quick-start/). Organization installation may require an owner. 3. Configure GitHub branch protection/rulesets so the Pages CMS GitHub App can commit directly to `main`. If pull requests or pre-merge checks are required, grant the installed app an appropriate bypass for direct publishing. Keep the normal review requirements for other contributors. `.pages.yml` does not grant access or change GitHub branch rules. 4. Open this repository on `main` in Pages CMS and invite trusted members through its collaborator interface. [Email collaborators](https://pagescms.org/docs/configuration/collaborators/) do not need GitHub accounts or repository write access: their edits use the installed GitHub App's access. Invitations grant direct publishing access, so invite only members trusted to follow the wiki rules. A GitHub login alone does not grant editing access. -5. Verify onboarding with a trusted editor: accept the invitation, open the repository on `main`, save a small valid edit, and confirm that the **Deploy to GitHub Pages** workflow succeeds and the live wiki updates. No PR is needed. Test aliases, images, spoilers, and includes before wider rollout. +5. Verify onboarding with a trusted editor: accept the invitation, open the repository on `main`, save a small valid edit, and confirm that the **Deploy to GitHub Pages** workflow succeeds and the live wiki updates. No PR is needed. Test aliases, images, spoilers, and includes before inviting more editors. 6. Use GitHub history to inspect changes and revert mistakes, and remove the CMS collaborator invitation when someone should no longer publish. If that person also has GitHub repository access, manage that separately. The existing deployment workflow runs on pushes to `main`. A saved edit remains in Git history even if validation or deployment fails; fix or revert the offending change and let deployment run again. Editors can ask a maintainer for help with failed builds or rollbacks. -The schema edits only `docs/pages/` and uploads images to `docs/public/img/`, writing `/img/…` URLs. Page bodies use plain text to preserve VitePress containers, includes, HTML, and Vue components; a visual rich-text round trip has not been validated for these constructs. Unknown frontmatter keys are preserved with `settings.content.merge`. +The schema edits only `docs/pages/` and uploads images to `docs/public/img/`, writing `/img/…` URLs. Page bodies offer WYSIWYG editing with Markdown output and an Editor/Source switch. Use Source mode for spoilers, includes, HTML, script examples, and Vue components. Inspect the source before saving. Unknown frontmatter keys are preserved with `settings.content.merge`. Page renaming and deletion are disabled in the CMS to avoid breaking links. New filenames must use the wiki convention (e.g. `Grey_Havens.md`); replace the suggested filename if needed, and omit leading `A`, `An`, or `The`. Section indexes and shared includes remain maintained through GitHub. When introducing a category, update the category choices in `.pages.yml` alongside the content. + +## Topic collections + +Articles are organized by topic and alphabetic filename range. Choose the topic first, then the collection for the article’s filename. Each article has one source file; tags can place it in multiple categories. Moving a source file between collections does not change its public `/wiki/pages/` URL. Keep filenames unchanged when editing existing pages. diff --git a/docs/.vitepress/config.ts b/docs/.vitepress/config.ts index ce63c7099..c45dab994 100644 --- a/docs/.vitepress/config.ts +++ b/docs/.vitepress/config.ts @@ -1,14 +1,30 @@ import { defineConfig } from 'vitepress' import fs from 'fs' import path from 'path' -import { slugify } from './shared' +import { createRequire } from 'node:module' +const require = createRequire(import.meta.url) +const { getMarkdownFiles } = require('../../scripts/utils.cjs') +const { publicRoute, topicFor, topicRoute } = require('../../scripts/routes.cjs') +const sources = getMarkdownFiles(path.resolve(__dirname, '../pages')).map((file: string) => path.relative(path.resolve(__dirname, '..'), file).replace(/\\/g, '/')) +const names = new Set() +for (const source of sources) { + const name = path.basename(source) + if (names.has(name)) throw new Error('Duplicate article filename: ' + name) + names.add(name) +} +const preview = (process.env.VITEPRESS_BASE ?? '/wiki/').includes('/pr-') || process.env.WIKI_PREVIEW === 'true' let pagesMeta = { terms: {}, paths: [], tags: {} } try { pagesMeta = JSON.parse(fs.readFileSync(path.resolve(__dirname, '../public/pages-meta.json'), 'utf-8')) } catch (e) {} const sortedTerms = Object.keys(pagesMeta.terms || {}).sort((a, b) => b.length - a.length) -const validPaths = new Set(pagesMeta.paths || []) +const termsByUrl = new Map() +for (const [term, url] of Object.entries(pagesMeta.terms || {})) { + const terms = termsByUrl.get(url as string) || [] + terms.push(term) + termsByUrl.set(url as string, terms) +} const editRepo = process.env.EDIT_REPO || 'MUME/wiki' const editBranch = process.env.EDIT_BRANCH || 'main' @@ -49,12 +65,22 @@ export default defineConfig({ description: 'A community wiki and guide to surviving in Multi-Users in Middle-earth.', base: process.env.VITEPRESS_BASE ?? '/wiki/', cleanUrls: true, - lastUpdated: true, - sitemap: { - hostname: 'https://docs.mume.org/wiki/' + rewrites: (source) => source.startsWith('pages/') ? publicRoute(source).slice(1) + '.md' : source, + transformPageData(pageData) { + const source = pageData.filePath || pageData.relativePath + if (source.startsWith('pages/')) { + const topic = topicFor(pageData.frontmatter.tags || []) + pageData.frontmatter.wikiTopic = topic + pageData.frontmatter.wikiTopicRoute = topicRoute(topic) + } + pageData.frontmatter.head ??= [] + if (!preview) pageData.frontmatter.head.push(['link', { rel: 'canonical', href: 'https://docs.mume.org/wiki' + publicRoute(source) }]) }, + lastUpdated: true, + sitemap: preview ? undefined : { hostname: 'https://docs.mume.org/wiki/' }, head: [ + ...(preview ? [['meta', { name: 'robots', content: 'noindex, nofollow' }]] as any : []), ['link', { rel: 'icon', type: 'image/png', href: '/img/Main_Gandalf.png' }], ['meta', { name: 'theme-color', content: '#1a1410' }], ], @@ -71,6 +97,8 @@ export default defineConfig({ { text: 'Races', link: '/races' }, { text: 'More', items: [ + { text: 'All topics', link: '/topics' }, + { text: 'Software', link: '/topics/software' }, { text: 'Lore', link: '/lore' }, { text: 'History', link: '/history' }, { text: 'Tags', link: '/tags' }, @@ -230,6 +258,18 @@ export default defineConfig({ markdown: { config(md) { + // Relative links retain the original flat published context after a source move. + md.core.ruler.after('inline', 'canonical-links', (state) => { + if (!state.env.relativePath?.startsWith('pages/')) return + for (const token of state.tokens) for (const child of token.children || []) { + if (child.type !== 'link_open') continue + const href = child.attrGet('href') + if (href && !/^(?:[a-z]+:|\/|#)/i.test(href)) { + const [target, suffix = ''] = href.split(/(?=[#?])/s, 2) + child.attrSet('href', '/' + path.posix.normalize('pages/' + target) + suffix) + } + } + }) // 1. Core auto-linking for text md.core.ruler.after('inline', 'auto-link', (state) => { if (state.env.frontmatter?.autolink === false) { @@ -237,7 +277,7 @@ export default defineConfig({ } const currentUrl = state.env.relativePath - ? '/' + state.env.relativePath.replace(/\.md$/, '').replace(/\\/g, '/') + ? publicRoute(state.env.relativePath) : ''; const usedTerms = new Set(); @@ -252,14 +292,12 @@ export default defineConfig({ if (href.startsWith('/')) { absoluteHref = href.replace(/\.md$/, ''); } else { - const dir = path.dirname(state.env.relativePath || ''); + const dir = path.posix.dirname(publicRoute(state.env.relativePath || '').slice(1)); absoluteHref = '/' + path.join(dir, href).replace(/\.md$/, '').replace(/\\/g, '/'); if (absoluteHref === '/.') absoluteHref = '/'; } - for (const [term, url] of Object.entries(pagesMeta.terms || {})) { - if (url === absoluteHref || url === absoluteHref.replace('/pages/', '/')) { - usedTerms.add(term); - } + for (const url of [absoluteHref, absoluteHref.replace('/pages/', '/')]) { + for (const term of termsByUrl.get(url) || []) usedTerms.add(term); } } } @@ -282,6 +320,7 @@ export default defineConfig({ if (isInLink) continue; const text = child.content; + const lowerText = text.toLowerCase(); let bestMatch = null; let bestIndex = -1; @@ -290,7 +329,7 @@ export default defineConfig({ const targetUrl = pagesMeta.terms[term]; if (targetUrl === currentUrl || targetUrl === currentUrl.replace('/pages/', '/')) continue; - const index = text.toLowerCase().indexOf(term); + const index = lowerText.indexOf(term); if (index !== -1) { const before = index > 0 ? text[index - 1] : ' '; const after = index + term.length < text.length ? text[index + term.length] : ' '; diff --git a/docs/.vitepress/theme/Layout.vue b/docs/.vitepress/theme/Layout.vue index e9ab75168..8274a889e 100644 --- a/docs/.vitepress/theme/Layout.vue +++ b/docs/.vitepress/theme/Layout.vue @@ -11,6 +11,11 @@ const { frontmatter } = useData()