Building Intelligent Documentation System with Jekyll and Cloudflare

Documentation systems require sophisticated features like versioning, intelligent search, API integration, and content reuse—capabilities that traditionally demand complex dynamic platforms. By leveraging Jekyll's extensibility and Cloudflare's edge computing, we can build a documentation platform that rivals commercial solutions while maintaining static site benefits. This technical guide details the implementation of a multi-version documentation system with intelligent search, Swagger/OpenAPI integration, and advanced content management features, all built on Jekyll and enhanced with Cloudflare Workers.

In This Guide

Multi-version Documentation Architecture

The multi-version architecture manages documentation for multiple product versions while maintaining shared content and version-specific overrides. The system uses Git branches for version isolation and Jekyll collections for content organization.

The architecture employs a base-and-override pattern where shared content lives in main branches, while version-specific content resides in version branches. During build, the system merges shared and version-specific content, resolving conflicts through predefined precedence rules. Cloudflare Workers handle version routing and provide seamless navigation between documentation versions.


// Version Management Structure:
// main/
//   ├── _docs/
//   │   ├── getting-started/
//   │   ├── api/
//   │   └── shared/          # Cross-version content
//   └── _config.yml
// 
// v2.0/
//   ├── _docs/
//   │   ├── api/            # Version-specific API docs
//   │   └── migration.md    # Version-specific content
//   └── _config.yml
//
// v1.0/
//   ├── _docs/
//   │   └── deprecated/     # Old version content
//   └── _config.yml

// Build Process:
// 1. Checkout version branch
// 2. Merge with shared content from main
// 3. Resolve conflicts (version-specific wins)
// 4. Build static site
// 5. Deploy to version-specific path

// Version Routing Worker:
async function handleVersionRouting(request) {
  const url = new URL(request.url);
  const version = detectVersionFromPath(url) || 
                  detectVersionFromCookie(request) ||
                  getLatestVersion();
  
  // Redirect to appropriate version
  if (!url.pathname.startsWith(`/docs/${version}/`)) {
    return Response.redirect(`/docs/${version}${url.pathname}`);
  }
  
  return fetch(request);
}

Advanced Content Reuse and Transclusion System

Content reuse eliminates duplication across documentation versions and enables modular content management. The system implements transclusion (include-with-parameters), variables, and conditional content blocks.

Here's the Jekyll plugin for advanced content reuse:


# _plugins/content_reuse.rb
module Jekyll
  class TranscludeTag < Liquid::Tag
    def initialize(tag_name, markup, tokens)
      super
      params = markup.strip.split(/\s+/, 3)
      @source = params[0]
      @context = params[1] || 'default'
      @variables = parse_variables(params[2])
    end

    def render(context)
      # Find source content
      source_content = find_source_content(@source, context)
      
      # Apply variable substitution
      content = substitute_variables(source_content, @variables)
      
      # Apply context-specific transformations
      content = apply_context_transform(content, @context)
      
      # Render includes within the transcluded content
      render_nested_includes(content, context)
    end
    
    private
    
    def find_source_content(source, context)
      # Look in current version first, then shared content
      site = context.registers[:site]
      
      # Check version-specific collections
      doc = site.collections['docs'].docs.find { |d| d.basename == source }
      return doc.content if doc
      
      # Check shared content
      shared_doc = site.collections['shared'].docs.find { |d| d.basename == source }
      return shared_doc.content if shared_doc
      
      # Fallback to includes
      include_path = File.join(site.source, '_includes', source)
      File.read(include_path) if File.exist?(include_path)
    end
    
    def substitute_variables(content, variables)
      variables.each do |key, value|
        content = content.gsub("{{#{key}}}", value)
      end
      content
    end
  end
end

Liquid::Template.register_tag('transclude', Jekyll::TranscludeTag)

# Usage in documentation:
# {% transclude installation_notes context="windows" version="2.1" %}
# {% transclude api_example context="python" endpoint="/users" %}

Swagger/OpenAPI Documentation Integration

Automated API documentation generation from Swagger/OpenAPI specifications ensures accuracy and reduces maintenance. The system parses OpenAPI files and generates interactive API documentation with Jekyll.

Here's the OpenAPI integration plugin:


# _plugins/openapi_generator.rb
require 'yaml'
require 'json-schema'

module Jekyll
  class OpenAPIGenerator < Generator
    def generate(site)
      @site = site
      
      # Process OpenAPI specifications
      Dir.glob('_api/*.{yaml,yml,json}').each do |spec_file|
        generate_api_docs(spec_file)
      end
    end
    
    private
    
    def generate_api_docs(spec_file)
      spec = load_specification(spec_file)
      base_name = File.basename(spec_file, '.*')
      
      # Generate overview page
      generate_overview_page(spec, base_name)
      
      # Generate endpoint pages
      generate_endpoint_pages(spec, base_name)
      
      # Generate schemas pages
      generate_schema_pages(spec, base_name)
    end
    
    def generate_endpoint_pages(spec, base_name)
      spec['paths'].each do |path, methods|
        methods.each do |http_method, definition|
          page = ApiEndpointPage.new(@site, base_name, path, http_method, definition)
          @site.pages << page
        end
      end
    end
    
    def generate_schema_pages(spec, base_name)
      return unless spec['components'] && spec['components']['schemas']
      
      spec['components']['schemas'].each do |name, schema|
        page = ApiSchemaPage.new(@site, base_name, name, schema)
        @site.pages << page
      end
    end
  end

  class ApiEndpointPage < Page
    def initialize(site, api_name, path, method, definition)
      @site = site
      @base = site.source
      @dir = "docs/api/#{api_name}"
      
      # Convert path to filename
      filename = path.gsub(/[{}]/) { |m| m == '{' ? '_' : '' }.gsub('/', '_')
      @name = "#{filename}_#{method}.html"
      
      self.process(@name)
      self.data = {
        'layout' => 'api_endpoint',
        'title' => "#{method.upcase} #{path}",
        'api_name' => api_name,
        'path' => path,
        'method' => method,
        'definition' => definition
      }
      
      # Generate content from template
      self.content = generate_endpoint_content(path, method, definition)
    end
  end
end

# API Endpoint Template (_layouts/api_endpoint.html)

{{ page.method | upcase }} {{ page.path }}

{{ page.definition.description | markdownify }}
{% if page.definition.parameters %}

Parameters

{% for param in page.definition.parameters %}
{{ param.name }} ({{ param.in }}) {{ param.schema.type }} {% if param.required %}required{% endif %}
{{ param.description | markdownify }}
{% endfor %}
{% endif %}

Documentation-Specific Search Implementation

Documentation search requires understanding code examples, API references, and conceptual content. The system implements specialized indexing and ranking for technical documentation.

Here's the documentation-specific search implementation:


// Documentation search index generator
class DocumentationSearch {
  constructor() {
    this.index = {
      concepts: new Map(),
      api: new Map(),
      code: new Map(),
      errors: new Map()
    };
  }

  indexPage(page) {
    const content = this.extractContent(page);
    
    // Index by content type
    this.indexConcepts(content.concepts, page);
    this.indexAPI(content.api, page);
    this.indexCodeExamples(content.code, page);
    this.indexErrorReferences(content.errors, page);
  }

  indexAPI(apiElements, page) {
    apiElements.forEach(element => {
      const key = this.normalizeAPIKey(element.name);
      const entry = this.index.api.get(key) || [];
      
      entry.push({
        page: page.url,
        element: element,
        context: this.extractAPIContext(element),
        relevance: this.calculateAPIRelevance(element)
      });
      
      this.index.api.set(key, entry);
    });
  }

  search(query, filters = {}) {
    const results = {
      concepts: this.searchConcepts(query),
      api: this.searchAPI(query),
      code: this.searchCode(query),
      combined: []
    };
    
    // Combine and rank results
    results.combined = this.combineResults(results, query);
    
    // Apply filters
    if (filters.version) {
      results.combined = results.combined.filter(r => 
        r.page.includes(filters.version)
      );
    }
    
    return results;
  }

  searchAPI(query) {
    const terms = this.tokenizeQuery(query);
    const results = new Set();
    
    terms.forEach(term => {
      // Exact API matches
      if (this.index.api.has(term)) {
        results.add(...this.index.api.get(term));
      }
      
      // Partial matches
      for (const [key, entries] of this.index.api) {
        if (key.includes(term)) {
          results.add(...entries);
        }
      }
    });
    
    return Array.from(results)
      .sort((a, b) => b.relevance - a.relevance);
  }
}

// Cloudflare Worker for documentation search
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    if (url.pathname === '/api/docs/search') {
      const { q, version, type } = Object.fromEntries(url.searchParams);
      
      // Load appropriate search index for version
      const indexKey = `search-index-${version || 'latest'}`;
      const indexData = await env.DOCS_BUCKET.get(indexKey);
      
      if (!indexData) {
        return new Response('Index not found', { status: 404 });
      }
      
      const search = new DocumentationSearch();
      search.loadIndex(await indexData.json());
      
      const results = search.search(q, { version, type });
      
      return new Response(JSON.stringify(results), {
        headers: { 'Content-Type': 'application/json' }
      });
    }
    
    return fetch(request);
  }
}

The navigation system generates context-aware menus and breadcrumbs based on documentation structure and user behavior. The implementation uses Jekyll data files and client-side enhancement.


// Dynamic navigation generator
class DocumentationNavigation {
  constructor(structure, currentPath) {
    this.structure = structure;
    this.currentPath = currentPath;
  }

  generateBreadcrumbs() {
    const pathParts = this.currentPath.split('/').filter(p => p);
    const breadcrumbs = [];
    
    let currentPath = '';
    pathParts.forEach(part => {
      currentPath += `/${part}`;
      const node = this.findNodeByPath(currentPath);
      
      if (node) {
        breadcrumbs.push({
          title: node.title,
          url: node.url,
          current: currentPath === this.currentPath
        });
      }
    });
    
    return breadcrumbs;
  }

  generateSidebar() {
    const currentSection = this.findCurrentSection();
    return this.buildSidebarTree(currentSection);
  }

  buildSidebarTree(section, depth = 0) {
    return {
      title: section.title,
      url: section.url,
      children: section.children?.map(child => 
        this.buildSidebarTree(child, depth + 1)
      ),
      expanded: this.isSectionExpanded(section),
      current: this.isSectionCurrent(section)
    };
  }

  generateContextualLinks() {
    const currentPage = this.findCurrentPage();
    if (!currentPage) return [];
    
    return {
      previous: this.findPreviousPage(currentPage),
      next: this.findNextPage(currentPage),
      related: this.findRelatedPages(currentPage)
    };
  }

  findRelatedPages(page) {
    // Based on content similarity and user behavior
    const related = [];
    
    // Same category
    related.push(...this.findPagesInSameCategory(page));
    
    // API references for conceptual pages
    if (page.type === 'concept') {
      related.push(...this.findAPIPagesForConcept(page));
    }
    
    // Code examples for API pages
    if (page.type === 'api') {
      related.push(...this.findCodeExamplesForAPI(page));
    }
    
    return related.slice(0, 5); // Limit to 5 related pages
  }
}

// Jekyll navigation data generator
# _plugins/navigation_generator.rb
module Jekyll
  class NavigationGenerator < Generator
    def generate(site)
      navigation = build_navigation_structure(site)
      site.data['navigation'] = navigation
      
      # Generate breadcrumb data for each page
      site.pages.each do |page|
        page.data['breadcrumbs'] = generate_breadcrumbs(page, navigation)
      end
    end
    
    private
    
    def build_navigation_structure(site)
      # Build hierarchical navigation from directory structure
      structure = {}
      
      site.pages.each do |page|
        next unless page.url.start_with?('/docs/')
        
        path_parts = page.url.split('/').reject(&:empty?)
        current_level = structure
        
        path_parts.each_with_index do |part, index|
          current_level[part] ||= {
            title: infer_title(part, page),
            url: '/' + path_parts[0..index].join('/'),
            children: {},
            pages: []
          }
          
          if index == path_parts.length - 1
            current_level[part][:pages] << page
          else
            current_level = current_level[part][:children]
          end
        end
      end
      
      structure
    end
  end
end

Multi-version Deployment and CDN Optimization

Multi-version documentation requires optimized deployment strategies and CDN configuration to handle version-specific caching and efficient storage.


// GitHub Actions workflow for multi-version docs
name: Deploy Documentation
on:
  push:
    branches: [main, 'v*']
  schedule:
    - cron: '0 0 * * 0'  # Weekly rebuild

jobs:
  deploy-docs:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        version: ['latest', 'v2.0', 'v1.0']
    
    steps:
      - name: Checkout
        uses: actions/checkout@v4
        with:
          ref: ${{ matrix.version == 'latest' && 'main' || matrix.version }}
      
      - name: Setup Ruby
        uses: ruby/setup-ruby@v1
        with:
          ruby-version: '3.1'
      
      - name: Build documentation
        run: |
          bundle install
          bundle exec jekyll build \
            --config _config.yml,_config.${{ matrix.version }}.yml \
            --destination _site/${{ matrix.version }}
      
      - name: Deploy to Cloudflare R2
        uses: cloudflare/wrangler-action@v2
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          command: pages publish _site/${{ matrix.version }} \
            --project-name=docs \
            --branch=${{ matrix.version }}
      
      - name: Update search index
        run: |
          python scripts/generate_search_index.py \
            --version ${{ matrix.version }} \
            --output _site/search-${{ matrix.version }}.json
      
      - name: Upload search index
        uses: cloudflare/wrangler-action@v2
        with:
          apiToken: ${{ secrets.CF_API_TOKEN }}
          command: r2 object put docs/search-${{ matrix.version }}.json \
            --file _site/search-${{ matrix.version }}.json

// Cloudflare Worker for version-aware routing
export default {
  async fetch(request, env, ctx) {
    const url = new URL(request.url);
    
    // Extract version from path or detect
    let version = this.extractVersionFromPath(url);
    if (!version) {
      version = await this.detectPreferredVersion(request);
      return Response.redirect(`/docs/${version}${url.pathname}`);
    }
    
    // Serve from appropriate version bucket
    const objectKey = url.pathname.replace(`/docs/${version}`, '');
    const object = await env.DOCS_BUCKET.get(`${version}${objectKey}`);
    
    if (object) {
      return new Response(object.body, {
        headers: {
          'Content-Type': object.httpMetadata.contentType,
          'Cache-Control': 'public, max-age=3600', // 1 hour
          'X-Docs-Version': version
        }
      });
    }
    
    // Fallback to latest version
    const fallback = await env.DOCS_BUCKET.get(`latest${objectKey}`);
    if (fallback) {
      return new Response(fallback.body, {
        headers: {
          'Content-Type': fallback.httpMetadata.contentType,
          'X-Docs-Version': 'latest'
        }
      });
    }
    
    return new Response('Not found', { status: 404 });
  }
}

This documentation system implementation provides enterprise-grade features while maintaining Jekyll's simplicity and performance. The multi-version architecture supports complex documentation needs, the intelligent search understands technical content, and the API integration ensures accuracy. The system scales to handle large documentation sets with multiple versions while providing excellent user experience through dynamic navigation and contextual help.