This site is the official documentation and single source of truth for the tether.io Documentation guild:
- Source code and content of the docs website
- Automation scripts for the integration between the codebase and the documentation
The site is a static export from a Next.js + Fumadocs app (output: 'export'). SEO behavior is implemented with workspace packages under @tether/docs-* (see below).
Prerequisites:
- Node.js >= 22.22.0
npm>= 10.9.2
Install dependencies:
npm installpostinstall runs fumadocs-mdx, which generates .source/ (gitignored). Run install after clone and after editing source.config.ts.
Copy the example file and fill in values (Next.js reads .env.local automatically):
cp env.example .env.localSee env.example for all variables. Required for production SEO:
NEXT_PUBLIC_DOCS_ORIGIN— public docs URL for canonical and social metadata (seo-config.ts); optional for local dev (defaults tohttp://localhost:3001)
Optional — Inkeep: set NEXT_PUBLIC_INKEEP_API_KEY only when you want Inkeep for search (replacing the Fumadocs default dialog) and the chat widget. If it is unset, the app uses Fumadocs’ default search and hides Inkeep-specific UI (provider.tsx, layout.tsx, page-actions.tsx).
Note
npm run prebuild runs tsx outside Next.js, so DOCS_OG_* and SKIP_OG_BUILD are not read from .env.local unless you export them in your shell or set them in CI.
| Package | Role |
|---|---|
@tether/docs-seo-schema |
Zod tetherSeoFrontmatterSchema: required description; optional noIndex, ogImage, schemaType, docType, lastModified. Exports warnMissingSeoFrontmatterFields, DOCS_SEO_WARN_PREFIX. |
@tether/docs-seo-core |
getPageSeoState (canonical, ogImage override, PageSeoState with slugs / seoAuditFields), buildJsonLdGraph, inferJsonLdType. Root-relative ogImage paths are turned into absolute URLs without applying doc trailingSlash (so /asset.png does not become /asset.png/). |
@tether/docs-seo-next |
buildDocsMetadata, buildDocsSitemap, buildDocsRobots, DocsJsonLd; re-exports core/schema SEO helpers. |
@tether/docs-seo-og |
getPageImage (URL layout), Takumi prebuild (@tether/docs-seo-og/build), optional Route Handler (@tether/docs-seo-og/handler); re-exports warnMissingSeoFrontmatterFields. |
The main app must not import @tether/docs-seo-og/handler in pages that ship to the static bundle; Takumi is only used at prebuild time or in a server Route Handler.
@tetherto/docs-seo-* is published to GitHub Packages, not the public npm registry. Consumers install it like any normal npm dependency (@tetherto/docs-seo-next: ^1.1.0) once npm is pointed at the right registry and authenticated with a personal access token.
Add an .npmrc to the consumer repo so the @tetherto scope is fetched from GitHub Packages and the token is read from a GITHUB_TOKEN environment variable:
@tetherto:registry=https://npm.pkg.github.com
//npm.pkg.github.com/:_authToken=${GITHUB_TOKEN}Commit this file. It contains no secret — only the variable reference.
Create a classic PAT at https://github.com/settings/tokens/new with these scopes:
read:packages— required, to download@tetherto/*from GitHub Packagesrepo— required if any of the source repositories that publish those packages are private (otherwise GitHub returns403even with the right scope)
Important: set an expiration on the token. Tokens that never expire are rejected by the registry. Pick the shortest expiration that fits your workflow; you'll be regenerating it.
After creating the token, if the tetherto organization uses SSO, click "Configure SSO" next to the token in the tokens list and authorize it for the org. Without that step the registry returns 403 even with the right scopes.
A fine-grained personal access token also works, scoped to the tetherto resource owner with Packages: Read repository permission. Same expiration requirement applies.
Add GITHUB_TOKEN=ghp_yourTokenHere to a gitignored .env (or .env.local) file. Keep it readable only by you (chmod 600 .env). Never commit the token.
Quick sanity check:
set -a && . ./.env && set +a
npm whoami --registry=https://npm.pkg.github.com # should print your GitHub username
npm view @tetherto/docs-seo-schema version --registry=https://npm.pkg.github.comIf npm whoami prints your username but the second command 403s, the token authenticates but lacks read:packages (and/or SSO authorization).
set -a && . ./.env && set +a
npm installCI sets GITHUB_TOKEN directly from secrets (e.g. the built-in ${{ secrets.GITHUB_TOKEN }} for org-internal workflows, or a PAT secret for cross-org reads) — no .env file involved.
- Add the packages you actually import (
@tetherto/docs-seo-next,@tetherto/docs-seo-og,@tetherto/docs-seo-schema); transitive@tetherto/*deps resolve automatically. - Sources are TypeScript and shipped as-is, so set
transpilePackagesin Next.js to include every@tetherto/docs-seo-*package you import. - Install peer dependencies (
fumadocs-core,next,react,zodfor schema, etc.) to versions compatible with each package'speerDependencies.
Extended fields are merged in source.config.ts via tetherSeoFrontmatterSchema. description is required (non-empty after trim) on every docs page for meta tags, Open Graph / Twitter, and JSON-LD. Other fields are optional:
noIndex(boolean): exclude from sitemap and setrobotsto noindex.ogImage(string): absolute URL or site-relative path override for Open Graph / Twitter images (relative paths are normalized as static assets, not doc routes—no stray trailing slash before the file extension).schemaType:TechArticle|APIReference|WebPagefor JSON-LD@type.docType:tutorial|how-to|reference|explanation|page|faq|getting-started(influences inferred JSON-LD whenschemaTypeis omitted).lastModified: string or date for sitemaplastmodand, when set, JSON-LDdatePublished/dateModifiedonWebPage,TechArticle, andAPIReferencegraphs.
Per-page metadata, sitemap, robots, and JSON-LD share the same logic through src/lib/seo-config.ts and @tether/docs-seo-next.
During next build / dev, getPageSeoState and buildDocsMetadata emit [@tether/docs-seo] console.warn lines for missing optional fields (ogImage, schemaType, docType, lastModified, and empty description if it bypasses MDX validation). Warnings are deduped per page per Node process. Two env knobs control them:
DOCS_SEO_SILENT=1— silence ALL warnings (including the required-descriptionwarning). Use sparingly.DOCS_SEO_QUIET_GENERATED=1— silence only the warnings for fields that have sensible auto-generated/inferred defaults (ogImage,schemaType,lastModified).descriptionanddocTypewarnings stay loud because neither has a useful default. Recommended when you opt into the Takumi OG prebuild + thefumadocs-mdxlastModifiedplugin.
The same helpers are re-exported from @tether/docs-seo-schema, @tether/docs-seo-core, @tether/docs-seo-next, and @tether/docs-seo-og (warnMissingSeoFrontmatterFields).
When using the SEO OG packages from a remote docs site:
- Clone with submodules (or after clone: git submodule update --init --recursive) so src/lib/docs-template is populated
- Run
npm installso workspaces link the @tetherto/docs-seo-* packages from the submodule - Run
npm run build(or whatever runs prebuild) sogenerate-takumi-og.tsxruns and fills public/og/docs/... for each page - CI / deploy, either:
- Checkout with submodules (same as local), or
- Keep using GITHUB_TOKEN / GitHub App flow from the README to install from GitHub Packages if the submodule isn’t used in that environment
- Consider adding a fallback, e.g. MDK docs has /og-default.webp if the per-page OG files are missing
Full template with comments: env.example.
| Variable | Required | Purpose |
|---|---|---|
NEXT_PUBLIC_DOCS_ORIGIN |
Yes (production) | Site origin for metadataBase, canonicals, and absolute og:image / Twitter URLs. Defaults to http://localhost:3001 when unset. |
NEXT_PUBLIC_INKEEP_API_KEY |
No | Inkeep CXKit API key. If set, replaces Fumadocs default search with Inkeep and enables the Inkeep chat widget; if unset, default Fumadocs search is used. |
NEXT_PUBLIC_DOCS_PUBLISHER_LOGO_URL |
No | HTTPS logo for JSON-LD publisher / Organization. |
SKIP_OG_BUILD |
No | Set to 1 to use static OG fallback instead of per-page public/og/docs/** URLs in metadata. |
DOCS_OG_SITE_LABEL |
No | Takumi site label during OG prebuild (default Tether). |
DOCS_OG_CONCURRENCY |
No | Parallelism for OG prebuild (default 3). |
DOCS_SEO_SILENT |
No | Set to 1 to disable ALL [@tether/docs-seo] console warnings (including required-description checks). |
DOCS_SEO_QUIET_GENERATED |
No | Set to 1 to silence only the warnings for fields with auto-generated / inferred defaults (ogImage, schemaType, lastModified). Keeps description and docType warnings live. |
Because static export cannot use dynamic OG Route Handlers, images are generated before next build and written under public/og/docs/.../image.webp, matching the URLs returned by getPageImage().
npm run buildandnpm run build:staticautomatically runprebuild, which executestsx scripts/generate-takumi-og.mts- Run the generator alone:
npm run build:og - Replace
public/og-default.pngwith a proper 1200×630 asset if you rely on theSKIP_OG_BUILDfallback
Git: This template gitignores public/og/docs/ (see .gitignore). CI and local npm run build must run prebuild so those WebP files exist before static export. To vendor generated images instead, stop ignoring that directory and commit the files.
Check broken links:
npm run check-linksDev server (port 3001):
npm run devFor local dev without generating OG files, you can use:
SKIP_OG_BUILD=1 npm run devSet NEXT_PUBLIC_DOCS_ORIGIN for production; add NEXT_PUBLIC_INKEEP_API_KEY only if you use Inkeep instead of default Fumadocs search (see env.example).
Static export:
npm run buildor:
npm run build:staticNext.js writes the static site to the out/ directory (not dist/).
Serve the export locally after npm run build:
npm run serveThis serves the out/ directory (Next.js static export output).
src: Next.js app and UIcontent/docs: MDX documentation contentpackages: workspace packages (@tether/docs-seo-*)public: static assets;public/og/docs/**holds prebuilt OG WebP files afterprebuildexamples: runnable DOCS code samples for snippets and toolingscripts: automation (includingscripts/generate-takumi-og.mts)env.example: environment variable template (SEO required for prod; Inkeep optional)REVIEW-CHECKLIST.md: optional manual QA checklist for SEO / static export (stage it if you want it in the repo).source/(gitignored, not in git): Fumadocs MDX output; created bynpm install/npm run postinstall. Regenerate after changingsource.config.ts
Note
Repository structure may evolve as automation and content organization mature.