diff --git a/website/package.json b/website/package.json index 51dd9081dd0..a998731e5d6 100644 --- a/website/package.json +++ b/website/package.json @@ -12,11 +12,12 @@ "clean": "rm -rf node_modules && rm -rf package-lock.json && rm -rf build", "full-test": "./test-links.sh test", "start": "docusaurus start", + "serve": "docusaurus serve", "build": "./node_modules/.bin/docusaurus build && yarn run process-markdown", "build:dev": "./node_modules/.bin/docusaurus build --dev && yarn run process-markdown", "build:preview": "yarn run process-markdown && ./node_modules/.bin/docusaurus build --locale en && yarn run process-markdown", - "process-markdown": "node ./scripts/copy-md-to-static.js", - "meilisearch:build-index": "npm run process-markdown && node scripts/index-meilisearch.mjs", + "process-markdown": "node ./scripts/copy-md-to-static.mjs", + "meilisearch:build-index": "node scripts/index-meilisearch.mjs", "swizzle": "docusaurus swizzle", "docusaurus": "docusaurus" }, @@ -32,6 +33,7 @@ "serve": "^14.2.4" }, "dependencies": { + "@ai-sdk/react": "^3.0.99", "@docsearch/core": "^4.5.0", "@docusaurus/core": "3.9.2", "@docusaurus/plugin-ideal-image": "3.9.2", @@ -42,6 +44,7 @@ "@hot-labs/near-connect": "^0.8.2", "@mermaid-js/layout-elk": "^0.2.0", "@saucelabs/theme-github-codeblock": "^0.3.0", + "ai": "^6.0.97", "axios": "^1.12.0", "clsx": "^1.1.1", "crypto-browserify": "^3.12.1", @@ -70,6 +73,7 @@ "typed.js": "^2.1.0", "url": "^0.11.4", "util": "^0.12.5", - "vm-browserify": "^1.1.2" + "vm-browserify": "^1.1.2", + "zod": "^4.3.6" } } diff --git a/website/scripts/copy-md-to-static.js b/website/scripts/copy-md-to-static.js deleted file mode 100644 index 9e11c26509d..00000000000 --- a/website/scripts/copy-md-to-static.js +++ /dev/null @@ -1,317 +0,0 @@ -#!/usr/bin/env node - -const glob = require('glob'); -const path = require('path'); -const fs = require('fs'); -const axios = require('axios'); -const { URL } = require('url'); - -async function fetchGitHubCode(tag) { - // Replace single quotes with double quotes for consistent parsing - const normalizedTag = tag.replace(/'/g, '"'); - - // Extract URL from the tag - const urlMatch = normalizedTag.match(/url="(.*?)"/); - if (!urlMatch) return null; - - let url = urlMatch[1]; - // Remove hash fragment - url = url.split('#')[0]; - - // Parse URL and extract components - const urlObj = new URL(url); - const pathSegments = urlObj.pathname.slice(1).split('/'); - - if (pathSegments.length < 4) return null; - - const [org, repo, , branch, ...pathSeg] = pathSegments; - const filePath = pathSeg.join('/'); - - // Construct raw GitHub URL - const rawUrl = `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${filePath}`; - - try { - // Fetch the code - const response = await axios.get(rawUrl); - return { - code: response.data, - normalizedTag, - url: rawUrl - }; - } catch (error) { - console.error(`ā ${rawUrl}`); - return null; - } -} - -// Extract code lines based on start/end parameters and format as code block -function formatCodeBlock(codeData, language = '') { - const { code, normalizedTag } = codeData; - - // Extract line numbers - const startMatch = normalizedTag.match(/start="(\d*)"/); - const endMatch = normalizedTag.match(/end="(\d*)"/); - - const codeLines = String(code).split('\n'); - const start = startMatch ? Math.max(parseInt(startMatch[1]) - 1, 0) : 0; - const end = endMatch ? parseInt(endMatch[1]) + 1 : codeLines.length; - - const selectedCode = codeLines.slice(start, end).join('\n'); - - // Return formatted code block with optional language syntax highlighting - return `\`\`\`${language}\n${selectedCode}\n\`\`\``; -} - -async function replaceTagsWithCode(content, tagName, includeLanguage = false) { - const tagRegex = new RegExp(`<${tagName}\\s[^>]*?(?:\\/>|>[^<]*<\\/${tagName}>)`, 'g'); - const tags = content.match(tagRegex) || []; - - for (const tag of tags) { - const codeData = await fetchGitHubCode(tag); - - if (codeData) { - let language = ''; - - // Extract language if needed (for File tags) - if (includeLanguage) { - const languageMatch = codeData.normalizedTag.match(/language="(.*?)"/); - language = languageMatch ? languageMatch[1] : ''; - } - - const codeBlock = formatCodeBlock(codeData, language); - content = content.replace(tag, codeBlock); - } - } - - return content; -} - -async function replaceGithubWithCode(content) { - return replaceTagsWithCode(content, 'Github', false); -} - -async function replaceFileWithCode(content) { - return replaceTagsWithCode(content, 'File', true); -} - -// Directories -const DOCS_DIR = path.join(__dirname, '../../docs'); -const BUILD_DIR = path.join(__dirname, '../build'); - -console.log('š Starting markdown files post-processing...'); -console.log('This script will copy processed .md files alongside .html files in build/'); - -// Clear md -async function cleanContent(content) { - let cleaned = content; - - // Remove all imports (including JSX components and MDX imports) - cleaned = cleaned.replace(/^import\s+.*?from\s+['"].*?['"];?\s*$/gm, ''); - cleaned = cleaned.replace(/^import\s+\{[^}]*\}\s+from\s+['"].*?['"];?\s*$/gm, ''); - cleaned = cleaned.replace(/^import\s+.*?$/gm, ''); - - cleaned = await replaceGithubWithCode(cleaned); - cleaned = await replaceFileWithCode(cleaned); - - return cleaned; -} - -function generatePath(filePath) { - const content = fs.readFileSync(filePath, 'utf8'); - - const frontmatterMatch = content.match(/^---\s*\n([\s\S]*?)\n---/); - let id = null; - if (frontmatterMatch) { - const frontmatter = frontmatterMatch[1]; - const idMatch = frontmatter.match(/^id:\s*(.+)$/m); - if (idMatch) { - id = idMatch[1].trim().replace(/['"]/g, ''); - } - } - - const relativePath = path.relative(DOCS_DIR, filePath); - - const dirPath = path.dirname(relativePath); - - let newFilename; - if (id) { - newFilename = `${id}.md`; - } else { - newFilename = path.basename(filePath); - } - - return path.join(BUILD_DIR, dirPath, newFilename); -} - -function extractFrontmatter(content) { - const frontmatter = {}; - let body = content; - - if (content.startsWith('---\n')) { - const endIndex = content.indexOf('\n---\n', 4); - if (endIndex !== -1) { - const frontmatterText = content.substring(4, endIndex); - body = content.substring(endIndex + 5); - const lines = frontmatterText.split('\n'); - for (const line of lines) { - const colonIndex = line.indexOf(':'); - if (colonIndex > 0) { - const key = line.substring(0, colonIndex).trim(); - const value = line.substring(colonIndex + 1).trim().replace(/^["']|["']$/g, ''); - frontmatter[key] = value; - } - } - } - } - - return { frontmatter, body }; -} - -function getDescription(content) { - const lines = content.split('\n'); - for (const line of lines) { - const trimmed = line.trim(); - if (trimmed && - trimmed.length > 0) { - return trimmed - } - } - return '' -} - -async function checkLink(url) { - try { - const response = await axios.head(url, { timeout: 5000 }); - return { url, status: response.status, ok: true }; - } catch (error) { - const status = error.response?.status || 'NO RESPONSE'; - console.log(`ā ${url} - ${status}`); - return { url, status, ok: false }; - } -} - -const allMarkdownFiles = glob.sync(path.join(DOCS_DIR, '**/*.md')); - -async function processMarkdownFiles() { - const documentationPages = {}; - - await Promise.all( - allMarkdownFiles.map(async (markdownFilePath) => { - let fileContent = fs.readFileSync(markdownFilePath, 'utf8'); - - fileContent = await cleanContent(fileContent); - - const outputFilePath = generatePath(markdownFilePath); - const outputDirectory = path.dirname(outputFilePath); - - if (!fs.existsSync(outputDirectory)) fs.mkdirSync(outputDirectory, { recursive: true }); - - fs.writeFileSync(outputFilePath, fileContent, 'utf8'); - - const relativeFilename = path.relative(DOCS_DIR, markdownFilePath); - - if (relativeFilename === "index.md" || relativeFilename === "help.md") return; - - const pathSegments = relativeFilename.split('/'); - const sectionName = pathSegments[0]; - const fileName = pathSegments.pop(); - const alternativeId = fileName.replace('.md', ''); - - if (!documentationPages[sectionName]) { - documentationPages[sectionName] = []; - } - - const { frontmatter, body } = extractFrontmatter(fileContent); - const pageDescription = getDescription(body); - - if (pageDescription.startsWith('#') || - pageDescription.startsWith('import') || - pageDescription.startsWith(':::') || - pageDescription.startsWith('![')) { - console.warn(`Warning: No valid description found in ${relativeFilename}`); - } - if(!frontmatter.description){ - console.log(`ā No description tag found for ${relativeFilename}`); - } - const pageTitle = frontmatter.title || - frontmatter.sidebar_label || - alternativeId.replace(/[-_]/g, ' ').replace(/\b\w/g, letter => letter.toUpperCase()); - - const pageUrl = `${pathSegments.join("/")}/${frontmatter.id ? frontmatter.id + ".md" : fileName}`; - const pageId = frontmatter.id || alternativeId; - - documentationPages[sectionName].push({ - title: pageTitle, - url: pageUrl, - description: frontmatter.description || pageDescription, - id: pageId, - }); - }) - ); - - const documentationSections = { - "protocol": { name: "Core Protocol" }, - "ai": { name: "AI and Agents" }, - "chain-abstraction": { name: "Chain Abstraction" }, - "smart-contracts": { name: "Smart Contracts" }, - "web3-apps": { name: "Web3 Applications" }, - "primitives": { name: "Tokens and Primitives" }, - "tools": { name: "Developer Tools" }, - "tutorials": { name: "Tutorials and Examples" }, - "api": { name: "API Reference" }, - "data-infrastructure": { name: "Data Infrastructure" }, - "integrations": { name: "Integration Examples" }, - "resources": { name: "Resources" } - }; - - let documentationContent = `# NEAR Protocol Documentation - -> NEAR is a layer-1 blockchain built for scale and multichain compatibility, featuring AI-native infrastructure and chain abstraction capabilities. This documentation covers smart contracts, Web3 applications, AI agents, cross-chain development, and the complete NEAR ecosystem. -NEAR Protocol is a proof-of-stake blockchain that enables developers to build decentralized applications with seamless user experiences. Key features include human-readable account names, minimal transaction fees, and built-in developer tools. The platform supports multiple programming languages and provides chain abstraction for cross-blockchain interactions. -This documentation is organized into several main sections: Protocol fundamentals, AI and agent development, chain abstraction features, smart contract development, Web3 application building, and comprehensive API references. Each section includes tutorials, examples, and detailed technical specifications. - -`; - const links =[]; - for (const sectionKey in documentationSections) { - const section = documentationSections[sectionKey]; - const sectionPages = documentationPages[sectionKey] || []; - - let sectionContent = `## ${section.name}\n`; - - const orderedPages = sectionPages.sort((a, b) => { - return a.url.localeCompare(b.url); - }); - - for (const page of orderedPages) { - const cleanDescription = (page.description || page.title) - .replace(/\s*\n\s*/g, ' ') - .trim(); - links.push(`https://docs.near.org/${page.url}`); - - sectionContent += `- [${page.title}](https://docs.near.org/${page.url}): ${cleanDescription}\n`; - } - - documentationContent += sectionContent + '\n'; - } - - const outputFilePath = path.join(BUILD_DIR, 'llms.txt'); - const outputDirectory = BUILD_DIR; - - if (!fs.existsSync(outputDirectory)) { - fs.mkdirSync(outputDirectory, { recursive: true }); - } - - fs.writeFileSync(outputFilePath, documentationContent, 'utf-8'); - - console.log("Checking links..."); - const results = await Promise.all(links.map(checkLink)); - const broken = results.filter(r => !r.ok); - if (broken.length > 0) { - console.log('\nš“ Broken URLs:'); - broken.forEach(b => console.log(`${b.url} - Status: ${b.status}`)); - } else { - console.log('š¢ All links are valid'); - } -} - -processMarkdownFiles(); diff --git a/website/scripts/copy-md-to-static.mjs b/website/scripts/copy-md-to-static.mjs new file mode 100644 index 00000000000..6ffe0dedf6a --- /dev/null +++ b/website/scripts/copy-md-to-static.mjs @@ -0,0 +1,570 @@ +#!/usr/bin/env node + +import { globSync } from 'glob'; +import path from 'path'; +import fs from 'fs'; +import { + DOCS_DIR, + BUILD_DIR, + BASE_URL, + extractFrontmatter, +} from './shared.mjs'; + + +export const DOCUMENTATION_SECTIONS = { + protocol: 'Core Protocol', + ai: 'AI and Agents', + 'chain-abstraction': 'Chain Abstraction', + 'smart-contracts': 'Smart Contracts', + 'web3-apps': 'Web3 Applications', + primitives: 'Tokens and Primitives', + tools: 'Developer Tools', + tutorials: 'Tutorials and Examples', + api: 'API Reference', + 'data-infrastructure': 'Data Infrastructure', + integrations: 'Integration Examples', + aurora: 'Aurora', + quest: 'Learning Quests', +}; + + +const SKIPPED_FILES = new Set(['index.md', 'help.md']); + +const githubCache = new Map(); +let cacheHits = 0; + +const JSX_COMPONENTS = [ + 'TabItem', 'Tabs', 'CodeTabs', + 'Card', 'ConceptCard', + 'SplitLayoutContainer', 'SplitLayoutLeft', 'SplitLayoutRight', + 'Language', 'Block', + 'Quiz', 'Progress','MultipleChoice', 'Option', + 'LantstoolLabel', 'TryOutOnLantstool', + 'MovingForwardSupportSection', 'SigsSupport', 'TryDemo', + 'ExplainCode', 'CodeBlock', + 'LandingHero', 'Faucet', 'AIBadges', + 'CreateTokenForm', 'MintNFT', + 'FeatureList', 'Column', 'Feature', +]; + +const LLMS_TXT_HEADER = `# NEAR Protocol Documentation + +> NEAR is a layer-1 blockchain built for scale and multichain compatibility, +> featuring AI-native infrastructure and chain abstraction capabilities. +> This documentation covers smart contracts, Web3 applications, AI agents, +> cross-chain development, and the complete NEAR ecosystem. + +NEAR Protocol is a proof-of-stake blockchain that enables developers to build +decentralized applications with seamless user experiences. Key features include +human-readable account names, minimal transaction fees, and built-in developer +tools. The platform supports multiple programming languages and provides chain +abstraction for cross-blockchain interactions. + +This documentation is organized into several main sections: Protocol fundamentals, +AI and agent development, chain abstraction features, smart contract development, +Web3 application building, and comprehensive API references. Each section includes +tutorials, examples, and detailed technical specifications. + + +`; + + +function parseGitHubTag(tag) { + const normalized = tag.replace(/'/g, '"'); + + const urlMatch = normalized.match(/url="(.*?)"/); + if (!urlMatch) return null; + + const url = urlMatch[1].split('#')[0]; + const urlObj = new URL(url); + const segments = urlObj.pathname.slice(1).split('/'); + if (segments.length < 4) return null; + + const [org, repo, , branch, ...rest] = segments; + const filePath = rest.join('/'); + + return { + rawUrl: `https://raw.githubusercontent.com/${org}/${repo}/${branch}/${filePath}`, + normalized, + }; +} + +async function fetchGitHubCode(tag) { + const parsed = parseGitHubTag(tag); + if (!parsed) { + console.warn('Invalid GitHub tag format'); + return null; + } + + if (githubCache.has(parsed.rawUrl)) { + cacheHits++; + return githubCache.get(parsed.rawUrl); + } + + try { + const response = await fetch(parsed.rawUrl); + + if (!response.ok) { + throw new Error(`HTTP ${response.status}`); + } + + const code = await response.text(); + const result = { code, normalized: parsed.normalized }; + + githubCache.set(parsed.rawUrl, result); + + return result; + } catch (error) { + console.error(`ā Failed to fetch ${parsed.rawUrl}: ${error.message}`); + return null; + } +} + +function extractCodeSlice(code, tagAttrs) { + const startMatch = tagAttrs.match(/start="(\d*)"/); + const endMatch = tagAttrs.match(/end="(\d*)"/); + + const lines = String(code).split('\n'); + const start = startMatch ? Math.max(parseInt(startMatch[1]) - 1, 0) : 0; + const end = endMatch ? parseInt(endMatch[1]) + 1 : lines.length; + + return lines.slice(start, end).join('\n'); +} + +async function replaceTagsWithCode(content, tagName, { includeLanguage = false } = {}) { + const tagRegex = new RegExp(`<${tagName}\\s[^>]*?(?:\\/>|>[^<]*<\\/${tagName}>)`, 'g'); + const tags = content.match(tagRegex) || []; + + for (const tag of tags) { + const codeData = await fetchGitHubCode(tag); + if (!codeData) continue; + + let language = ''; + if (includeLanguage) { + const langMatch = codeData.normalized.match(/language="(.*?)"/); + language = langMatch ? langMatch[1] : ''; + } + + const slice = extractCodeSlice(codeData.code, codeData.normalized); + content = content.replace(tag, `\`\`\`${language}\n${slice}\n\`\`\``); + } + + return content; +} + +function transformOutsideCodeBlocks(content, transformFn) { + const segments = content.split(/(```[\s\S]*?```)/g); + return segments.map((segment, index) => { + if (index % 2 === 1) return segment; // code block, keep as-is + return transformFn(segment); + }).join(''); +} + +function removeImports(content) { + return transformOutsideCodeBlocks(content, (text) => + text + .replace(/^import\s+.*?from\s+['"].*?['"];?\s*$/gm, '') + .replace(/^import\s+\{[^}]*\}\s+from\s+['"].*?['"];?\s*$/gm, '') + .replace(/^import\s+.*?$/gm, '') + ); +} + + +function convertComponentTitles(content) { + const openTag = /^\s*<(?:Card|ConceptCard)\b/; + const lines = content.split('\n'); + const result = []; + let collecting = false; + let collected = ''; + + for (const line of lines) { + if (!collecting && openTag.test(line)) { + collected = line; + collecting = true; + const trimmed = line.trim(); + if (trimmed.endsWith('>') || trimmed.endsWith('/>')) { + const heading = extractHeadingFromAttrs(collected); + result.push(heading !== null ? heading : line); + collecting = false; + collected = ''; + } + continue; + } + + if (collecting) { + collected += '\n' + line; + const trimmed = line.trim(); + if (trimmed === '>' || trimmed === '/>') { + const heading = extractHeadingFromAttrs(collected); + result.push(heading !== null ? heading : collected); + collecting = false; + collected = ''; + } + continue; + } + + result.push(line); + } + + return result.join('\n'); +} + +function extractHeadingFromAttrs(tagText) { + const titleMatch = tagText.match(/title=["']([^"']+)["']/); + if (!titleMatch) return null; + + const title = titleMatch[1]; + const hrefMatch = tagText.match(/href=["']([^"']+)["']/); + return hrefMatch ? `### [${title}](${hrefMatch[1]})` : `### ${title}`; +} + +function stripJsx(content) { + const names = JSX_COMPONENTS.join('|'); + const singleLineSelfClosing = new RegExp(`^\\s*<(?:${names})\\b.*/>\\s*$`); + const singleLineOpening = new RegExp(`^\\s*<(?:${names})\\b.*>\\s*$`); + const closingTag = new RegExp(`^\\s*(?:${names})\\s*>\\s*$`); + const multiLineStart = new RegExp(`^\\s*<(?:${names})\\b`); + + const lines = content.split('\n'); + const result = []; + let insideMultiLineTag = false; + + for (const line of lines) { + if (insideMultiLineTag) { + const trimmed = line.trim(); + if (trimmed === '/>' || trimmed === '>' || trimmed.endsWith('/>') || trimmed.endsWith('>')) { + insideMultiLineTag = false; + } + continue; + } + + if (singleLineSelfClosing.test(line)) continue; + if (singleLineOpening.test(line)) continue; + if (closingTag.test(line)) continue; + + if (multiLineStart.test(line) && !line.includes('>')) { + insideMultiLineTag = true; + continue; + } + + result.push(line); + } + + content = result.join('\n'); + + return content + .replace(/