Skip to content

Commit 07d01be

Browse files
authored
docs: reorganize Orleans documentation navigation (#10336)
* docs: restore authoritative deployment links Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7e07faee-c877-4089-8032-a3898f7a6bf0 * docs: align deployment guidance with canonical policy Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7e07faee-c877-4089-8032-a3898f7a6bf0 * fix(samples): validate App Service configuration Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 7e07faee-c877-4089-8032-a3898f7a6bf0 * docs: reorganize Orleans documentation navigation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be838b5-7660-444e-b2c5-bf2e9b373472 * test(docs): enforce documentation project policy Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be838b5-7660-444e-b2c5-bf2e9b373472 * docs: correct integrated guidance examples Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be838b5-7660-444e-b2c5-bf2e9b373472 * test(docs): harden package policy validation Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be838b5-7660-444e-b2c5-bf2e9b373472 * docs: resolve documentation API references Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be838b5-7660-444e-b2c5-bf2e9b373472 * docs: correct scaling and memory guidance Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com> Copilot-Session: 9be838b5-7660-444e-b2c5-bf2e9b373472 --------- Copilot-Session: 7e07faee-c877-4089-8032-a3898f7a6bf0 Copilot-Session: 9be838b5-7660-444e-b2c5-bf2e9b373472
1 parent f2c317e commit 07d01be

81 files changed

Lines changed: 1147 additions & 2042 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

docs/AGENTS.md

Lines changed: 32 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,21 @@
11
# Orleans documentation guidance
22

3-
These rules apply to documentation and samples under `docs/`.
3+
These rules apply recursively to documentation, snippets, and samples under
4+
`docs/`. The repository-level guidance also applies.
45

56
## Code examples
67

78
- Put reusable examples in a `snippets` project near the page that consumes them.
89
- Include snippets with `:::code` and named snippet regions instead of duplicating fenced code in Markdown.
9-
- Make examples self-contained: declare builders, configuration, services, and values used by the displayed region.
10+
- Keep snippets minimal, complete, and current. Make examples self-contained:
11+
declare builders, configuration, services, and values used by the displayed
12+
region.
1013
- Compile every affected snippet project. Don't publish pseudo-code as if it were a copyable example.
1114
- Maintained documentation and snippet projects target `net10.0`.
1215
- Every `Microsoft.Orleans.*` package reference must use the approved version `10.2.2`. Keep the Orleans package family aligned and centralize versions where the project structure supports it.
13-
- Use an older Orleans package only for a narrow migration example whose purpose requires that version, and document the reason next to the exception.
16+
- Use an older Orleans package only for a narrow migration example whose purpose
17+
requires that version. Keep it under `migration` and document the reason in
18+
`OrleansDocumentationVersionException` in that project.
1419
- Keep direct dependency versions at or above the minimums required by the selected Orleans packages.
1520
- Don't demonstrate an unreleased API using an older package that doesn't contain it. Link to its API reference until a compilable source- or package-based example is available.
1621

@@ -29,7 +34,9 @@ These rules apply to documentation and samples under `docs/`.
2934
- Link public .NET symbols to generated API documentation using DocFX xref syntax instead of formatting the symbol only as inline code.
3035
- Use `<xref:Namespace.Type>` for types and `<xref:Namespace.Type.Member*>` for members or overload groups.
3136
- Add `?displayProperty=nameWithType` when the fully qualified display name improves clarity.
32-
- Use inline code for literals, configuration keys, provider names, and syntax that isn't a linkable public symbol.
37+
- Use inline code for literals, configuration values, CLI commands, filenames,
38+
provider names, and syntax that isn't a linkable public symbol. Avoid
39+
repeatedly linking a symbol when an earlier contextual link is clearer.
3340
- Confirm the xref target exists in the generated API surface before publishing.
3441

3542
Examples:
@@ -44,13 +51,32 @@ Configure it with <xref:Orleans.Hosting.AzureTableSiloBuilderExtensions.AddAzure
4451

4552
- Keep ordinary conceptual and how-to documentation timeless. Name Orleans releases only in migration or upgrade guidance where the release boundary matters.
4653
- Document implemented behavior and verified limitations. Don't promise planned capabilities.
54+
- Prefer correcting or enhancing useful content over deleting it. Preserve
55+
authoritative references during rewrites, and remove them only when obsolete,
56+
redundant, or replaced with a clearer current source.
4757
- Preserve valuable architecture and implementation detail, and keep it distinct from conceptual and task-oriented how-to guidance.
4858
- Treat hub pages as overviews: link to peer detail pages instead of singling out one provider or feature for inline configuration guidance.
4959
- Preserve stable URLs and anchors when moving content, or provide an explicit redirect or compatibility anchor.
5060

61+
## Sources and generated output
62+
63+
- Keep recursive includes within the documentation source tree. Missing,
64+
circular, traversal, absolute, drive-relative, or symlink-escaping includes
65+
are invalid. Edit the include source and ensure active includes participate in
66+
link validation.
67+
- Don't hand-edit generated site output, generated API data, dependency folders,
68+
or build output such as generated `.mdx` siblings, `dist`, `node_modules`,
69+
`bin`, or `obj`.
70+
5171
## Validation
5272

53-
- Build every changed snippet project with `dotnet build`.
73+
- Run the parser-backed link, include, redirect, navigation, and project-policy
74+
checks that cover the changed content.
75+
- Build every changed snippet project with `dotnet build`, and run
76+
`docs/site/src/content/docs/validate-snippets.ps1` when snippet or project
77+
policy changes.
78+
- Run `samples/Validate-Samples.ps1` when maintained samples change.
5479
- When `docs/Docs.slnx` is present after integration, build it as the aggregate documentation project.
55-
- From `docs/site`, run `npm run validate`.
80+
- From `docs/site`, run `npm run validate`, including redirect and rendered
81+
output auditing.
5682
- Check `git diff --check` before committing.

docs/site/README.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,9 @@ and on manual dispatch.
5757
The production build also emits compatibility redirects for every URL in the
5858
legacy `gh-pages` sitemap. Existing pages retain their exact URL when the new
5959
site owns it; retired documentation and blog URLs redirect to the nearest
60-
current documentation entry point.
60+
current documentation entry point. Explicit replacements in
61+
`src/data/redirects.json` preserve inbound anchors and override the automatic
62+
legacy-path matching.
6163

6264
`npm run audit:output` scans the complete rendered site for duplicate or missing
6365
page headings, leaked Microsoft Learn directives, malformed API signatures and

docs/site/astro.config.mjs

Lines changed: 0 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -10,22 +10,10 @@ import { remarkVersionZones } from './src/plugins/remark-version-zones.mjs';
1010
const sidebar = await createSidebar(new URL('./src/content/docs/toc.yml', import.meta.url));
1111
const buildConcurrency = Number(process.env.ORLEANS_DOCS_BUILD_CONCURRENCY) || 4;
1212
sidebar.unshift({ label: 'Documentation', link: '/docs/' });
13-
sidebar.push({
14-
label: 'Reference',
15-
items: [
16-
{ label: 'Samples', link: '/samples/' },
17-
{ label: 'C# API reference', link: '/docs/api/csharp/' },
18-
],
19-
});
2013

2114
export default defineConfig({
2215
site: 'https://dotnet.github.io/orleans/',
2316
base: '/orleans/',
24-
redirects: {
25-
'/docs/deployment/consul-deployment': '/orleans/docs/host/configuration-guide/clustering/consul/',
26-
'/docs/tutorials-and-samples/overview-helloworld': '/orleans/docs/tutorials-and-samples/hello-world/',
27-
'/docs/tutorials-and-samples/tutorial-1': '/orleans/docs/tutorials-and-samples/hello-world/',
28-
},
2917
integrations: [
3018
starlight({
3119
title: 'Microsoft Orleans',

docs/site/package-lock.json

Lines changed: 4 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

docs/site/package.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,11 @@
3232
"devDependencies": {
3333
"@astrojs/check": "0.9.9",
3434
"@types/node": "26.1.1",
35+
"hast-util-from-html": "2.0.3",
36+
"remark-parse": "11.0.0",
3537
"typescript": "6.0.3",
38+
"unified": "11.0.5",
39+
"unist-util-visit": "5.1.0",
3640
"vitest": "4.1.10"
3741
}
3842
}

docs/site/scripts/audit-output.mjs

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@ import { readFile, readdir, stat } from 'node:fs/promises';
22
import path from 'node:path';
33
import { fileURLToPath } from 'node:url';
44
import legacyPaths from '../src/data/legacy-pages.json' with { type: 'json' };
5+
import redirects from '../src/data/redirects.json' with { type: 'json' };
6+
import { compatibilityOutputPath } from './lib/compatibility-paths.mjs';
57

68
const siteRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
79
const distRoot = path.join(siteRoot, 'dist');
@@ -96,13 +98,24 @@ if (files.includes(snippetReadme)) {
9698
}
9799

98100
for (const legacyPath of legacyPaths) {
99-
const relativeLegacy = decodeURIComponent(legacyPath.slice('/orleans/'.length));
100-
const outputPath = path.join(distRoot, relativeLegacy);
101+
const outputPath = compatibilityOutputPath(legacyPath, distRoot);
101102
if (!files.includes(outputPath)) {
102103
failures.push(`Missing legacy Pages compatibility path '${legacyPath}'.`);
103104
}
104105
}
105106

107+
for (const [source, target] of Object.entries(redirects)) {
108+
const outputPath = compatibilityOutputPath(source, distRoot);
109+
if (!files.includes(outputPath)) {
110+
failures.push(`Missing explicit compatibility redirect '${source}'.`);
111+
continue;
112+
}
113+
const html = await readFile(outputPath, 'utf8');
114+
if (!/http-equiv="refresh"/i.test(html) || !html.includes(target) || !html.includes('location.hash')) {
115+
failures.push(`Compatibility path '${source}' is not an anchor-preserving redirect to '${target}'.`);
116+
}
117+
}
118+
106119
if (failures.length > 0) {
107120
console.error(`Rendered output audit found ${failures.length} issue(s):`);
108121
for (const failure of failures.slice(0, 100)) {

docs/site/scripts/generate-legacy-redirects.mjs

Lines changed: 25 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@ import { access, mkdir, readdir, writeFile } from 'node:fs/promises';
22
import path from 'node:path';
33
import { fileURLToPath } from 'node:url';
44
import legacyPaths from '../src/data/legacy-pages.json' with { type: 'json' };
5+
import redirects from '../src/data/redirects.json' with { type: 'json' };
6+
import { compatibilityOutputPath, deploymentBase } from './lib/compatibility-paths.mjs';
57

68
const siteRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), '..');
79
const distRoot = path.join(siteRoot, 'dist');
8-
const deploymentBase = '/orleans';
910

1011
async function walk(directory) {
1112
const files = [];
@@ -71,22 +72,39 @@ const conceptualRoutes = [...currentRoutes].filter(
7172

7273
let written = 0;
7374
let preserved = 0;
74-
for (const legacyPath of legacyPaths) {
75+
for (const [source, target] of Object.entries(redirects)) {
76+
if (!source.startsWith(`${deploymentBase}/`) || source.includes('..')) {
77+
throw new Error(`Unsafe redirect source '${source}'.`);
78+
}
79+
if (!currentRoutes.has(target)) {
80+
throw new Error(`Redirect target '${target}' for '${source}' is not a current route.`);
81+
}
82+
}
83+
84+
for (const legacyPath of new Set([...legacyPaths, ...Object.keys(redirects)])) {
7585
if (!legacyPath.startsWith(`${deploymentBase}/`) || legacyPath.includes('..')) {
7686
throw new Error(`Unsafe legacy Pages path '${legacyPath}'.`);
7787
}
7888

79-
const relative = decodeURIComponent(legacyPath.slice(`${deploymentBase}/`.length));
80-
const outputPath = path.join(distRoot, relative);
89+
const outputPath = compatibilityOutputPath(legacyPath, distRoot);
90+
let exists = true;
8191
try {
8292
await access(outputPath);
93+
} catch (error) {
94+
if (error?.code !== 'ENOENT') {
95+
throw error;
96+
}
97+
exists = false;
98+
}
99+
if (exists && Object.hasOwn(redirects, legacyPath)) {
100+
throw new Error(`Explicit redirect source '${legacyPath}' is still served by a current page.`);
101+
}
102+
if (exists) {
83103
preserved += 1;
84104
continue;
85-
} catch {
86-
// Generate a compatibility page when the new site does not already own this exact path.
87105
}
88106

89-
const target = targetFor(legacyPath, currentRoutes, conceptualRoutes);
107+
const target = redirects[legacyPath] ?? targetFor(legacyPath, currentRoutes, conceptualRoutes);
90108
const document = `<!doctype html>
91109
<html lang="en">
92110
<head>
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
import path from 'node:path';
2+
3+
export const deploymentBase = '/orleans';
4+
5+
export function compatibilityOutputPath(route, outputRoot) {
6+
const prefix = `${deploymentBase}/`;
7+
if (typeof route !== 'string' || !route.startsWith(prefix)) {
8+
throw new Error(`Compatibility path '${route}' is outside the deployment base.`);
9+
}
10+
11+
let relativeRoute;
12+
try {
13+
relativeRoute = decodeURIComponent(route.slice(prefix.length));
14+
} catch {
15+
throw new Error(`Compatibility path '${route}' has invalid URL encoding.`);
16+
}
17+
18+
if (relativeRoute.includes('\0') || relativeRoute.includes('\\')) {
19+
throw new Error(`Compatibility path '${route}' contains an unsafe path separator or character.`);
20+
}
21+
if (
22+
path.isAbsolute(relativeRoute) ||
23+
path.posix.isAbsolute(relativeRoute) ||
24+
path.win32.isAbsolute(relativeRoute)
25+
) {
26+
throw new Error(`Compatibility path '${route}' contains an absolute path.`);
27+
}
28+
29+
const isDirectoryRoute = relativeRoute.length === 0 || relativeRoute.endsWith('/');
30+
const segments = relativeRoute.split('/');
31+
const pathSegments = isDirectoryRoute ? segments.slice(0, -1) : segments;
32+
if (
33+
pathSegments.some(
34+
(segment) =>
35+
segment.length === 0 || segment === '.' || segment === '..' || segment.includes(':'),
36+
)
37+
) {
38+
throw new Error(`Compatibility path '${route}' contains an unsafe path segment.`);
39+
}
40+
41+
const resolvedRoot = path.resolve(outputRoot);
42+
const outputPath = path.resolve(
43+
resolvedRoot,
44+
...pathSegments,
45+
...(isDirectoryRoute ? ['index.html'] : []),
46+
);
47+
const relativeOutput = path.relative(resolvedRoot, outputPath);
48+
if (
49+
relativeOutput.length === 0 ||
50+
relativeOutput === '..' ||
51+
relativeOutput.startsWith(`..${path.sep}`) ||
52+
path.isAbsolute(relativeOutput)
53+
) {
54+
throw new Error(`Compatibility path '${route}' resolves outside the output directory.`);
55+
}
56+
57+
return outputPath;
58+
}

0 commit comments

Comments
 (0)