From 025844c4cbbafffca4a94cd4d1b159e683019988 Mon Sep 17 00:00:00 2001 From: vihar Date: Sat, 22 Aug 2026 01:35:09 +0530 Subject: [PATCH] docs: add canonical links and Organization JSON-LD to both sites MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit docs.plane.so had no and neither site published structured data, which the is-agentic scan flagged (metadata completeness, JSON-LD, Organization schema). Add `packages/theme/src/seo.ts`, exported as `@plane/docs-theme/seo`, with: - `siteJsonLd(site)` — one `application/ld+json` head entry carrying a shared Plane Organization (logo, GitHub/X profiles, support and sales contact points) plus a WebSite node for the site, so both sites publish the same identity - `canonicalLink(origin, pageData)` — per-page canonical mirroring cleanUrls, skipped when frontmatter already sets one Wire both into `apps/docs` (new `transformPageData`) and `apps/developer-docs` (replacing its inline canonical logic). Co-Authored-By: Claude Fable 5 Claude-Session: https://claude.ai/code/session_01CsPSwTnpsEb5c5Ud2CLrL8 --- .../developer-docs/docs/.vitepress/config.mts | 24 +++--- apps/docs/docs/.vitepress/config.ts | 14 ++++ packages/theme/README.md | 1 + packages/theme/package.json | 1 + packages/theme/src/seo.ts | 83 +++++++++++++++++++ 5 files changed, 112 insertions(+), 11 deletions(-) create mode 100644 packages/theme/src/seo.ts diff --git a/apps/developer-docs/docs/.vitepress/config.mts b/apps/developer-docs/docs/.vitepress/config.mts index 5a74acc0..030df609 100644 --- a/apps/developer-docs/docs/.vitepress/config.mts +++ b/apps/developer-docs/docs/.vitepress/config.mts @@ -1,9 +1,10 @@ /// -import { defineConfig, type HeadConfig, type PageData } from "vitepress"; +import { defineConfig, type PageData } from "vitepress"; 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 { canonicalLink, siteJsonLd } from "@plane/docs-theme/seo"; import { readFileSync, readdirSync, statSync, mkdirSync, copyFileSync } from "node:fs"; import { resolve, join, relative, dirname } from "node:path"; @@ -213,21 +214,22 @@ export default extendConfig( "meta", { name: "twitter:image", content: "https://media.docs.plane.so/logo/og-docs.webp#hero" }, ], + + // Organization + WebSite JSON-LD (shared identity from packages/theme) + siteJsonLd({ + name: "Plane Developer Docs", + url: "https://developers.plane.so", + description: + "Developer documentation for Plane: REST API reference, webhooks, MCP server, OAuth apps, and self-hosting guides.", + }), ], transformPageData(pageData: PageData) { const head = (pageData.frontmatter.head ??= []); - // Inject canonical URL if not already defined in frontmatter - const hasCanonical = (head as HeadConfig[]).some( - ([tag, attrs]) => tag === "link" && attrs?.rel === "canonical", - ); - if (!hasCanonical) { - const canonicalUrl = `https://developers.plane.so/${pageData.relativePath}` - .replace(/index\.md$/, "") - .replace(/\.md$/, ""); - head.push(["link", { rel: "canonical", href: canonicalUrl }]); - } + // Canonical URL per page (skipped when frontmatter already sets one) + const canonical = canonicalLink("https://developers.plane.so", pageData); + if (canonical) head.push(canonical); // Inject frontmatter keywords as a meta tag (VitePress doesn't do this natively) const keywords = pageData.frontmatter.keywords; diff --git a/apps/docs/docs/.vitepress/config.ts b/apps/docs/docs/.vitepress/config.ts index ebeae667..ad4ab588 100644 --- a/apps/docs/docs/.vitepress/config.ts +++ b/apps/docs/docs/.vitepress/config.ts @@ -6,6 +6,7 @@ import { defineConfig } from "vitepress"; import { extendConfig } from "@voidzero-dev/vitepress-theme/config"; import { tabsMarkdownPlugin } from "vitepress-plugin-tabs"; import llmstxt from "vitepress-plugin-llms"; +import { canonicalLink, siteJsonLd } from "@plane/docs-theme/seo"; function loadEnvVar(key: string): string | undefined { // process.env takes precedence (CI/hosting platforms set vars here) @@ -211,6 +212,13 @@ const config = defineConfig({ content: "index, follow", }, ], + // Organization + WebSite JSON-LD (shared identity from packages/theme) + siteJsonLd({ + name: "Plane Docs", + url: "https://docs.plane.so", + description: + "Product documentation for Plane: workspaces, projects, work items, cycles, modules, pages, integrations, importers, automations, and Plane AI.", + }), ], themeConfig: { @@ -785,6 +793,12 @@ const config = defineConfig({ // Enables per-page git timestamps used for sitemap (and the // "Last updated" footer). Without this, sitemap entries omit lastmod. + transformPageData(pageData) { + // Canonical URL per page (skipped when frontmatter already sets one) + const canonical = canonicalLink("https://docs.plane.so", pageData); + if (canonical) (pageData.frontmatter.head ??= []).push(canonical); + }, + lastUpdated: true, sitemap: { diff --git a/packages/theme/README.md b/packages/theme/README.md index c077c3f6..63605ec0 100644 --- a/packages/theme/README.md +++ b/packages/theme/README.md @@ -44,6 +44,7 @@ the repo root); changes hot-reload through the workspace link. `pnpm check:types src/ index.ts createPlaneTheme(options) + client setup (appearance sync, medium-zoom, tab hashes) options.ts PlaneThemeOptions / planeOptionsKey + seo.ts build-time helpers for the VitePress configs (`@plane/docs-theme/seo`): Organization/WebSite JSON-LD, canonical link layout/ Layout.vue, doc-layout.vue (PlaneHeader + bordered content wrapper), slots/header helpers components/ PlaneHeader, CopyPageMenu, CookieConsent, Card, CardGroup, Tags, brand icons css/ index.css → fonts, tokens, base, layout, components, api diff --git a/packages/theme/package.json b/packages/theme/package.json index 583a9731..3753760b 100644 --- a/packages/theme/package.json +++ b/packages/theme/package.json @@ -6,6 +6,7 @@ "type": "module", "exports": { ".": "./src/index.ts", + "./seo": "./src/seo.ts", "./package.json": "./package.json" }, "scripts": { diff --git a/packages/theme/src/seo.ts b/packages/theme/src/seo.ts new file mode 100644 index 00000000..11ca5bf0 --- /dev/null +++ b/packages/theme/src/seo.ts @@ -0,0 +1,83 @@ +/** + * Build-time SEO helpers shared by both sites. Imported by the VitePress + * configs as `@plane/docs-theme/seo`; nothing here ships to the browser. + */ +import type { HeadConfig, PageData } from "vitepress"; + +const PLANE_ORGANIZATION_ID = "https://plane.so/#organization"; + +/** + * Plane as a schema.org Organization. Single source of truth so both sites + * publish the same identity (name, logo, profiles, contact points). + */ +export const planeOrganization = { + "@type": "Organization", + "@id": PLANE_ORGANIZATION_ID, + name: "Plane", + url: "https://plane.so", + logo: "https://media.docs.plane.so/logo/new-logo-white.png", + sameAs: ["https://github.com/makeplane", "https://x.com/planepowers"], + contactPoint: [ + { + "@type": "ContactPoint", + contactType: "customer support", + email: "support@plane.so", + url: "https://docs.plane.so/support/get-help", + availableLanguage: "English", + }, + { + "@type": "ContactPoint", + contactType: "sales", + email: "sales@plane.so", + availableLanguage: "English", + }, + ], +}; + +export interface SiteIdentity { + /** Site name, e.g. "Plane Docs". */ + name: string; + /** Origin without a trailing slash, e.g. "https://docs.plane.so". */ + url: string; + /** One-sentence description of what the site covers. */ + description: string; +} + +/** + * `