Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion apps/fumadocs/AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ Every page is served under a version segment: `/docs/next/quickstart`, `/docs/1.
- **A repository link names the branch, and the snapshot pins it.** Pages write `https://github.com/suiramdev/freenary/blob/main/…`; the snapshot rewrites each one to `blob/v<X.Y.Z>/` in the copy, because that link cannot resolve at render time. `docs:check` fails a `blob/main` link inside a frozen version.
- **`/docs/*` with no version redirects** (307) to the newest release. `versionMiddleware` in `src/app/start.ts` owns it, and it runs before `llmMiddleware`. The newest release is `stableVersion()` in `src/shared/content/source.ts`: the highest `X.Y` folder, or `next` while no release exists.
- **The language rules apply to `next` alone**; the structure rules apply to every version. `check-docs.ts` skips `checkHeadings`, `checkPhrases` and `checkSentences` outside `next`.
- **A release needs its snapshot first.** A maintainer runs `bun run docs:snapshot X.Y.Z` in a pull request to `dev`; the `tag` job of `.github/workflows/release.yml` refuses a stable version whose `content/docs/X.Y` folder is missing (the `main and dev` ruleset admits no workflow push). The script derives the `X.Y` folder from the release version, pins repository links to `v<X.Y.Z>` and pins `FREENARY_VERSION=main` lines to `X.Y`; `docs:check` fails a frozen page that still carries either moving form. It stages the copy outside `content/docs` and rewrites the root `meta.json` on every run, so a repeat run finishes an interrupted one; a pre-release snapshots nothing.
- **A release needs its snapshot first, and it snapshots last.** A maintainer runs `bun run docs:snapshot X.Y.Z` in a pull request to `dev`; the `tag` job of `.github/workflows/release.yml` refuses a stable version whose `content/docs/X.Y` folder is missing (the `main and dev` ruleset admits no workflow push). The folder freezes at that merge, so a documentation change merged between it and the release reaches `next` alone and no gate reports it. The script derives the `X.Y` folder from the release version and pins the three moving forms `scripts/moving-refs.ts` holds: a repository link and a `raw.githubusercontent.com` download URL take `v<X.Y.Z>`, and a `FREENARY_VERSION=main` line takes `X.Y`. `docs:check` reads the same table, so it fails a frozen page that still carries any of them. It stages the copy outside `content/docs` and rewrites the root `meta.json` on every run, so a repeat run finishes an interrupted one; a pre-release snapshots nothing.
- **Search, the AI panel and the LLM endpoints are scoped.** `/api/search` tags each record with its version and the default dialog passes `defaultTag` — the version of the pathname, or the newest release off the docs tree, from the root loader in `src/app/routes/__root.tsx`. `/api/chat` builds one index per version and honours the request body's version only when `listVersions()` holds it. `/llms.txt` and `/llms-full.txt` serve the newest release alone.

## Content structure
Expand Down
4 changes: 2 additions & 2 deletions apps/fumadocs/content/docs/0.1/quickstart.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,8 @@ The stack runs published images, so it needs no source code. Two files are enoug

```bash
mkdir freenary && cd freenary
curl -O https://raw.githubusercontent.com/suiramdev/freenary/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/suiramdev/freenary/main/.env.example
curl -O https://raw.githubusercontent.com/suiramdev/freenary/v0.1.0/docker-compose.yml
curl -O https://raw.githubusercontent.com/suiramdev/freenary/v0.1.0/.env.example
```

Every command below runs in that directory.
Expand Down
6 changes: 3 additions & 3 deletions apps/fumadocs/content/docs/0.1/self-hosting/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Two more values name the public origins. Both default to `localhost`, which serv

Compose gives the `web` service `PUBLIC_SERVER_URL` from `BETTER_AUTH_URL`, so the browser calls the same API origin. A `PUBLIC_SERVER_URL` line of your own does nothing.

`FREENARY_VERSION` names the tag of both images. The compose default is `latest`, and the registry holds no tag of that name, so write a real tag.
`FREENARY_VERSION` names the tag of both images. The compose default is `latest`, the newest release, and it moves at each one. Name a version such as `0.1` to hold an instance on one release line.

[Configuration](/docs/self-hosting/configuration) holds every variable, its default and its refusal message.

Expand All @@ -72,8 +72,8 @@ The stack runs published images, so it needs no source code. Two files are enoug

```bash
mkdir freenary && cd freenary
curl -O https://raw.githubusercontent.com/suiramdev/freenary/main/docker-compose.yml
curl -O https://raw.githubusercontent.com/suiramdev/freenary/main/.env.example
curl -O https://raw.githubusercontent.com/suiramdev/freenary/v0.1.0/docker-compose.yml
curl -O https://raw.githubusercontent.com/suiramdev/freenary/v0.1.0/.env.example
```

A clone gives the same two files, plus the source for a [build from source](/docs/self-hosting/from-source). Every command below runs from the directory that holds the two files.
Expand Down
6 changes: 4 additions & 2 deletions apps/fumadocs/content/docs/next/contributing/releasing.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,7 @@ A maintainer runs it by hand:

### Snapshot the documentation

On a branch from `dev`, run `cd apps/fumadocs && bun run docs:snapshot X.Y.Z`, then open a pull request against `dev` and merge it. A pre-release skips this step.
On a branch from `dev`, run `cd apps/fumadocs && bun run docs:snapshot X.Y.Z`, then open a pull request against `dev` and merge it. Make it the last change before the release: a documentation change merged after it reaches `next` alone. A pre-release skips this step, and a patch release finds its folder in place and copies nothing.

</Step>

Expand Down Expand Up @@ -140,7 +140,9 @@ The documentation site publishes one copy of the pages for each version. `conten

The snapshot lands through a pull request into `dev`. The `tag` job refuses a stable version whose folder is missing. The `main and dev` ruleset admits no push outside a pull request, so the workflow writes no commit.

The copy carries two edits. A repository link names `main`, and the frozen page names the release tag. A `FREENARY_VERSION=main` line names the branch, and the frozen page names `X.Y`. Two rules follow:
The folder therefore freezes when that pull request merges, and the release comes later. A documentation change merged in between reaches `next` and never reaches the frozen folder, and no gate reports it. Snapshot last, and the window stays empty.

The copy pins every moving reference. A repository link names `main`, and the frozen page names the release tag. A download URL names `main`, and the frozen page names the tag. A `FREENARY_VERSION=main` line names the branch, and the frozen page names `X.Y`. Two rules follow:

- A patch release finds `content/docs/X.Y` in place, and copies nothing. A documentation fix for a released version therefore edits that folder directly.
- A pre-release copies nothing. Its readers stay on `next`.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@ To freeze a version by hand:
cd apps/fumadocs && bun run docs:snapshot 1.2.0
```

The script takes the release version and freezes it as the `1.2` folder. It refuses anything else, and it leaves the pages alone when the folder exists. It labels the copy and adds it to the root `meta.json`, which orders the version dropdown. It also pins each repository link in the copy to the release tag. It also pins each `FREENARY_VERSION=main` line in the copy to `X.Y`.
The script takes the release version and freezes it as the `1.2` folder. It refuses anything else, and it leaves the pages alone when the folder exists. It labels the copy and adds it to the root `meta.json`, which orders the version dropdown. It also pins every moving reference in the copy. A repository link and a download URL take the release tag, and a `FREENARY_VERSION=main` line takes `1.2`. `docs:check` fails a frozen page that still holds one of the three moving forms.

## The five steps

Expand Down Expand Up @@ -243,6 +243,8 @@ The array also accepts four special entries: `"---Separator---"`, `"..."`, `"!sl
| A link that names a version | Passes | Fails |
| A version folder with no `"root": true`, or a stale title | Passes, drops out of the dropdown | Fails |
| A `blob/main` repository link inside a frozen version | Passes | Fails |
| A `raw.githubusercontent.com` download URL that names `main`, inside a frozen version | Passes | Fails |
| A `FREENARY_VERSION=main` line inside a frozen version | Passes | Fails |
| A command or a variable inside `guides/` | Passes | Fails |
| A contraction, a banned word, a plan | Passes | Fails |
| A sentence over 25 words | Passes | Fails |
Expand Down
2 changes: 1 addition & 1 deletion apps/fumadocs/content/docs/next/self-hosting/index.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ Two more values name the public origins. Both default to `localhost`, which serv

Compose gives the `web` service `PUBLIC_SERVER_URL` from `BETTER_AUTH_URL`, so the browser calls the same API origin. A `PUBLIC_SERVER_URL` line of your own does nothing.

`FREENARY_VERSION` names the tag of both images. The compose default is `latest`, and the registry holds no tag of that name, so write a real tag.
`FREENARY_VERSION` names the tag of both images. The compose default is `latest`, the newest release, and it moves at each one. Name a version such as `X.Y` to hold an instance on one release line.

[Configuration](/docs/self-hosting/configuration) holds every variable, its default and its refusal message.

Expand Down
53 changes: 16 additions & 37 deletions apps/fumadocs/scripts/check-docs.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@ import { join, relative } from "node:path";

import { icons } from "lucide-react";

import { gitConfig, repoBlobUrl } from "../src/shared/config/site";
import { isVersionId, NEXT_VERSION } from "../src/shared/lib/versions";
import { getMDXComponents } from "../src/shared/ui/mdx";
import {
Expand All @@ -28,6 +27,7 @@ import {
STE_PARAGRAPH_SENTENCE_LIMIT,
STE_WORD_SUBSTITUTIONS,
} from "./docs-rules";
import { indexOfMatchedText, MOVING_REFS } from "./moving-refs";

type Severity = "error" | "warning";

Expand Down Expand Up @@ -296,43 +296,23 @@ const checkLinks = (
}
};

const checkRepoLinksArePinned = (page: DocPage, report: Report) => {
const movingBranchLinkTarget = `](${repoBlobUrl(gitConfig.branch)}`;
let index = page.raw.indexOf(movingBranchLinkTarget);
const checkMovingRefsArePinned = (page: DocPage, report: Report) => {
const folder = versionOf(page.relativePath);

while (index !== -1) {
report(
"error",
page.relativePath,
lineAt(page.raw, index),
"link",
`a repository link names \`${gitConfig.branch}\`. A released page pins it: \`/blob/v${versionOf(page.relativePath)}.0/\`, or whichever patch tag holds the code the page describes.`
);

index = page.raw.indexOf(
movingBranchLinkTarget,
index + movingBranchLinkTarget.length
);
}
};
for (const ref of MOVING_REFS) {
let index = page.raw.indexOf(ref.moving);

const checkVersionExamplesArePinned = (page: DocPage, report: Report) => {
const movingVersionExampleLine = "\nFREENARY_VERSION=main\n";
let index = page.raw.indexOf(movingVersionExampleLine);

while (index !== -1) {
report(
"error",
page.relativePath,
lineAt(page.raw, index + 1),
"version",
`\`FREENARY_VERSION=main\` names the moving branch. A released page pins it: \`FREENARY_VERSION=${versionOf(page.relativePath)}\`.`
);
while (index !== -1) {
report(
"error",
page.relativePath,
lineAt(page.raw, indexOfMatchedText(ref, index)),
ref.rule,
ref.message(folder)
);

index = page.raw.indexOf(
movingVersionExampleLine,
index + movingVersionExampleLine.length
);
index = page.raw.indexOf(ref.moving, index + ref.moving.length);
}
}
};

Expand Down Expand Up @@ -710,8 +690,7 @@ for (const page of pages) {
if (versionOf(page.relativePath) === NEXT_VERSION) {
checkAuthoredLanguage(page, report);
} else {
checkRepoLinksArePinned(page, report);
checkVersionExamplesArePinned(page, report);
checkMovingRefsArePinned(page, report);
}
}

Expand Down
50 changes: 50 additions & 0 deletions apps/fumadocs/scripts/moving-refs.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
import { gitConfig, repoBlobUrl, repoRawUrl } from "../src/shared/config/site";

export type ReleasePin = {
readonly folder: string;
readonly tag: string;
};

export type MovingRef = {
readonly message: (folder: string) => string;
readonly moving: string;
readonly pinned: (pin: ReleasePin) => string;
readonly rule: "link" | "version";
};

export const MOVING_REFS: readonly MovingRef[] = [
{
message: (folder) =>
`a repository link names \`${gitConfig.branch}\`. A released page pins it: \`/blob/v${folder}.0/\`, or whichever patch tag holds the code the page describes.`,
moving: `](${repoBlobUrl(gitConfig.branch)}`,
pinned: ({ tag }) => `](${repoBlobUrl(tag)}`,
rule: "link",
},
{
message: (folder) =>
`a download URL names \`${gitConfig.branch}\`, so the reader gets a file that moved past this release. A released page pins it: \`/v${folder}.0/\`, or whichever patch tag holds the files the page describes.`,
moving: repoRawUrl(gitConfig.branch),
pinned: ({ tag }) => repoRawUrl(tag),
rule: "link",
},
{
message: (folder) =>
`\`FREENARY_VERSION=${gitConfig.branch}\` names the moving branch. A released page pins it: \`FREENARY_VERSION=${folder}\`.`,
moving: `\nFREENARY_VERSION=${gitConfig.branch}\n`,
pinned: ({ folder }) => `\nFREENARY_VERSION=${folder}\n`,
rule: "version",
},
];

export const pinMovingRefs = (raw: string, pin: ReleasePin) => {
let pinned = raw;

for (const ref of MOVING_REFS) {
pinned = pinned.replaceAll(ref.moving, ref.pinned(pin));
}

return pinned;
};

export const indexOfMatchedText = (ref: MovingRef, index: number) =>
ref.moving.startsWith("\n") ? index + 1 : index;
39 changes: 10 additions & 29 deletions apps/fumadocs/scripts/snapshot-version.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,12 @@
import { cp, readdir, readFile, rename, rm, writeFile } from "node:fs/promises";
import { join } from "node:path";

import { gitConfig, repoBlobUrl } from "../src/shared/config/site";
import {
compareVersionIds,
NEXT_VERSION,
releaseFolder,
} from "../src/shared/lib/versions";
import { pinMovingRefs, type ReleasePin } from "./moving-refs";

type NavMeta = {
pages?: string[];
Expand All @@ -18,7 +18,6 @@ const CONTENT_DIR = "content/docs";
const STAGING_DIR_OUTSIDE_DOCS = "content/.snapshot";
const VERSION_LIST_FILE = "meta.json";
const PAGE_EXTENSION = ".mdx";
const MOVING_VERSION_EXAMPLE_LINE = "\nFREENARY_VERSION=main\n";

const exists = async (path: string) => {
const parent = path.slice(0, path.lastIndexOf("/"));
Expand Down Expand Up @@ -54,33 +53,13 @@ const mdxFilesUnder = async (dir: string): Promise<string[]> => {
return found;
};

const pinRepoLinksToTag = async (dir: string, tag: string) => {
const movingBranchLinkTarget = `](${repoBlobUrl(gitConfig.branch)}`;
const releaseTagLinkTarget = `](${repoBlobUrl(tag)}`;

for (const page of await mdxFilesUnder(dir)) {
const raw = await readFile(page, "utf8");

if (raw.includes(movingBranchLinkTarget)) {
await writeFile(
page,
raw.replaceAll(movingBranchLinkTarget, releaseTagLinkTarget)
);
}
}
};

const pinVersionExamplesToRelease = async (dir: string, release: string) => {
const releaseVersionExampleLine = `\nFREENARY_VERSION=${release}\n`;

const pinMovingRefsUnder = async (dir: string, pin: ReleasePin) => {
for (const page of await mdxFilesUnder(dir)) {
const raw = await readFile(page, "utf8");
const pinned = pinMovingRefs(raw, pin);

if (raw.includes(MOVING_VERSION_EXAMPLE_LINE)) {
await writeFile(
page,
raw.replaceAll(MOVING_VERSION_EXAMPLE_LINE, releaseVersionExampleLine)
);
if (pinned !== raw) {
await writeFile(page, pinned);
}
}
};
Expand Down Expand Up @@ -109,8 +88,10 @@ if (await exists(target)) {

await rm(STAGING_DIR_OUTSIDE_DOCS, { force: true, recursive: true });
await cp(authored, STAGING_DIR_OUTSIDE_DOCS, { recursive: true });
await pinRepoLinksToTag(STAGING_DIR_OUTSIDE_DOCS, `v${version}`);
await pinVersionExamplesToRelease(STAGING_DIR_OUTSIDE_DOCS, folder);
await pinMovingRefsUnder(STAGING_DIR_OUTSIDE_DOCS, {
folder,
tag: `v${version}`,
});

const stagedVersionList = join(STAGING_DIR_OUTSIDE_DOCS, VERSION_LIST_FILE);
const stagedMeta = await readNavMeta(stagedVersionList);
Expand All @@ -119,7 +100,7 @@ if (await exists(target)) {

await rename(STAGING_DIR_OUTSIDE_DOCS, target);
console.log(
`Wrote ${target}, with repository links pinned to v${version} and FREENARY_VERSION pinned to ${folder}.`
`Wrote ${target}, with every moving reference pinned to v${version} and FREENARY_VERSION pinned to ${folder}.`
);
}

Expand Down
3 changes: 3 additions & 0 deletions apps/fumadocs/src/shared/config/site.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,3 +10,6 @@ export const gitConfig = {

export const repoBlobUrl = (ref: string) =>
`https://github.com/${gitConfig.user}/${gitConfig.repo}/blob/${ref}/`;

export const repoRawUrl = (ref: string) =>
`https://raw.githubusercontent.com/${gitConfig.user}/${gitConfig.repo}/${ref}/`;
Loading
Loading