Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
9fd9b41
feat(docs): fan out shared content files to per-SDK URLs
pratyush618 Jul 10, 2026
045b460
feat(docs): resolve SDK from URL in boot script
pratyush618 Jul 10, 2026
a864e52
feat(docs): add content-parity CI gate
pratyush618 Jul 10, 2026
0278ca0
docs: document shared-content authoring rules
pratyush618 Jul 10, 2026
64db2c7
docs: migrate core guides to shared pages
pratyush618 Jul 10, 2026
9057a87
docs: migrate reliability guides to shared pages
pratyush618 Jul 10, 2026
acb3d48
docs: migrate workflow guides to shared pages
pratyush618 Jul 10, 2026
5c66ed9
docs: redirect renamed locking and sagas pages
pratyush618 Jul 10, 2026
811f2ed
docs: migrate extensibility guides to shared pages
pratyush618 Jul 10, 2026
346d2ba
docs: migrate sentry and prometheus guides to shared
pratyush618 Jul 10, 2026
7fd7085
docs: migrate interception guide to shared page
pratyush618 Jul 10, 2026
067aab3
docs: migrate operations guides to shared pages
pratyush618 Jul 10, 2026
f13c01e
docs: migrate quickstart and benchmark to shared pages
pratyush618 Jul 10, 2026
4241c03
docs: clarify predicate denial outcomes
pratyush618 Jul 10, 2026
00031c9
docs: scope prometheus alert rules per SDK
pratyush618 Jul 10, 2026
f6bdec5
docs: keep java scaler quick start alive
pratyush618 Jul 10, 2026
d5f98e4
docs: start mesh worker in compose entrypoint example
pratyush618 Jul 10, 2026
196a5b9
docs: qualify concurrency guarantee for timeout retries
pratyush618 Jul 10, 2026
edfc040
docs: use atomic claim in notify idempotency example
pratyush618 Jul 10, 2026
58f5bea
docs: define node idempotency producer values
pratyush618 Jul 10, 2026
f991085
docs: make chord result passing SDK-specific
pratyush618 Jul 10, 2026
a39aa6f
docs: compensate only completed chain steps
pratyush618 Jul 10, 2026
da1f6e4
docs: make gate webhook examples fail closed
pratyush618 Jul 10, 2026
8290671
docs: fix java saga example statements
pratyush618 Jul 10, 2026
f77f738
docs: fix python benchmark drain and staging order
pratyush618 Jul 10, 2026
5d97d3f
docs: isolate node benchmark db and bound its drain
pratyush618 Jul 10, 2026
4c056aa
docs: bound the java benchmark drain
pratyush618 Jul 10, 2026
e9cde94
fix(docs): match Tab sdk attribute in any position
pratyush618 Jul 10, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/workflows/docs.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,9 @@ jobs:
- name: Lint
run: pnpm lint

- name: Content parity
run: pnpm check:parity

- name: Build
env:
DOCS_BASE_PATH: /taskito
Expand Down
36 changes: 36 additions & 0 deletions docs/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,39 @@ app/
Adding a page: drop an `.mdx` file under `content/docs/`, add it to the directory's
`meta.json`. It is picked up, prerendered, indexed for search, and slotted into the
sidebar automatically.

## Shared content (one file, one URL per SDK)

A file under `content/docs/shared/` mounts at the same path in **every** SDK tree:
`shared/guides/operations/mesh.mdx` serves `/python/guides/operations/mesh`,
`/node/...`, and `/java/...` from one source. Slug + fan-out logic lives in
`app/lib/doc-slugs.ts` — the runtime loader, prerender walk, manifest plugin, and
parity checks all resolve through it. Non-default-SDK mounts carry a
`<link rel="canonical">` to the default-SDK URL, and `llms.txt` lists each shared
page once.

Authoring rules for shared pages:

- **Prose is SDK-neutral.** Use `<SdkName/>` / `<SdkSwap python=… node=… java=…/>`
for language-specific words; wrap SDK-specific paragraphs in `<SdkOnly sdk="…">`.
- **Code goes in `<CodeTabs>`** with one `<Tab sdk="…">` per SDK. CI fails a
shared page whose CodeTabs misses an SDK — add `data-parity-exempt` to the tag
only when a feature genuinely doesn't exist there (prefer `SdkOnly` instead).
- **Frontmatter is shared** across all mounts — keep title/description
SDK-neutral.
- **No `meta.json` under `shared/`.** List the page name in each SDK section's
`meta.json` (that's also how an SDK opts out of a topic).
- **Collisions fail the build.** A per-SDK file at the same path as a shared file
is an error, never a silent override — delete the per-SDK file when migrating.
- **Accuracy first.** Verify every per-SDK claim against that SDK's source before
writing it; a missing tab is better than a fabricated API.

`pnpm check:parity` runs the CI gate (`scripts/parity/`): CodeTabs SDK coverage,
slug collisions, redirect shadowing, plus an informational drift report ranking
per-SDK topic pairs by word-count ratio — that report is the migration queue.
Genuinely SDK-specific pages (Django/Flask/FastAPI, Express/Fastify/Nest,
Spring/GraalVM, `postgres`, `dashboard-api`, `upgrading-0.15`, …) stay per-SDK.

Future work: extracted code snippets (region-marked files compiled/tested in CI,
inlined by a remark plugin — the `remarkPlugins` array in `vite.config.ts` is the
seam) so examples can't rot.
6 changes: 1 addition & 5 deletions docs/app/components/landing/scenario-finder.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,6 @@ const GUIDE_OVERRIDES: Record<string, Record<string, string>> = {
node: "/node/guides/core/streaming",
java: "/java/guides/core/streaming",
},
"/python/guides/workflows/sagas": {
node: "/node/guides/workflows/saga",
java: "/java/guides/workflows/saga",
},
};

/** Resolve a scenario's Python guide path to the active SDK's docs. */
Expand Down Expand Up @@ -330,7 +326,7 @@ saga.<span class="def">delay</span>() <span class="cmt"># any failure → comp
title: "Saga — compensation & rollback",
label: "Watch the saga roll back",
},
guide: { to: "/python/guides/workflows/sagas", label: "Read the guide" },
guide: { to: "/python/guides/workflows/saga", label: "Read the guide" },
},
{
id: "worksteal",
Expand Down
16 changes: 6 additions & 10 deletions docs/app/lib/content.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import type { ComponentType } from "react";
import { mountsForRelPath } from "./doc-slugs";

interface MdxModule {
default: ComponentType;
Expand All @@ -9,18 +10,13 @@ interface MdxModule {
// nav/search never pull these heavy (shiki-inflated) modules into a shared chunk.
const LOADERS = import.meta.glob<MdxModule>("../../content/docs/**/*.mdx");

function keyToSlug(key: string): string {
const rel = key.replace(/^.*\/content\/docs\//, "").replace(/\.mdx$/, "");
const parts = rel.split("/");
if (parts[parts.length - 1] === "index") {
parts.pop();
}
return `/${parts.join("/")}`.replace(/\/$/, "") || "/";
}

const BY_SLUG = new Map<string, () => Promise<MdxModule>>();
for (const [key, loader] of Object.entries(LOADERS)) {
BY_SLUG.set(keyToSlug(key), loader);
const rel = key.replace(/^.*\/content\/docs\//, "");
// A shared file registers the same loader at every SDK mount (one chunk).
for (const mount of mountsForRelPath(rel)) {
BY_SLUG.set(mount.slug, loader);
}
}

/** The dynamic import for a doc page's compiled component, or undefined if unknown. */
Expand Down
21 changes: 10 additions & 11 deletions docs/app/lib/doc-paths.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,20 @@
import { readdirSync } from "node:fs";
import { join, relative, sep } from "node:path";
import { fileURLToPath } from "node:url";
import { mountsForRelPath } from "./doc-slugs";

// Filesystem walk of the MDX content tree, used by react-router.config's
// `prerender()` to enumerate every static doc URL at build time. The runtime
// router (app/lib/content.ts) maps the same files via import.meta.glob — both
// derive slugs identically so the prerendered set matches the served routes.
// router (app/lib/content.ts) maps the same files through the same doc-slugs
// module, so the prerendered set matches the served routes — including shared
// files that fan out to one URL per SDK.

const CONTENT_DIR = fileURLToPath(
new URL("../../content/docs", import.meta.url),
);

/** `content/docs/a/b.mdx` → `/a/b`; `…/a/index.mdx` → `/a`. */
export function fileToDocPath(absFile: string): string {
const rel = relative(CONTENT_DIR, absFile).replace(/\.mdx$/, "");
const parts = rel.split(sep);
if (parts[parts.length - 1] === "index") {
parts.pop();
}
return `/${parts.join("/")}`.replace(/\/$/, "") || "/";
function toRelPath(absFile: string): string {
return relative(CONTENT_DIR, absFile).split(sep).join("/");
}

function walk(dir: string, out: string[]): void {
Expand All @@ -36,5 +32,8 @@ function walk(dir: string, out: string[]): void {
export function allDocPaths(): string[] {
const files: string[] = [];
walk(CONTENT_DIR, files);
return files.map(fileToDocPath).filter((p) => p !== "/");
return files
.flatMap((file) => mountsForRelPath(toRelPath(file)))
.map((mount) => mount.slug)
.filter((slug) => slug !== "/");
}
46 changes: 46 additions & 0 deletions docs/app/lib/doc-slugs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
// Explicit .ts extension: this module is also imported by the parity script
// under plain Node (type stripping), where extensionless specifiers don't resolve.
import { DEFAULT_SDK, SDK_IDS } from "./sdk-registry.ts";

// The single source of slug derivation and shared-content fan-out. The runtime
// loader (content.ts), the prerender walk (doc-paths.ts), the manifest plugin
// (vite-plugin-docs-manifest.ts), and the parity script all resolve content
// files through here, so the served, prerendered, and indexed URL sets can't
// drift apart. Isomorphic on purpose: operates on posix content-relative paths,
// no node imports.

/** Directory under `content/docs` whose files mount once per SDK. */
export const SHARED_DIR = "shared";

/** One URL a content file is served at. `canonical` is set on fan-out mounts
* and points at the default-SDK mount (self-referential there). */
export interface DocMount {
slug: string;
canonical?: string;
}

/** `a/b.mdx` → `/a/b`; `a/b/index.mdx` → `/a/b`. Posix content-relative path. */
export function relPathToSlug(rel: string): string {
const parts = rel.replace(/\.mdx$/, "").split("/");
if (parts[parts.length - 1] === "index") {
parts.pop();
}
return `/${parts.join("/")}`.replace(/\/$/, "") || "/";
}

/** True when the file lives under the shared (fan-out) tree. */
export function isSharedRelPath(rel: string): boolean {
return rel === SHARED_DIR || rel.startsWith(`${SHARED_DIR}/`);
}

/** Every URL a content file mounts at. A shared file fans out to one mount per
* SDK (`shared/x.mdx` → `/{sdk}/x`); anything else keeps its single 1:1 slug. */
export function mountsForRelPath(rel: string): DocMount[] {
const slug = relPathToSlug(rel);
if (!isSharedRelPath(rel)) {
return [{ slug }];
}
const topic = slug.slice(`/${SHARED_DIR}`.length); // "/guides/x" or ""
const canonical = `/${DEFAULT_SDK}${topic}`;
return SDK_IDS.map((sdk) => ({ slug: `/${sdk}${topic}`, canonical }));
}
18 changes: 13 additions & 5 deletions docs/app/lib/llms.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,26 @@
import { SEARCH_DOCS } from "./search";
import { SEARCH_DOCS, type SearchDoc } from "./search";

// Shared pages mount at one URL per SDK; list each only once, at its
// canonical (default-SDK) URL, so the corpus carries no duplicates.
function uniqueDocs(): SearchDoc[] {
return SEARCH_DOCS.filter(
(doc) => !doc.canonical || doc.canonical === doc.id,
).sort((a, b) => a.id.localeCompare(b.id));
}

/** Index of every doc page (title + URL) — the `/llms.txt` body. */
export function llmsIndex(): string {
const lines = ["# Taskito documentation", ""];
for (const doc of [...SEARCH_DOCS].sort((a, b) => a.id.localeCompare(b.id))) {
for (const doc of uniqueDocs()) {
lines.push(`- [${doc.title}](${doc.id})`);
}
return `${lines.join("\n")}\n`;
}

/** Full corpus (title + stripped body per page) — the `/llms-full.txt` body. */
export function llmsFull(): string {
const blocks = [...SEARCH_DOCS]
.sort((a, b) => a.id.localeCompare(b.id))
.map((doc) => `## ${doc.title}\nURL: ${doc.id}\n\n${doc.text}\n`);
const blocks = uniqueDocs().map(
(doc) => `## ${doc.title}\nURL: ${doc.id}\n\n${doc.text}\n`,
);
return `# Taskito documentation (full text)\n\n${blocks.join("\n---\n\n")}`;
}
3 changes: 3 additions & 0 deletions docs/app/lib/manifest.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,9 @@ export interface DocMeta {
slug: string;
title: string;
description: string;
/** Default-SDK URL of a shared page; present only on fan-out mounts.
* Kept in sync with the plugin's DocMeta across the virtual-module boundary. */
canonical?: string;
}

export const DOC_METAS: DocMeta[] = DOCS;
Expand Down
4 changes: 4 additions & 0 deletions docs/app/lib/redirects.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ export const REDIRECTS: Record<string, string> = {
"/python/more/comparison": "/resources/comparison",
"/python/more/faq": "/resources/faq",
"/python/more/changelog": "/resources/changelog",
// Shared-content migration: python's old "locking" and "sagas" names
// normalized to the canonical "locks" / "saga" slugs shared with node/java.
"/python/guides/reliability/locking": "/python/guides/reliability/locks",
"/python/guides/workflows/sagas": "/python/guides/workflows/saga",
};

/** The destination for a moved path, or undefined if it isn't a redirect. */
Expand Down
3 changes: 3 additions & 0 deletions docs/app/lib/search.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ export interface SearchDoc {
section: string;
description: string;
text: string;
/** Default-SDK URL when this entry is an SDK mount of a shared page. */
canonical?: string;
}

function sectionOf(slug: string): string {
Expand All @@ -30,6 +32,7 @@ export const SEARCH_DOCS: SearchDoc[] = DOC_METAS.map((d) => ({
section: sectionOf(d.slug),
description: d.description,
text: `${d.description} ${humanizeSlug(d.slug)}`.trim(),
canonical: d.canonical,
}));

let index: MiniSearch<SearchDoc> | null = null;
Expand Down
17 changes: 12 additions & 5 deletions docs/app/root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,19 @@ export const links: Route.LinksFunction = () => [
// Apply the persisted theme before paint to avoid a light/dark flash.
const THEME_INIT = `(function(){try{var t=localStorage.getItem('taskito-theme')||'dark';document.documentElement.setAttribute('data-theme',t);}catch(e){}})();`;

// Registry ids + default as inert JSON on the <html> data attribute below.
const SDK_CONFIG = JSON.stringify({ ids: [...SDK_IDS], def: DEFAULT_SDK });
// Registry ids + default + deploy base as inert JSON on the <html> data
// attribute below; the base lets the boot script find the URL's SDK segment.
const SDK_CONFIG = JSON.stringify({
ids: [...SDK_IDS],
def: DEFAULT_SDK,
base: import.meta.env.BASE_URL.replace(/\/$/, ""),
});

// No-flash SDK bootstrap: query > localStorage > default, validated against the
// data-sdk-config above. Static string (no interpolation); keeps the SSR default on error.
const SDK_INIT = `(function(){try{var c=JSON.parse(document.documentElement.getAttribute('data-sdk-config')),ids=c.ids,def=c.def;var u=new URLSearchParams(location.search).get('sdk');var s=ids.indexOf(u)>=0?u:(localStorage.getItem('taskito-sdk')||def);if(ids.indexOf(s)<0){s=def;}document.documentElement.setAttribute('data-sdk',s);}catch(e){}})();`;
// No-flash SDK bootstrap: URL prefix > query > localStorage > default — same
// precedence as useActiveSdk, so shared pages (one file mounted per SDK) paint
// the URL's SDK variants immediately. Static string (no interpolation); keeps
// the SSR default on error.
const SDK_INIT = `(function(){try{var c=JSON.parse(document.documentElement.getAttribute('data-sdk-config')),ids=c.ids,def=c.def;var seg=location.pathname.slice(c.base.length).split('/')[1];var u=new URLSearchParams(location.search).get('sdk');var s=ids.indexOf(seg)>=0?seg:ids.indexOf(u)>=0?u:(localStorage.getItem('taskito-sdk')||def);if(ids.indexOf(s)<0){s=def;}document.documentElement.setAttribute('data-sdk',s);}catch(e){}})();`;

export function Layout({ children }: { children: React.ReactNode }) {
return (
Expand Down
15 changes: 13 additions & 2 deletions docs/app/routes/docs.$.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { MDXProvider } from "@mdx-js/react";
import { Fragment, lazy, Suspense, useEffect, useMemo } from "react";
import { Link, useNavigate } from "react-router";
import { Link, type MetaDescriptor, useNavigate } from "react-router";
import { PrevNext } from "@/components/docs";
import { mdxComponents } from "@/components/mdx";
import { getDocLoader } from "@/lib/content";
Expand Down Expand Up @@ -59,10 +59,21 @@ export function meta({ params }: Route.MetaArgs) {
];
}
const meta = docMeta(path);
return [
const tags: MetaDescriptor[] = [
{ title: meta?.title ? `${meta.title} | Taskito` : "Taskito" },
{ name: "description", content: meta?.description ?? "" },
];
if (meta?.canonical) {
// Shared pages mount at one URL per SDK; point search engines at the
// default-SDK mount (self-referential there — still valid).
const base = import.meta.env.BASE_URL.replace(/\/$/, "");
tags.push({
tagName: "link",
rel: "canonical",
href: `${base}${meta.canonical}`,
});
}
return tags;
}

/** Stub shown at a moved URL: meta-refresh handles direct hits, this handles
Expand Down
56 changes: 0 additions & 56 deletions docs/content/docs/java/getting-started/quickstart.mdx

This file was deleted.

Loading