Skip to content
Closed

test pr #2300

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
6 changes: 6 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -85,6 +85,12 @@ 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`.

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
Expand Down
2 changes: 2 additions & 0 deletions content/contracts/overview.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

<Callout
Expand Down
141 changes: 141 additions & 0 deletions functions/api/search.js
Original file line number Diff line number Diff line change
@@ -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 });
}
}
11 changes: 9 additions & 2 deletions next.config.static.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', {
Expand All @@ -30,6 +31,10 @@ function resolveBaseUrl() {
return ghPagesUrl;
}

if (isCloudflarePagesBuild) {
return process.env.CF_PAGES_URL ?? 'https://docs.ton.org';
}

return 'http://localhost:3000';
}

Expand All @@ -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',
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
3 changes: 3 additions & 0 deletions scripts/common.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -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`;
Expand Down
Loading
Loading