diff --git a/apps/docs/content/references/networking/public-access.mdx b/apps/docs/content/references/networking/public-access.mdx index 7701b482..c7ac3611 100644 --- a/apps/docs/content/references/networking/public-access.mdx +++ b/apps/docs/content/references/networking/public-access.mdx @@ -4,23 +4,8 @@ description: Detailed guide for configuring public internet access to your Zerop --- import Image from '/src/components/Image'; -import GroupCards from '/src/components/GroupCards' import Video from '/src/components/Video'; -export const languages = [ - { name: "Bun", link: "/java/how-to/build-pipeline#ports" }, - { name: "Deno", link: "/go/how-to/build-pipeline#ports" }, - { name: ".NET", link: "/dotnet/how-to/build-pipeline#ports" }, - { name: "Elixir", link: "/php/how-to/build-pipeline#ports" }, - { name: "Gleam", link: "/dotnet/how-to/build-pipeline#ports" }, - { name: "Go", link: "/go/how-to/build-pipeline#ports" }, - { name: "Java", link: "/java/how-to/build-pipeline#ports" }, - { name: "Node.js", link: "/nodejs/how-to/build-pipeline#ports" }, - { name: "PHP", link: "/php/how-to/build-pipeline#ports" }, - { name: "Python", link: "/python/how-to/build-pipeline#ports" }, - { name: "Rust", link: "/rust/how-to/build-pipeline#ports" }, -] - This guide provides detailed configuration instructions for making your Zerops services publicly accessible from the internet. For an overview of all access methods, see the [Access & Networking guide](/features/access). ## Public Access Methods diff --git a/apps/docs/src/plugins/markdown-source/index.js b/apps/docs/src/plugins/markdown-source/index.js index 18694a7c..ffdd36c2 100644 --- a/apps/docs/src/plugins/markdown-source/index.js +++ b/apps/docs/src/plugins/markdown-source/index.js @@ -4,8 +4,88 @@ const path = require('path'); /** * Docusaurus plugin to copy raw markdown files to build output * This allows users to view markdown source by appending .md to URLs + * Now handles imported MDX files by inlining their content */ +// Load data.json for variable substitution +let dataJson = null; +function getDataJson(siteDir) { + if (!dataJson) { + try { + const dataPath = path.join(siteDir, 'static', 'data.json'); + dataJson = JSON.parse(fs.readFileSync(dataPath, 'utf8')); + } catch (error) { + console.warn('[markdown-source-plugin] Could not load data.json:', error.message); + dataJson = {}; + } + } + return dataJson; +} + +// Extract title and description from frontmatter +function extractFrontmatter(content) { + const frontmatterMatch = content.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n/); + + if (!frontmatterMatch) { + return { title: null, description: null, content }; + } + + const frontmatter = frontmatterMatch[1]; + const titleMatch = frontmatter.match(/^title:\s*(.+)$/m); + const descMatch = frontmatter.match(/^description:\s*(.+)$/m); + + const title = titleMatch ? titleMatch[1].trim() : null; + const description = descMatch ? descMatch[1].trim() : null; + + // Remove frontmatter from content + const contentWithoutFrontmatter = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, ''); + + return { title, description, content: contentWithoutFrontmatter }; +} + +// Substitute variables in content +function substituteVariables(content, siteDir) { + const data = getDataJson(siteDir); + + // Replace {data.path.to.value} with actual values + content = content.replace(/\{data\.([^}]+)\}/g, (match, path) => { + try { + // Split path and traverse the data object + const keys = path.split('.'); + let value = data; + + for (const key of keys) { + if (value && typeof value === 'object' && key in value) { + value = value[key]; + } else { + // Path not found, return original expression + return match; + } + } + + // Handle different value types + if (Array.isArray(value)) { + // For arrays, join with commas + return value.join(', '); + } else if (typeof value === 'object') { + // For objects, try to convert to a readable string + return JSON.stringify(value); + } else { + // For primitives, convert to string + return String(value); + } + } catch (error) { + console.warn(`[markdown-source-plugin] Error resolving variable: ${path}`, error.message); + return match; // Keep original if resolution fails + } + }); + + // Also handle template literals like ${variable} - just remove them as context isn't available + content = content.replace(/\$\{[^}]+\}/g, ''); + + return content; +} + // Convert Tabs/TabItem components to readable markdown format function convertTabsToMarkdown(content) { const tabsPattern = /]*>([\s\S]*?)<\/Tabs>/g; @@ -50,56 +130,226 @@ function convertDetailsToMarkdown(content) { }); } +// Convert FAQ components to readable markdown format +function convertFAQToMarkdown(content) { + // Convert FAQItem components + content = content.replace( + /([\s\S]*?)<\/FAQItem>/g, + (match, question, answer) => { + const cleanAnswer = answer.trim(); + return `**Question: ${question}**\n\n${cleanAnswer}\n`; + } + ); + + // Remove FAQ wrapper tags but keep content + content = content.replace(/([\s\S]*?)<\/FAQ>/g, (match, content) => { + return content; + }); + + return content; +} + +// Preserve markdown tables while processing other content +function preserveTablesWhileProcessing(content, processingFn) { + // Split content by tables + const tablePattern = /(\|.*\|\n\|.*\|\n(?:\|.*\|\n)*)/g; + const sections = content.split(tablePattern); + + let result = ''; + + for (let i = 0; i < sections.length; i++) { + const section = sections[i]; + if (!section) continue; + + // If section starts with |, it's a table - preserve it + if (section.trim().startsWith('|')) { + result += section; + } else { + // Process non-table content + result += processingFn(section); + } + } + + return result; +} + +// Resolve and inline imported MDX files +function resolveImports(content, currentFilePath, docsDir, siteDir, processedFiles = new Set()) { + // Prevent circular imports + const normalizedPath = path.normalize(currentFilePath); + if (processedFiles.has(normalizedPath)) { + console.warn(`[markdown-source-plugin] Circular import detected: ${currentFilePath}`); + return content; + } + processedFiles.add(normalizedPath); + + // Match import statements for MDX files + // Matches: import Something from './path/to/file.mdx' + // Matches: import Something from '@site/path/to/file.mdx' + // Matches: import Something from 'src/components/content/file.mdx' + const importPattern = /^import\s+(?:\{[^}]+\}|\w+)\s+from\s+['"]([^'"]+\.mdx?)['"];?\s*$/gm; + + let match; + const imports = []; + + while ((match = importPattern.exec(content)) !== null) { + imports.push({ + fullMatch: match[0], + importPath: match[1], + index: match.index + }); + } + + // Process imports in reverse order to maintain correct indices + imports.reverse(); + + for (const { fullMatch, importPath } of imports) { + try { + // Resolve the import path + let resolvedPath; + + if (importPath.startsWith('@site/')) { + // Handle @site alias + resolvedPath = path.join(siteDir, importPath.replace('@site/', '')); + } else if (importPath.startsWith('./') || importPath.startsWith('../')) { + // Handle relative imports + const currentDir = path.dirname(path.join(docsDir, currentFilePath)); + resolvedPath = path.resolve(currentDir, importPath); + } else if (importPath.startsWith('src/components/content/')) { + // Handle src/components/content/ imports (content templates) + resolvedPath = path.join(siteDir, importPath); + console.log(`[markdown-source-plugin] Resolving content template: ${importPath} -> ${resolvedPath}`); + } else if (importPath.includes('/components/content/')) { + // Handle variations like @/components/content/ or ~/components/content/ + const contentPath = importPath.substring(importPath.indexOf('components/content/')); + resolvedPath = path.join(siteDir, 'src', contentPath); + console.log(`[markdown-source-plugin] Resolving content template: ${importPath} -> ${resolvedPath}`); + } else { + // Skip non-local imports (probably npm packages or other non-content imports) + console.log(`[markdown-source-plugin] Skipping non-local import: ${importPath}`); + continue; + } + + // Check if file exists + if (!fs.existsSync(resolvedPath)) { + console.warn(`[markdown-source-plugin] Import not found: ${resolvedPath}`); + continue; + } + + // Read the imported file + const importedContent = fs.readFileSync(resolvedPath, 'utf8'); + + // Remove frontmatter from imported content + const { content: contentWithoutFrontmatter } = extractFrontmatter(importedContent); + + // Recursively resolve imports in the imported file + const relativeImportPath = path.relative(docsDir, resolvedPath); + const processedImportContent = resolveImports( + contentWithoutFrontmatter, + relativeImportPath, + docsDir, + siteDir, + new Set(processedFiles) + ); + + // Replace the import statement with the imported content + content = content.replace( + fullMatch, + `\n${processedImportContent}\n` + ); + + console.log(` ✓ Inlined import: ${importPath} into ${currentFilePath}`); + } catch (error) { + console.error(`[markdown-source-plugin] Error processing import ${importPath}:`, error.message); + } + } + + return content; +} + // Clean markdown content for raw display - remove MDX/Docusaurus-specific syntax -function cleanMarkdownForDisplay(content, filepath) { +function cleanMarkdownForDisplay(content, filepath, siteDir, docsDir) { // Get the directory path for this file (relative to docs root) - const fileDir = filepath.replace(/[^/]*$/, ''); // Remove filename, keep directory + const fileDir = filepath.replace(/[^/]*$/, ''); + + // 1. Extract frontmatter and get title/description + const { title, description, content: contentWithoutFrontmatter } = extractFrontmatter(content); + content = contentWithoutFrontmatter; + + // 2. Resolve and inline imported MDX files FIRST (before removing imports) + content = resolveImports(content, filepath, docsDir, siteDir); + + // 3. Remove JSX-style comments + content = content.replace(/\{\/\*[\s\S]*?\*\/\}/g, ''); - // 1. Strip YAML front matter (--- at start, content, then ---) - content = content.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n/, ''); + // 4. Remove
tags (replace with newlines) + content = content.replace(//g, '\n'); - // 2. Remove import statements (MDX imports) + // 5. Remove remaining import statements (after inlining MDX imports) content = content.replace(/^import\s+.*?from\s+['"].*?['"];?\s*$/gm, ''); - // 3. Convert HTML images to markdown - // Pattern:

...

+ // 6. Substitute variables like {data.something} + content = substituteVariables(content, siteDir); + + // 7. Convert HTML images to markdown + // 7a. Convert React components to markdown (before removing figure tags) + content = content.replace( + /]*(?:lightImage|src)=["']([^"']+)["'][^>]*alt=["']([^"']+)["'][^>]*\/?>/g, + (match, src, alt) => { + return `![${alt}](${src})`; + } + ); + + // 7b. Convert standard HTML tags to markdown + content = content.replace( + /]*src=["']([^"']+)["'][^>]*alt=["']([^"']*?)["'][^>]*\/?>/g, + (match, src, alt) => { + return `![${alt || 'image'}](${src})`; + } + ); + + // 7c. Convert HTML images with require() syntax content = content.replace( /

\s*\n?\s*([^\s*\n?\s*<\/p>/g, (match, imagePath, alt) => { - // Clean the path: remove @site/static prefix const cleanPath = imagePath.replace('@site/static/', '/'); return `![${alt}](${cleanPath})`; } ); - // 4. Convert YouTube iframes to text links + // 8. Remove figure tags but keep any text content inside + content = content.replace(/]*>([\s\S]*?)<\/figure>/g, '$1'); + + // 9. Convert YouTube iframes to text links content = content.replace( /]*src="https:\/\/www\.youtube\.com\/embed\/([a-zA-Z0-9_-]+)[^"]*"[^>]*title="([^"]*)"[^>]*>[\s\S]*?<\/iframe>/g, 'Watch the video: [$2](https://www.youtube.com/watch?v=$1)' ); - // 5. Clean HTML5 video tags - keep HTML but add fallback text + // 10. Clean HTML5 video tags - keep HTML but add fallback text content = content.replace( /]*>\s*]*>\s*<\/video>/g, '

Video demonstration: $1

\n' ); - // 6. Remove components with structured data (SEO metadata not needed in raw markdown) + // 11. Remove components content = content.replace(/[\s\S]*?<\/Head>/g, ''); - // 7. Convert Tabs/TabItem components to readable markdown (preserve content) + // 12. Convert FAQ components to readable markdown (preserve content) + content = convertFAQToMarkdown(content); + + // 13. Convert Tabs/TabItem components to readable markdown (preserve content) content = convertTabsToMarkdown(content); - // 8. Convert details/summary components to readable markdown (preserve content) + // 14. Convert details/summary components to readable markdown (preserve content) content = convertDetailsToMarkdown(content); - // 9. Remove custom React/MDX components (FAQStructuredData, etc.) - // Matches both self-closing and paired tags: or ... - // This runs AFTER Tabs/details conversion to preserve their content - content = content.replace(/<[A-Z][a-zA-Z]*[\s\S]*?(?:\/>|<\/[A-Z][a-zA-Z]*>)/g, ''); + // 15. Remove custom React/MDX components while preserving tables + content = preserveTablesWhileProcessing(content, (section) => { + return section.replace(/<[A-Z][a-zA-Z]*[\s\S]*?(?:\/>|<\/[A-Z][a-zA-Z]*>)/g, ''); + }); - // 10. Convert relative image paths to absolute paths from /docs/ root (Claude style) - // Matches: ![alt](./img/file.png) or ![alt](img/file.png) + // 16. Convert relative image paths to absolute paths content = content.replace( /!\[([^\]]*)\]\((\.\/)?img\/([^)]+)\)/g, (match, alt, relPrefix, filename) => { @@ -108,7 +358,30 @@ function cleanMarkdownForDisplay(content, filepath) { } ); - // 11. Remove any leading blank lines + // 17. Remove consecutive blank lines (keep max 2 newlines = 1 blank line) + const lines = content.split('\n'); + const processedLines = []; + let lastLineWasEmpty = false; + + for (const line of lines) { + const trimmedLine = line.trim(); + + if (trimmedLine === '' && lastLineWasEmpty) { + continue; // Skip consecutive empty lines + } + + processedLines.push(line); + lastLineWasEmpty = trimmedLine === ''; + } + + content = processedLines.join('\n'); + + // 18. Add title as H1 at the beginning if it exists + if (title) { + content = `# ${title}\n\n${content}`; + } + + // 19. Remove any leading blank lines content = content.replace(/^\s*\n/, ''); return content; @@ -197,7 +470,6 @@ module.exports = function markdownSourcePlugin(context, options) { // Find all markdown files in docs directory const mdFiles = findMarkdownFiles(docsDir); - let copiedCount = 0; // Process each markdown file to build directory @@ -214,8 +486,8 @@ module.exports = function markdownSourcePlugin(context, options) { // Read the markdown file const content = await fs.readFile(sourcePath, 'utf8'); - // Clean markdown for raw display - const cleanedContent = cleanMarkdownForDisplay(content, mdFile); + // Pass siteDir and docsDir for import resolution + const cleanedContent = cleanMarkdownForDisplay(content, mdFile, context.siteDir, docsDir); // Write the cleaned content await fs.writeFile(destPath, cleanedContent, 'utf8'); diff --git a/apps/docs/static/llms-full.txt b/apps/docs/static/llms-full.txt index 9672f2ac..5bd8569a 100644 --- a/apps/docs/static/llms-full.txt +++ b/apps/docs/static/llms-full.txt @@ -20695,19 +20695,6 @@ Return custom content for specific conditions: # References > Networking > Public Access -export const languages = [ - { name: "Bun", link: "/java/how-to/build-pipeline#ports" }, - { name: "Deno", link: "/go/how-to/build-pipeline#ports" }, - { name: ".NET", link: "/dotnet/how-to/build-pipeline#ports" }, - { name: "Elixir", link: "/php/how-to/build-pipeline#ports" }, - { name: "Gleam", link: "/dotnet/how-to/build-pipeline#ports" }, - { name: "Go", link: "/go/how-to/build-pipeline#ports" }, - { name: "Java", link: "/java/how-to/build-pipeline#ports" }, - { name: "Node.js", link: "/nodejs/how-to/build-pipeline#ports" }, - { name: "PHP", link: "/php/how-to/build-pipeline#ports" }, - { name: "Python", link: "/python/how-to/build-pipeline#ports" }, - { name: "Rust", link: "/rust/how-to/build-pipeline#ports" }, -] This guide provides detailed configuration instructions for making your Zerops services publicly accessible from the internet. For an overview of all access methods, see the [Access & Networking guide](/features/access). ## Public Access Methods Choose the access method that best fits your needs: diff --git a/zerops.yml b/zerops.yml index a68b5443..b4844012 100644 --- a/zerops.yml +++ b/zerops.yml @@ -16,3 +16,18 @@ zerops: - apps/docs/build/~ run: base: static + initCommands: + - sudo sed -i 's/^\(\s*text\/plain\s\+\)txt;$/\1txt md;/' /etc/nginx/mime.types + routing: + headers: + - for: "~* \.md$*" + values: + Content-Disposition: '"inline" always' + X-Content-Type-Options: '"nosniff"' + X-Robots-Tag: '"googlebot: noindex, nofollow, bingbot: noindex, nofollow"' + Cache-Control: '"public, max-age=3600, must-revalidate"' + - for: "~* ^/.*/img/.*" + values: + X-Robots-Tag: '"googlebot: noindex, nofollow, bingbot: noindex, nofollow"' + Cache-Control: '"public, max-age=86400, immutable"' +