From 5b0f5fd33a5b34998cd64725b4dd5e6b147c5e4d Mon Sep 17 00:00:00 2001
From: Petr Makhnev <51853996+i582@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:09:27 +0400
Subject: [PATCH 1/6] changes
---
README.md | 4 ++++
next.config.static.ts | 11 +++++++++--
package.json | 1 +
scripts/common.mjs | 3 +++
scripts/post-build.mjs | 34 +++++++++++++++++++++++++++++++++-
scripts/pre-build.mjs | 14 +++++++++++---
6 files changed, 61 insertions(+), 6 deletions(-)
diff --git a/README.md b/README.md
index 4ddb5cf6b..145f10bd1 100644
--- a/README.md
+++ b/README.md
@@ -85,6 +85,10 @@ See the [`snippets/` directory](./snippets) and the corresponding docs in [`cont
Changes are deployed to [production](https://docs.ton.org) automatically after pushing to the default branch (`main`).
+### Cloudflare Pages
+
+For a static Cloudflare Pages deployment, use `npm run build:cloudflare` as the build command and `out` as the output directory. The build writes `out/_redirects` from the redirect rules in `vercel.json`; set `NEXT_PUBLIC_SITE_URL` in the Pages environment when the public URL differs from `https://docs.ton.org`.
+
## Need help?
### Troubleshooting
diff --git a/next.config.static.ts b/next.config.static.ts
index e1088544c..e2672ad3f 100644
--- a/next.config.static.ts
+++ b/next.config.static.ts
@@ -8,8 +8,9 @@ const withMDX = createMDX();
const isGitHubPagesBuild =
process.env.GITHUB_ACTIONS === 'true' || process.env.GITHUB_PAGES === 'true';
const isVercelBuild = process.env.VERCEL === '1';
+const isCloudflarePagesBuild = process.env.CF_PAGES === '1';
const isVercelProd = isVercelBuild && resolveBaseUrl().startsWith('https://docs.ton.org');
-const isLocalBuild = !isGitHubPagesBuild && !isVercelBuild;
+const isLocalBuild = !isGitHubPagesBuild && !isVercelBuild && !isCloudflarePagesBuild;
let gitRepoMatch: RegExpMatchArray | null = null;
try {
const gitUrl = execSync('git config --get remote.origin.url', {
@@ -30,6 +31,10 @@ function resolveBaseUrl() {
return ghPagesUrl;
}
+ if (isCloudflarePagesBuild) {
+ return process.env.CF_PAGES_URL ?? 'https://docs.ton.org';
+ }
+
return 'http://localhost:3000';
}
@@ -54,7 +59,9 @@ const config: NextConfig = {
: 'vercel-dev'
: isGitHubPagesBuild
? 'github'
- : 'unknown',
+ : isCloudflarePagesBuild
+ ? 'cloudflare'
+ : 'unknown',
NEXT_PUBLIC_BASE_URL: resolveBaseUrl(),
NEXT_PUBLIC_BASE_PATH: resolveBasePath() ?? '',
NEXT_GIT_USER: gitRepoMatch?.at(1) ?? 'ton-blockchain',
diff --git a/package.json b/package.json
index f0e83cc07..2d174abe5 100644
--- a/package.json
+++ b/package.json
@@ -6,6 +6,7 @@
"start": "next dev",
"start:vercel": "NEXT_CONFIG=vercel next start",
"build": "node scripts/pre-build.mjs && cross-env NODE_OPTIONS=--max_old_space_size=4096 next build && node scripts/post-build.mjs",
+ "build:cloudflare": "cross-env CF_PAGES=1 NEXT_CONFIG=static npm run build",
"build:vercel": "node scripts/pre-build.mjs && NEXT_CONFIG=vercel next build",
"build:serve": "serve out",
"check:types": "fumadocs-mdx && next typegen && tsc --noEmit",
diff --git a/scripts/common.mjs b/scripts/common.mjs
index a8ef2fca6..4df197f68 100644
--- a/scripts/common.mjs
+++ b/scripts/common.mjs
@@ -36,6 +36,9 @@ export const prefix = '/docs';
export const isGitHubPagesBuild =
process.env.GITHUB_ACTIONS === 'true' || process.env.GITHUB_PAGES === 'true';
+// WARN: Must match next.config.static.ts isCloudflarePagesBuild
+export const isCloudflarePagesBuild = process.env.CF_PAGES === '1';
+
/** @param src {string} */
export function ansiRed(src) {
return `\x1b[31m${src}\x1b[0m`;
diff --git a/scripts/post-build.mjs b/scripts/post-build.mjs
index 2987ea1b6..249c40619 100644
--- a/scripts/post-build.mjs
+++ b/scripts/post-build.mjs
@@ -14,7 +14,14 @@
import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
import { join, extname, dirname } from 'node:path';
// Common
-import { prefix, outDir, isGitHubPagesBuild, getConfig, getRedirects } from './common.mjs';
+import {
+ prefix,
+ outDir,
+ isGitHubPagesBuild,
+ isCloudflarePagesBuild,
+ getConfig,
+ getRedirects,
+} from './common.mjs';
/**
* @param {string} path - file path
@@ -135,6 +142,20 @@ const generateStaticRedirects = (dir) => {
return stats;
};
+/** @param {string} dir */
+const generateCloudflareRedirects = (dir) => {
+ const redirects = getRedirects(getConfig()).map((redirect) => {
+ const { source, destination, permanent } = redirect;
+ if (/\s/.test(source) || /\s/.test(destination)) {
+ throw new Error(`Cloudflare redirect contains whitespace: ${source} → ${destination}`);
+ }
+ return `${source} ${destination} ${permanent === false ? 307 : 301}`;
+ });
+
+ writeFileWithDirs(join(dir, '_redirects'), `${redirects.join('\n')}\n`);
+ return { redirects: redirects.length };
+};
+
/** @param {string} dir */
const generateSiblingMarkdownFiles = (dir) => {
const llms = join(dir, 'llms');
@@ -169,6 +190,17 @@ const main = (dir) => {
const { files: mdFiles } = generateSiblingMarkdownFiles(dir);
console.log(pfx, `${mdFiles} markdown files`);
+ if (isCloudflarePagesBuild) {
+ if (!existsSync(dir) || !statSync(dir).isDirectory()) {
+ console.log(pfx, `skipped — ${dir}/ directory not found`);
+ process.exit(1);
+ }
+
+ console.log(pfx, 'generating Cloudflare Pages _redirects...');
+ const { redirects } = generateCloudflareRedirects(dir);
+ console.log(pfx, `${redirects} redirects`);
+ }
+
if (!isGitHubPagesBuild) {
console.log(pfx, 'skipped GitHub Pages-only steps');
process.exit(0);
diff --git a/scripts/pre-build.mjs b/scripts/pre-build.mjs
index d9f956ec8..1552381c6 100644
--- a/scripts/pre-build.mjs
+++ b/scripts/pre-build.mjs
@@ -11,18 +11,26 @@
╚─────────────────────────────────────────────────────────────────────────────*/
// Common utils
-import { $, ansiGreen } from './common.mjs';
+import { $, ansiGreen, isCloudflarePagesBuild } from './common.mjs';
const main = () => {
const pfx = 'pre-build:';
const isGitHubPagesBuild =
process.env.GITHUB_ACTIONS === 'true' || process.env.GITHUB_PAGES === 'true';
const isVercelBuild = process.env.VERCEL === '1';
- const isLocalBuild = !isGitHubPagesBuild && !isVercelBuild;
+ const isLocalBuild = !isGitHubPagesBuild && !isVercelBuild && !isCloudflarePagesBuild;
console.log(
pfx,
'build type is —',
- isLocalBuild ? 'local' : isVercelBuild ? 'vercel' : isGitHubPagesBuild ? 'github' : 'unknown',
+ isLocalBuild
+ ? 'local'
+ : isVercelBuild
+ ? 'vercel'
+ : isGitHubPagesBuild
+ ? 'github'
+ : isCloudflarePagesBuild
+ ? 'cloudflare'
+ : 'unknown',
);
const scripts = [
'check:types',
From 2a8a49852bffe9aeaeb26249e2dfa751564e4338 Mon Sep 17 00:00:00 2001
From: Petr Makhnev <51853996+i582@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:15:53 +0400
Subject: [PATCH 2/6] disable search
---
src/app/api/search/route.ts | 25 -------------------------
src/app/layout.tsx | 3 +--
src/components/provider.tsx | 19 +++----------------
3 files changed, 4 insertions(+), 43 deletions(-)
delete mode 100644 src/app/api/search/route.ts
diff --git a/src/app/api/search/route.ts b/src/app/api/search/route.ts
deleted file mode 100644
index 4b1e64641..000000000
--- a/src/app/api/search/route.ts
+++ /dev/null
@@ -1,25 +0,0 @@
-import { source, getSearchablePages } from '@/lib/source';
-import { flexsearchFromSource } from 'fumadocs-core/search/flexsearch';
-
-const searchSource: typeof source = {
- ...source,
- getPages: getSearchablePages,
-};
-
-// https://www.fumadocs.dev/docs/headless/search/flexsearch#static-export
-const searchAPI = flexsearchFromSource(searchSource, {
- // async buildIndex(page) {
- // return {
- // title: page.data.title,
- // description: page.data.description,
- // url: page.url,
- // id: page.url,
- // structuredData: await page.data.structuredData(),
- // breadcrumbs: page.slugs.slice(0, -1),
- // // tag: undefined,
- // };
- // }
-});
-
-export const revalidate = false;
-export const GET = process.env.NEXT_CONFIG === 'vercel' ? searchAPI.GET : searchAPI.staticGET;
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index b552303b6..f2d57adae 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,6 +1,5 @@
import { Inter } from 'next/font/google';
import { Provider } from '@/components/provider';
-import { getQuickJumpPages } from '@/lib/source';
import './global.css';
import 'katex/dist/katex.css';
@@ -36,7 +35,7 @@ export default function Layout({ children }: LayoutProps<'/'>) {
return (
- {children}
+ {children}
);
diff --git a/src/components/provider.tsx b/src/components/provider.tsx
index d2c133288..a70825c17 100644
--- a/src/components/provider.tsx
+++ b/src/components/provider.tsx
@@ -1,20 +1,7 @@
'use client';
-import SearchDialog, { type QuickJumpPage } from '@/components/search';
import { RootProvider } from 'fumadocs-ui/provider/next';
-import type { SharedProps } from 'fumadocs-ui/components/dialog/search';
-import { type ReactNode, useCallback } from 'react';
+import type { ReactNode } from 'react';
-export function Provider({
- children,
- quickJumpPages,
-}: {
- children: ReactNode;
- quickJumpPages: QuickJumpPage[];
-}) {
- const ConfiguredSearchDialog = useCallback(
- (props: SharedProps) => ,
- [quickJumpPages],
- );
-
- return {children};
+export function Provider({ children }: { children: ReactNode }) {
+ return {children};
}
From 427a49d95277953f933a8ecc87e9b5bf51b6bd83 Mon Sep 17 00:00:00 2001
From: Petr Makhnev <51853996+i582@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:35:18 +0400
Subject: [PATCH 3/6] search with workers
---
README.md | 2 +
functions/api/search.js | 141 ++++++++++++++++++++++++++++++++++
scripts/post-build.mjs | 105 ++++++++++++++++++++++++-
src/app/layout.tsx | 3 +-
src/app/search-index/route.ts | 18 +++++
src/components/provider.tsx | 24 +++++-
src/components/search.tsx | 13 +---
7 files changed, 294 insertions(+), 12 deletions(-)
create mode 100644 functions/api/search.js
create mode 100644 src/app/search-index/route.ts
diff --git a/README.md b/README.md
index 145f10bd1..c3b0c6884 100644
--- a/README.md
+++ b/README.md
@@ -89,6 +89,8 @@ Changes are deployed to [production](https://docs.ton.org) automatically after p
For a static Cloudflare Pages deployment, use `npm run build:cloudflare` as the build command and `out` as the output directory. The build writes `out/_redirects` from the redirect rules in `vercel.json`; set `NEXT_PUBLIC_SITE_URL` in the Pages environment when the public URL differs from `https://docs.ton.org`.
+The Cloudflare build also enables search through the Pages Function in `functions/api/search.js`. The build creates a compact search catalog under `out/search-index`; no D1 or R2 binding is required. `out/_routes.json` limits Function invocations to `/api/search`, so the documentation pages remain static asset requests.
+
## Need help?
### Troubleshooting
diff --git a/functions/api/search.js b/functions/api/search.js
new file mode 100644
index 000000000..b6b134d75
--- /dev/null
+++ b/functions/api/search.js
@@ -0,0 +1,141 @@
+let documentsPromise;
+const shardPromises = new Map();
+
+const normalize = (value) => value.normalize('NFKC').toLocaleLowerCase();
+
+const tokenize = (value) => normalize(value).match(/[\p{L}\p{N}]+/gu) ?? [];
+
+const getShardName = (term) => {
+ const first = term[0] ?? '';
+ if (/^[a-z]$/.test(first)) return first;
+ if (/^[0-9]$/.test(first)) return 'digits';
+ return 'other';
+};
+
+const loadJsonAsset = async (context, path) => {
+ const assetUrl = new URL(path, context.request.url);
+ const response = await context.env.ASSETS.fetch(assetUrl);
+ if (!response.ok) {
+ throw new Error(`Search asset request failed with ${response.status}: ${path}`);
+ }
+
+ return response.json();
+};
+
+const loadDocuments = (context) => {
+ if (!documentsPromise) {
+ documentsPromise = loadJsonAsset(context, '/search-index/documents').catch((error) => {
+ documentsPromise = undefined;
+ throw error;
+ });
+ }
+
+ return documentsPromise;
+};
+
+const loadShard = (context, shardName) => {
+ if (!shardPromises.has(shardName)) {
+ const promise = loadJsonAsset(context, `/search-index/${shardName}`).catch((error) => {
+ shardPromises.delete(shardName);
+ throw error;
+ });
+ shardPromises.set(shardName, promise);
+ }
+
+ return shardPromises.get(shardName);
+};
+
+const getSnippet = (document) => {
+ const snippet = document.description ?? document.excerpt;
+ if (!snippet) return undefined;
+ return snippet.replace(/\s+/g, ' ').trim().slice(0, 240);
+};
+
+const searchDocuments = async (context, query) => {
+ const terms = [...new Set(tokenize(query))].filter((term) => term.length > 1).slice(0, 8);
+ if (terms.length === 0) return [];
+
+ const shardNames = [...new Set(terms.map(getShardName))];
+ const [catalog, ...shards] = await Promise.all([
+ loadDocuments(context),
+ ...shardNames.map((shardName) => loadShard(context, shardName)),
+ ]);
+ const shardByName = new Map(shardNames.map((shardName, index) => [shardName, shards[index]]));
+ const candidates = new Map();
+
+ for (const term of terms) {
+ const shard = shardByName.get(getShardName(term));
+ if (!shard?.terms) continue;
+
+ for (const [indexedTerm, documentIds] of Object.entries(shard.terms)) {
+ if (indexedTerm !== term && !indexedTerm.startsWith(term)) continue;
+
+ for (const documentId of documentIds) {
+ const document = catalog.documents[documentId];
+ if (!document) continue;
+
+ let candidate = candidates.get(documentId);
+ if (!candidate) {
+ candidate = { document, matchedTerms: new Set(), score: 0 };
+ candidates.set(documentId, candidate);
+ }
+
+ if (candidate.matchedTerms.has(term)) continue;
+ candidate.matchedTerms.add(term);
+
+ const title = normalize(document.title);
+ const description = normalize(document.description ?? '');
+ if (title.includes(term)) candidate.score += 120;
+ else if (description.includes(term)) candidate.score += 45;
+ else candidate.score += indexedTerm === term ? 10 : 5;
+ }
+ }
+ }
+
+ const normalizedQuery = normalize(query).trim();
+ const scored = [...candidates.values()];
+ for (const candidate of scored) {
+ if (candidate.matchedTerms.size === terms.length) candidate.score += 50;
+ if (normalize(candidate.document.title).includes(normalizedQuery)) candidate.score += 100;
+ }
+
+ scored.sort((a, b) => b.score - a.score || a.document.title.localeCompare(b.document.title));
+
+ return scored.slice(0, 30).flatMap(({ document }) => {
+ const snippet = getSnippet(document);
+ return [
+ {
+ id: `${document.id}:page`,
+ type: 'page',
+ content: document.title,
+ url: document.url,
+ },
+ ...(snippet
+ ? [
+ {
+ id: `${document.id}:text`,
+ type: 'text',
+ content: snippet,
+ url: document.url,
+ },
+ ]
+ : []),
+ ];
+ });
+};
+
+export async function onRequestGet(context) {
+ const query = new URL(context.request.url).searchParams.get('query') ?? '';
+ if (!query.trim()) return Response.json([]);
+
+ try {
+ return Response.json(await searchDocuments(context, query), {
+ headers: {
+ 'cache-control': 'public, max-age=60, s-maxage=3600',
+ },
+ });
+ } catch (error) {
+ console.error('Search request failed', error);
+ return Response.json({ error: 'Search is temporarily unavailable' }, { status: 500 });
+ }
+}
diff --git a/scripts/post-build.mjs b/scripts/post-build.mjs
index 249c40619..4ccafc299 100644
--- a/scripts/post-build.mjs
+++ b/scripts/post-build.mjs
@@ -11,7 +11,15 @@
╚─────────────────────────────────────────────────────────────────────────────*/
// Node.js
-import { existsSync, readdirSync, readFileSync, statSync, writeFileSync, mkdirSync } from 'node:fs';
+import {
+ existsSync,
+ mkdirSync,
+ readFileSync,
+ readdirSync,
+ statSync,
+ unlinkSync,
+ writeFileSync,
+} from 'node:fs';
import { join, extname, dirname } from 'node:path';
// Common
import {
@@ -156,6 +164,94 @@ const generateCloudflareRedirects = (dir) => {
return { redirects: redirects.length };
};
+/** @param {string} dir */
+const generateCloudflareRoutes = (dir) => {
+ const routes = {
+ version: 1,
+ include: ['/api/search', '/api/search/*'],
+ exclude: [],
+ };
+
+ writeFileWithDirs(join(dir, '_routes.json'), `${JSON.stringify(routes, null, 2)}\n`);
+};
+
+const cloudflareSearchShards = [...'abcdefghijklmnopqrstuvwxyz', 'digits', 'other'];
+
+/** @param {string} term */
+const getCloudflareSearchShard = (term) => {
+ const first = term[0] ?? '';
+ if (/^[a-z]$/.test(first)) return first;
+ if (/^[0-9]$/.test(first)) return 'digits';
+ return 'other';
+};
+
+/** @param {string} value */
+const getSearchTokens = (value) =>
+ [
+ ...new Set(
+ value
+ .normalize('NFKC')
+ .toLocaleLowerCase()
+ .match(/[\p{L}\p{N}]+/gu) ?? [],
+ ),
+ ].filter((token) => token.length > 1);
+
+/** @param {string} dir */
+const generateCloudflareSearchIndex = (dir) => {
+ const sourcePath = join(dir, 'search-index');
+ if (!existsSync(sourcePath)) {
+ throw new Error(`Cloudflare search index source is missing: ${sourcePath}`);
+ }
+
+ const source = JSON.parse(readFileSync(sourcePath, 'utf8'));
+ /** @type {Record} */
+ const documents = Object.create(null);
+ /** @type {Map }>} */
+ const shards = new Map(
+ cloudflareSearchShards.map((shard) => [shard, { version: 1, terms: Object.create(null) }]),
+ );
+
+ for (const document of source.documents ?? []) {
+ const text = String(document.text ?? '')
+ .replace(/\s+/g, ' ')
+ .trim();
+ documents[document.id] = {
+ id: document.id,
+ url: document.url,
+ title: document.title,
+ ...(document.description ? { description: document.description } : {}),
+ ...(text ? { excerpt: text.slice(0, 240) } : {}),
+ };
+
+ const searchableText = [document.title, document.description, document.text]
+ .filter(Boolean)
+ .join(' ');
+ for (const token of getSearchTokens(searchableText)) {
+ const shard = shards.get(getCloudflareSearchShard(token));
+ if (!shard) continue;
+ const posting = shard.terms[token] ?? [];
+ posting.push(document.id);
+ shard.terms[token] = posting;
+ }
+ }
+
+ // Next's static route creates this temporary, full-text JSON file. Replace it
+ // with the small catalog plus token shards consumed by the Pages Function.
+ unlinkSync(sourcePath);
+ writeFileWithDirs(
+ join(dir, 'search-index', 'documents'),
+ `${JSON.stringify({ version: 1, documents })}\n`,
+ );
+ for (const [shardName, shard] of shards) {
+ writeFileWithDirs(join(dir, 'search-index', shardName), `${JSON.stringify(shard)}\n`);
+ }
+
+ return {
+ documents: Object.keys(documents).length,
+ shards: shards.size,
+ };
+};
+
/** @param {string} dir */
const generateSiblingMarkdownFiles = (dir) => {
const llms = join(dir, 'llms');
@@ -199,6 +295,13 @@ const main = (dir) => {
console.log(pfx, 'generating Cloudflare Pages _redirects...');
const { redirects } = generateCloudflareRedirects(dir);
console.log(pfx, `${redirects} redirects`);
+
+ console.log(pfx, 'limiting Cloudflare Pages Functions to /api/search...');
+ generateCloudflareRoutes(dir);
+
+ console.log(pfx, 'compacting Cloudflare search index into token shards...');
+ const searchIndex = generateCloudflareSearchIndex(dir);
+ console.log(pfx, `${searchIndex.documents} documents, ${searchIndex.shards} shards`);
}
if (!isGitHubPagesBuild) {
diff --git a/src/app/layout.tsx b/src/app/layout.tsx
index f2d57adae..b552303b6 100644
--- a/src/app/layout.tsx
+++ b/src/app/layout.tsx
@@ -1,5 +1,6 @@
import { Inter } from 'next/font/google';
import { Provider } from '@/components/provider';
+import { getQuickJumpPages } from '@/lib/source';
import './global.css';
import 'katex/dist/katex.css';
@@ -35,7 +36,7 @@ export default function Layout({ children }: LayoutProps<'/'>) {
return (
- {children}
+ {children}
);
diff --git a/src/app/search-index/route.ts b/src/app/search-index/route.ts
new file mode 100644
index 000000000..3bbfa5b5f
--- /dev/null
+++ b/src/app/search-index/route.ts
@@ -0,0 +1,18 @@
+import { getSearchablePages } from '@/lib/source';
+
+export const dynamic = 'force-static';
+export const revalidate = false;
+
+export async function GET() {
+ const documents = await Promise.all(
+ getSearchablePages().map(async (page) => ({
+ id: page.url,
+ url: page.url,
+ title: page.data.title.replace(/`/g, ''),
+ description: page.data.description,
+ text: await page.data.getText('processed'),
+ })),
+ );
+
+ return Response.json({ version: 1, documents });
+}
diff --git a/src/components/provider.tsx b/src/components/provider.tsx
index a70825c17..62550cfdb 100644
--- a/src/components/provider.tsx
+++ b/src/components/provider.tsx
@@ -1,7 +1,29 @@
'use client';
import { RootProvider } from 'fumadocs-ui/provider/next';
+import type { SharedProps } from 'fumadocs-ui/components/dialog/search';
import type { ReactNode } from 'react';
+import DefaultSearchDialog, { type QuickJumpPage } from '@/components/search';
+
+export function Provider({
+ children,
+ quickJumpPages,
+}: {
+ children: ReactNode;
+ quickJumpPages: QuickJumpPage[];
+}) {
+ if (process.env.NEXT_BUILD_TYPE === 'cloudflare') {
+ return (
+ (
+
+ ),
+ }}
+ >
+ {children}
+
+ );
+ }
-export function Provider({ children }: { children: ReactNode }) {
return {children};
}
diff --git a/src/components/search.tsx b/src/components/search.tsx
index ec5d916ba..ec1d74546 100644
--- a/src/components/search.tsx
+++ b/src/components/search.tsx
@@ -20,7 +20,6 @@ import {
} from 'fumadocs-ui/components/dialog/search';
import { useDocsSearch } from 'fumadocs-core/search/client';
import { fetchClient } from 'fumadocs-core/search/client/fetch';
-import { flexsearchStaticClient } from 'fumadocs-core/search/client/flexsearch-static';
import { useI18n } from 'fumadocs-ui/contexts/i18n';
export interface QuickJumpPage {
@@ -35,14 +34,10 @@ export default function DefaultSearchDialog({
}: SharedProps & { quickJumpPages: QuickJumpPage[] }) {
// const [tag] = useState();
const { locale } = useI18n(); // (optional) for i18n
- const client =
- process.env.NEXT_CONFIG === 'vercel'
- ? fetchClient({ locale })
- : flexsearchStaticClient({
- locale,
- from: `${process.env.NEXT_PUBLIC_BASE_PATH ?? ''}/api/search`,
- // tag,
- });
+ const client = fetchClient({
+ api: `${process.env.NEXT_PUBLIC_BASE_PATH ?? ''}/api/search`,
+ locale,
+ });
const { search, setSearch, query } = useDocsSearch({ client });
const router = useRouter();
const quickJumpAction = useMemo(() => {
From 1be02a8a36ea3ca6ea5f8a2a777f19da35251e25 Mon Sep 17 00:00:00 2001
From: Petr Makhnev <51853996+i582@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:41:26 +0400
Subject: [PATCH 4/6] add .md links rewrite
---
scripts/post-build.mjs | 26 ++++++++++++++++++--------
1 file changed, 18 insertions(+), 8 deletions(-)
diff --git a/scripts/post-build.mjs b/scripts/post-build.mjs
index 4ccafc299..2ab07ff1b 100644
--- a/scripts/post-build.mjs
+++ b/scripts/post-build.mjs
@@ -151,7 +151,7 @@ const generateStaticRedirects = (dir) => {
};
/** @param {string} dir */
-const generateCloudflareRedirects = (dir) => {
+const generateCloudflareRedirects = (dir, markdownRoutes = []) => {
const redirects = getRedirects(getConfig()).map((redirect) => {
const { source, destination, permanent } = redirect;
if (/\s/.test(source) || /\s/.test(destination)) {
@@ -159,9 +159,17 @@ const generateCloudflareRedirects = (dir) => {
}
return `${source} ${destination} ${permanent === false ? 307 : 301}`;
});
+ const markdownRewrites = markdownRoutes
+ .filter((route) => !/^(?:llms|og|api|_next)(?:\/|$)/.test(route))
+ .map((route) => `/${route}.md /llms/${route}/content.md 200`);
+ const lines = [...redirects, ...markdownRewrites];
- writeFileWithDirs(join(dir, '_redirects'), `${redirects.join('\n')}\n`);
- return { redirects: redirects.length };
+ if (lines.length > 2000) {
+ throw new Error(`Cloudflare _redirects has ${lines.length} static rules; the limit is 2000`);
+ }
+
+ writeFileWithDirs(join(dir, '_redirects'), `${lines.join('\n')}\n`);
+ return { redirects: redirects.length, rewrites: markdownRewrites.length };
};
/** @param {string} dir */
@@ -255,8 +263,9 @@ const generateCloudflareSearchIndex = (dir) => {
/** @param {string} dir */
const generateSiblingMarkdownFiles = (dir) => {
const llms = join(dir, 'llms');
- if (!existsSync(llms)) return { files: 0 };
+ if (!existsSync(llms)) return { files: 0, routes: [] };
let files = 0;
+ const routes = [];
/** @param {string} cur */
const walk = (cur) => {
for (const entry of readdirSync(cur, { withFileTypes: true })) {
@@ -272,18 +281,19 @@ const generateSiblingMarkdownFiles = (dir) => {
if (!existsSync(html)) continue;
writeFileWithDirs(target, readFileSync(path, 'utf8'));
files += 1;
+ routes.push(route);
}
};
walk(llms);
- return { files };
+ return { files, routes };
};
/** @param {string} dir */
const main = (dir) => {
const pfx = 'post-build:';
console.log(pfx, 'generating sibling LLM markdown files...');
- const { files: mdFiles } = generateSiblingMarkdownFiles(dir);
+ const { files: mdFiles, routes: markdownRoutes } = generateSiblingMarkdownFiles(dir);
console.log(pfx, `${mdFiles} markdown files`);
if (isCloudflarePagesBuild) {
@@ -293,8 +303,8 @@ const main = (dir) => {
}
console.log(pfx, 'generating Cloudflare Pages _redirects...');
- const { redirects } = generateCloudflareRedirects(dir);
- console.log(pfx, `${redirects} redirects`);
+ const { redirects, rewrites } = generateCloudflareRedirects(dir, markdownRoutes);
+ console.log(pfx, `${redirects} redirects, ${rewrites} markdown rewrites`);
console.log(pfx, 'limiting Cloudflare Pages Functions to /api/search...');
generateCloudflareRoutes(dir);
From a7cb71d4c7ad2d216ae6857f5c845d56893118b1 Mon Sep 17 00:00:00 2001
From: Petr Makhnev <51853996+i582@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:56:04 +0400
Subject: [PATCH 5/6] headers and fixes in redirects
---
scripts/post-build.mjs | 34 +++++++++++++++++++++++++++++++++-
1 file changed, 33 insertions(+), 1 deletion(-)
diff --git a/scripts/post-build.mjs b/scripts/post-build.mjs
index 2ab07ff1b..2e5f4f01b 100644
--- a/scripts/post-build.mjs
+++ b/scripts/post-build.mjs
@@ -157,7 +157,7 @@ const generateCloudflareRedirects = (dir, markdownRoutes = []) => {
if (/\s/.test(source) || /\s/.test(destination)) {
throw new Error(`Cloudflare redirect contains whitespace: ${source} → ${destination}`);
}
- return `${source} ${destination} ${permanent === false ? 307 : 301}`;
+ return `${source} ${destination} ${permanent === false ? 307 : 308}`;
});
const markdownRewrites = markdownRoutes
.filter((route) => !/^(?:llms|og|api|_next)(?:\/|$)/.test(route))
@@ -172,6 +172,34 @@ const generateCloudflareRedirects = (dir, markdownRoutes = []) => {
return { redirects: redirects.length, rewrites: markdownRewrites.length };
};
+/** @param {string} dir */
+const generateCloudflareHeaders = (dir) => {
+ const headerRules = getConfig().headers ?? [];
+ const lines = [];
+ let headers = 0;
+
+ for (const rule of headerRules) {
+ const { source, headers: ruleHeaders } = rule;
+ if (!source || /[\r\n]/.test(source)) {
+ throw new Error(`Cloudflare header contains an invalid source: ${source}`);
+ }
+
+ lines.push(source);
+ for (const header of ruleHeaders ?? []) {
+ const { key, value } = header;
+ if (!key || value === undefined || /[\r\n]/.test(String(value))) {
+ throw new Error(`Cloudflare header contains an invalid definition for ${source}`);
+ }
+ lines.push(` ${key}: ${value}`);
+ headers += 1;
+ }
+ lines.push('');
+ }
+
+ writeFileWithDirs(join(dir, '_headers'), `${lines.join('\n').trimEnd()}\n`);
+ return { rules: headerRules.length, headers };
+};
+
/** @param {string} dir */
const generateCloudflareRoutes = (dir) => {
const routes = {
@@ -306,6 +334,10 @@ const main = (dir) => {
const { redirects, rewrites } = generateCloudflareRedirects(dir, markdownRoutes);
console.log(pfx, `${redirects} redirects, ${rewrites} markdown rewrites`);
+ console.log(pfx, 'generating Cloudflare Pages _headers...');
+ const headers = generateCloudflareHeaders(dir);
+ console.log(pfx, `${headers.rules} header rules, ${headers.headers} headers`);
+
console.log(pfx, 'limiting Cloudflare Pages Functions to /api/search...');
generateCloudflareRoutes(dir);
From bbb2a903cf87877c5e06c319d2353275d66a76c4 Mon Sep 17 00:00:00 2001
From: Petr Makhnev <51853996+i582@users.noreply.github.com>
Date: Wed, 19 Aug 2026 23:56:56 +0400
Subject: [PATCH 6/6] fix
---
content/contracts/overview.mdx | 2 ++
1 file changed, 2 insertions(+)
diff --git a/content/contracts/overview.mdx b/content/contracts/overview.mdx
index b8997e914..a5c037bd6 100644
--- a/content/contracts/overview.mdx
+++ b/content/contracts/overview.mdx
@@ -4,6 +4,8 @@ sidebarTitle: "Overview"
description: "How to build, test, deploy, debug, and otherwise interact with TON smart contracts"
---
+Just use Acton.
+
This section covers the recommended toolchain, editor support, standard contracts, reusable techniques, and the legacy TypeScript environment.