From 79dd59d32412c2e71b06d400469b1fd3102c37fd Mon Sep 17 00:00:00 2001 From: vihar Date: Sat, 22 Aug 2026 01:55:07 +0530 Subject: [PATCH 1/2] docs: server-render a useful 404 page on both sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Unknown paths on docs.plane.so and developers.plane.so returned a 404 whose body was an empty app shell (VitePress deliberately leaves #app empty in the 404.html it writes), so agents and no-JS clients got nothing to recover from. - packages/theme: new NotFound.vue with the "page not found" content — home, search, sitemap.xml, llms.txt, llms-full.txt, the sibling Plane docs site, a help link, and a hint for agents (append .md / Accept: text/markdown). Registered as and used as the Layout's default `not-found` slot so client-side navigation to a missing page shows the same content. Site-specific wording comes from a new `notFound` option on createPlaneTheme. - apps/*: docs/not-found.md renders the component; buildEnd moves the server-rendered not-found.html over 404.html and drops the stray copies. The page is noindex, excluded from search, llms.txt and the sitemap. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CsPSwTnpsEb5c5Ud2CLrL8 --- apps/developer-docs/AGENTS.md | 1 + .../developer-docs/docs/.vitepress/config.mts | 19 +++- .../docs/.vitepress/theme/index.ts | 9 ++ apps/developer-docs/docs/not-found.md | 24 +++++ apps/docs/AGENTS.md | 1 + apps/docs/docs/.vitepress/config.ts | 18 +++- apps/docs/docs/.vitepress/theme/index.ts | 9 ++ apps/docs/docs/not-found.md | 24 +++++ packages/theme/README.md | 2 +- packages/theme/src/components/NotFound.vue | 87 +++++++++++++++++++ packages/theme/src/index.ts | 5 +- packages/theme/src/layout/Layout.vue | 11 ++- packages/theme/src/options.ts | 16 ++++ 13 files changed, 219 insertions(+), 7 deletions(-) create mode 100644 apps/developer-docs/docs/not-found.md create mode 100644 apps/docs/docs/not-found.md create mode 100644 packages/theme/src/components/NotFound.vue diff --git a/apps/developer-docs/AGENTS.md b/apps/developer-docs/AGENTS.md index 1115e776..3fe386a6 100644 --- a/apps/developer-docs/AGENTS.md +++ b/apps/developer-docs/AGENTS.md @@ -26,6 +26,7 @@ pnpm --filter developer-docs check:types - **`docs/api-reference/`** — REST API endpoint docs (180+ endpoints across 30+ resource categories) - **`docs/self-hosting/`** — Deployment and configuration guides - **`docs/dev-tools/`** — Webhooks, OAuth apps, agents, MCP server docs + - **`docs/not-found.md`** — "Page not found" page; `buildEnd` turns it into the `404.html` Vercel serves (content is the shared `NotFound` theme component) - **`vercel.json`** — cleanUrls, headers, redirects, `Accept: text/markdown` rewrite (per-app Vercel project) - Shared visual identity (header, layout, tokens, `Card`/`CardGroup`/`Tags`, Copy page menu, cookie consent) lives in `packages/theme` — never fork it here. diff --git a/apps/developer-docs/docs/.vitepress/config.mts b/apps/developer-docs/docs/.vitepress/config.mts index 5a74acc0..2167d22c 100644 --- a/apps/developer-docs/docs/.vitepress/config.mts +++ b/apps/developer-docs/docs/.vitepress/config.mts @@ -4,7 +4,15 @@ import { tabsMarkdownPlugin } from "vitepress-plugin-tabs"; import { withMermaid } from "vitepress-plugin-mermaid"; import { extendConfig } from "@voidzero-dev/vitepress-theme/config"; import llmstxt from "vitepress-plugin-llms"; -import { readFileSync, readdirSync, statSync, mkdirSync, copyFileSync } from "node:fs"; +import { + readFileSync, + readdirSync, + statSync, + mkdirSync, + copyFileSync, + renameSync, + rmSync, +} from "node:fs"; import { resolve, join, relative, dirname } from "node:path"; function loadEnvVar(key: string): string | undefined { @@ -75,6 +83,7 @@ export default extendConfig( // Pages hidden from search (search: false / noindex) are excluded // from the LLM files too. ignoreFiles: [ + "not-found.md", "self-hosting/methods/install-methods-commercial/docker-compose.md", "self-hosting/methods/install-methods-commercial/kubernetes.md", ], @@ -117,6 +126,12 @@ export default extendConfig( } walk(srcDir); + + // The server-rendered "page not found" page becomes the static 404.html that + // Vercel serves for unknown paths (VitePress leaves #app empty in the 404.html + // it writes). + renameSync(join(outDir, "not-found.html"), join(outDir, "404.html")); + rmSync(join(outDir, "not-found.md")); }, title: "Plane developer documentation", description: @@ -129,6 +144,8 @@ export default extendConfig( // SEO: Generate sitemap automatically sitemap: { hostname: "https://developers.plane.so", + // not-found.md only exists to become 404.html (see buildEnd). + transformItems: (items) => items.filter((item) => item.url !== "not-found"), }, // SEO: Clean URLs without .html extension diff --git a/apps/developer-docs/docs/.vitepress/theme/index.ts b/apps/developer-docs/docs/.vitepress/theme/index.ts index 41c43395..a48f1456 100644 --- a/apps/developer-docs/docs/.vitepress/theme/index.ts +++ b/apps/developer-docs/docs/.vitepress/theme/index.ts @@ -30,6 +30,15 @@ export default createPlaneTheme({ footerBg: "https://media.docs.plane.so/logo/og-docs.webp", monoIcon: "/logo/favicon-32x32.png", }, + notFound: { + siteName: "the Plane developer docs", + sibling: { + name: "docs.plane.so", + url: "https://docs.plane.so", + covers: "how to use Plane (workspaces, projects, work items, pages)", + }, + help: { text: "Get help from the Plane team", link: "https://docs.plane.so/support/get-help" }, + }, components: { ApiParam, CodePanel, ResponsePanel }, setup() { const route = useRoute(); diff --git a/apps/developer-docs/docs/not-found.md b/apps/developer-docs/docs/not-found.md new file mode 100644 index 00000000..0f65ff00 --- /dev/null +++ b/apps/developer-docs/docs/not-found.md @@ -0,0 +1,24 @@ +--- +title: Page not found +description: This page doesn't exist on developers.plane.so. Start from the home page, search, the sitemap, or llms.txt to find what you're looking for. +keywords: plane, page not found, 404 +# Not a real page: keep it out of search, the LLM files, and search engines. +search: false +copyPage: false +outline: false +prev: false +next: false +editLink: false +lastUpdated: false +head: + - - meta + - name: robots + content: noindex, nofollow +--- + + + + diff --git a/apps/docs/AGENTS.md b/apps/docs/AGENTS.md index 69e70c14..538c9fb9 100644 --- a/apps/docs/AGENTS.md +++ b/apps/docs/AGENTS.md @@ -36,6 +36,7 @@ docs/ ai/ # Plane AI features support/ # Keyboard shortcuts, get help templates/ # Page, project, work-item templates +docs/not-found.md # "Page not found" page → becomes 404.html in buildEnd (shared NotFound component) vercel.json # cleanUrls, headers, redirects, Accept: text/markdown rewrite ``` diff --git a/apps/docs/docs/.vitepress/config.ts b/apps/docs/docs/.vitepress/config.ts index ebeae667..298f70ce 100644 --- a/apps/docs/docs/.vitepress/config.ts +++ b/apps/docs/docs/.vitepress/config.ts @@ -1,6 +1,14 @@ /** @format */ -import { copyFileSync, mkdirSync, readFileSync, readdirSync, statSync } from "fs"; +import { + copyFileSync, + mkdirSync, + readFileSync, + readdirSync, + renameSync, + rmSync, + statSync, +} from "fs"; import { dirname, join, relative, resolve } from "path"; import { defineConfig } from "vitepress"; import { extendConfig } from "@voidzero-dev/vitepress-theme/config"; @@ -64,6 +72,7 @@ const config = defineConfig({ // Pages hidden from search (search: false / noindex) are excluded // from the LLM files too. ignoreFiles: [ + "not-found.md", "core-concepts/issues.md", "core-concepts/projects/run-project.md", "importers/github-imp.md", @@ -94,6 +103,11 @@ const config = defineConfig({ } walk(srcDir); + + // The server-rendered "page not found" page becomes the static 404.html that Vercel + // serves for unknown paths (VitePress leaves #app empty in the 404.html it writes). + renameSync(join(outDir, "not-found.html"), join(outDir, "404.html")); + rmSync(join(outDir, "not-found.md")); }, head: [ @@ -789,6 +803,8 @@ const config = defineConfig({ sitemap: { hostname: "https://docs.plane.so", + // not-found.md only exists to become 404.html (see buildEnd). + transformItems: (items) => items.filter((item) => item.url !== "not-found"), }, markdown: { diff --git a/apps/docs/docs/.vitepress/theme/index.ts b/apps/docs/docs/.vitepress/theme/index.ts index f7bb9bcd..5365bd8d 100644 --- a/apps/docs/docs/.vitepress/theme/index.ts +++ b/apps/docs/docs/.vitepress/theme/index.ts @@ -14,4 +14,13 @@ export default createPlaneTheme({ footerBg: "https://media.docs.plane.so/logo/og-docs.webp", monoIcon: "https://media.docs.plane.so/logo/favicon-32x32.png", }, + notFound: { + siteName: "the Plane docs", + sibling: { + name: "developers.plane.so", + url: "https://developers.plane.so", + covers: "the API, webhooks, the MCP server, or self-hosting", + }, + help: { text: "Get help from the Plane team", link: "/support/get-help" }, + }, }); diff --git a/apps/docs/docs/not-found.md b/apps/docs/docs/not-found.md new file mode 100644 index 00000000..9af239d8 --- /dev/null +++ b/apps/docs/docs/not-found.md @@ -0,0 +1,24 @@ +--- +title: Page not found +description: This page doesn't exist on docs.plane.so. Start from the home page, search, the sitemap, or llms.txt to find what you're looking for. +keywords: plane, page not found, 404 +# Not a real page: keep it out of search, the LLM files, and search engines. +search: false +copyPage: false +outline: false +prev: false +next: false +editLink: false +lastUpdated: false +head: + - - meta + - name: robots + content: noindex, nofollow +--- + + + + diff --git a/packages/theme/README.md b/packages/theme/README.md index c077c3f6..8409c1c9 100644 --- a/packages/theme/README.md +++ b/packages/theme/README.md @@ -45,7 +45,7 @@ src/ index.ts createPlaneTheme(options) + client setup (appearance sync, medium-zoom, tab hashes) options.ts PlaneThemeOptions / planeOptionsKey layout/ Layout.vue, doc-layout.vue (PlaneHeader + bordered content wrapper), slots/header helpers - components/ PlaneHeader, CopyPageMenu, CookieConsent, Card, CardGroup, Tags, brand icons + components/ PlaneHeader, CopyPageMenu, CookieConsent, NotFound (not-found.md → 404.html + not-found slot), Card, CardGroup, Tags, brand icons css/ index.css → fonts, tokens, base, layout, components, api types/ ambient shims (*.vue, @vp-* aliases) and the VitePress config augmentation ``` diff --git a/packages/theme/src/components/NotFound.vue b/packages/theme/src/components/NotFound.vue new file mode 100644 index 00000000..8e05daa8 --- /dev/null +++ b/packages/theme/src/components/NotFound.vue @@ -0,0 +1,87 @@ + + + + + diff --git a/packages/theme/src/index.ts b/packages/theme/src/index.ts index 88a32d6d..fc39e0e6 100644 --- a/packages/theme/src/index.ts +++ b/packages/theme/src/index.ts @@ -25,11 +25,12 @@ import Card from "./components/Card.vue"; import CardGroup from "./components/CardGroup.vue"; import Tags from "./components/Tags.vue"; import CookieConsent from "./components/CookieConsent.vue"; +import NotFound from "./components/NotFound.vue"; import { planeOptionsKey, type PlaneThemeOptions } from "./options"; export type { PlaneThemeOptions } from "./options"; export { planeOptionsKey } from "./options"; -export { Card, CardGroup, Tags, CookieConsent, PlaneLayout }; +export { Card, CardGroup, Tags, CookieConsent, NotFound, PlaneLayout }; /* --------------------------------------------------------------------------- * Client-side helpers @@ -122,6 +123,8 @@ export function createPlaneTheme(options: PlaneThemeOptions): Theme { app.component("Card", Card); app.component("CardGroup", CardGroup); app.component("Tags", Tags); + // Used by each app's docs/404.md (see components/NotFound.vue). + app.component("PlaneNotFound", NotFound); for (const [name, component] of Object.entries(options.components ?? {})) { app.component(name, component); } diff --git a/packages/theme/src/layout/Layout.vue b/packages/theme/src/layout/Layout.vue index d23a5cae..594676bb 100644 --- a/packages/theme/src/layout/Layout.vue +++ b/packages/theme/src/layout/Layout.vue @@ -5,13 +5,15 @@ import VPDefaultLayout from "./default-layout"; import OSSHeader from "./header"; import TopBanner from "./top-banner"; import CopyPageMenu from "../components/CopyPageMenu.vue"; +import NotFound from "../components/NotFound.vue"; const { frontmatter, site } = useData(); const slots = useSlots(); -// `doc-before` is rendered explicitly below (it also hosts the "Copy page" control), -// so keep it out of the dynamic forwarding loop. +// `doc-before` and `not-found` are rendered explicitly below (the first also hosts the +// "Copy page" control, the second defaults to the shared NotFound content), so keep them +// out of the dynamic forwarding loop. const forwardSlotNames = computed(() => - (Object.keys(slots) as string[]).filter((name) => name !== "doc-before"), + (Object.keys(slots) as string[]).filter((name) => name !== "doc-before" && name !== "not-found"), ); const variant = computed( @@ -41,6 +43,9 @@ const useDocLayout = computed(() => { +
diff --git a/packages/theme/src/options.ts b/packages/theme/src/options.ts index e33b624a..2d3a0550 100644 --- a/packages/theme/src/options.ts +++ b/packages/theme/src/options.ts @@ -19,6 +19,22 @@ export interface PlaneThemeOptions { }; /** Render the cookie-consent banner. Default: true. */ cookieConsent?: boolean; + /** Site-specific wording for the shared "page not found" content (404.md + not-found slot). */ + notFound?: { + /** How this site is referred to in prose, e.g. "the Plane docs". */ + siteName: string; + /** The other Plane docs site, for visitors who landed on the wrong one. */ + sibling?: { + /** Display name, e.g. "developers.plane.so". */ + name: string; + /** Origin without a trailing slash. */ + url: string; + /** What it covers, completing "Looking for …?", e.g. "the API, webhooks, or self-hosting". */ + covers: string; + }; + /** Where a person can ask for help. */ + help?: { text: string; link: string }; + }; /** Extra globally-registered components (site-specific markdown components). */ components?: Record; /** Extra `enhanceApp` work, run after the shared setup. */ From 103994de5607fd33a426f713b91efa0d0a6c538c Mon Sep 17 00:00:00 2001 From: vihar Date: Sat, 22 Aug 2026 17:09:35 +0530 Subject: [PATCH 2/2] docs: no canonical on the 404 page With #509 merged, not-found.md picked up a canonical pointing at /not-found, a URL that only exists to become 404.html. Add a `canonical: false` frontmatter opt-out to canonicalLink() and use it there. Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CsPSwTnpsEb5c5Ud2CLrL8 --- apps/developer-docs/docs/not-found.md | 4 +++- apps/docs/docs/not-found.md | 4 +++- packages/theme/src/seo.ts | 8 +++++--- 3 files changed, 11 insertions(+), 5 deletions(-) diff --git a/apps/developer-docs/docs/not-found.md b/apps/developer-docs/docs/not-found.md index 0f65ff00..48daef64 100644 --- a/apps/developer-docs/docs/not-found.md +++ b/apps/developer-docs/docs/not-found.md @@ -2,8 +2,10 @@ title: Page not found description: This page doesn't exist on developers.plane.so. Start from the home page, search, the sitemap, or llms.txt to find what you're looking for. keywords: plane, page not found, 404 -# Not a real page: keep it out of search, the LLM files, and search engines. +# Not a real page: keep it out of search, the LLM files, and search engines, and +# don't give it a canonical URL (it is served at whatever path was not found). search: false +canonical: false copyPage: false outline: false prev: false diff --git a/apps/docs/docs/not-found.md b/apps/docs/docs/not-found.md index 9af239d8..f289a22c 100644 --- a/apps/docs/docs/not-found.md +++ b/apps/docs/docs/not-found.md @@ -2,8 +2,10 @@ title: Page not found description: This page doesn't exist on docs.plane.so. Start from the home page, search, the sitemap, or llms.txt to find what you're looking for. keywords: plane, page not found, 404 -# Not a real page: keep it out of search, the LLM files, and search engines. +# Not a real page: keep it out of search, the LLM files, and search engines, and +# don't give it a canonical URL (it is served at whatever path was not found). search: false +canonical: false copyPage: false outline: false prev: false diff --git a/packages/theme/src/seo.ts b/packages/theme/src/seo.ts index 11ca5bf0..4f019ddc 100644 --- a/packages/theme/src/seo.ts +++ b/packages/theme/src/seo.ts @@ -71,11 +71,13 @@ export function siteJsonLd(site: SiteIdentity): HeadConfig { /** * `` for a page, or undefined when the page's - * frontmatter already sets one. Mirrors cleanUrls: `dir/page.md` → - * `${origin}/dir/page`, `dir/index.md` → `${origin}/dir`, `index.md` → - * `${origin}/`. Call from `transformPageData`. + * frontmatter already sets one or opts out with `canonical: false` (e.g. the + * 404 page, which is served at arbitrary URLs). Mirrors cleanUrls: + * `dir/page.md` → `${origin}/dir/page`, `dir/index.md` → `${origin}/dir`, + * `index.md` → `${origin}/`. Call from `transformPageData`. */ export function canonicalLink(origin: string, pageData: PageData): HeadConfig | undefined { + if (pageData.frontmatter.canonical === false) return undefined; const head = (pageData.frontmatter.head ?? []) as HeadConfig[]; if (head.some(([tag, attrs]) => tag === "link" && attrs?.rel === "canonical")) return undefined; const path = pageData.relativePath.replace(/\.md$/, "").replace(/(^|\/)index$/, "");