Building Intelligent Documentation System with Jekyll and Cloudflare
In This Guide
- Multi-version Documentation Architecture
- Advanced Content Reuse and Transclusion System
- Swagger/OpenAPI Documentation Integration
- Documentation-Specific Search Implementation
- Dynamic Navigation and Contextual Help
- Multi-version Deployment and CDN Optimization
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);
}
}
Dynamic Navigation and Contextual Help
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.
