for-ais-only/tcedocs/SPECIFICATION.md
For Documentation Authors: See
for-ais-only/tcedocs/README.mdfor user-facing documentation on writing examples.
This specification is for developers who need to:
Not covered: Line-by-line code walkthrough, Hugo basics, JavaScript implementation details.
I want to...
The code example system provides a multi-language, tabbed code example interface for the Redis documentation site. It allows documentation authors to embed executable, tested code examples from multiple programming languages in a single, unified interface with language-specific tabs.
Critical Design Principle: All examples are actual test code from client library repositories or local test files. This ensures examples are always valid, executable, and tested against real Redis instances.
Remote Examples (Preferred):
redis-py/doctests/)Local Examples (local_examples/):
Important: Local examples should eventually migrate to client repositories when stable.
┌─────────────────────────────────────────────────────────────────┐
│ Build Process │
│ │
│ ┌──────────────────┐ ┌──────────────────┐ │
│ │ Remote Examples │ │ Local Examples │ │
│ │ (GitHub Repos) │ │ (local_examples/)│ │
│ └────────┬─────────┘ └────────┬─────────┘ │
│ │ │ │
│ ▼ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ build/make.py (Orchestrator) │ │
│ │ - Calls component.py for remote examples │ │
│ │ - Calls local_examples.py for local │ │
│ └────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Example Processing (example.py) │ │
│ │ - Parse special comments │ │
│ │ - Extract steps, hide/remove blocks │ │
│ │ - Generate metadata │ │
│ └────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Output │ │
│ │ - examples/ (processed code files) │ │
│ │ - data/examples.json (metadata) │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ Hugo Rendering │
│ │
│ ┌────────────────────────────────────────────┐ │
│ │ Documentation Pages (Markdown) │ │
│ │ {{< clients-example set="..." />}} │ │
│ └────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Shortcode (clients-example.html) │ │
│ │ - Parse parameters │ │
│ │ - Call partial template │ │
│ └────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ Partial (tabbed-clients-example.html) │ │
│ │ - Load examples.json │ │
│ │ - Generate tabs for each language │ │
│ │ - Apply syntax highlighting │ │
│ └────────────────┬───────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌────────────────────────────────────────────┐ │
│ │ HTML Output (Interactive Tabs) │ │
│ └────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────┘
The system operates in three distinct phases:
1. Build Time (Python) - Processes example source code:
EXAMPLE:, HIDE_START, etc.)examples/ directorydata/examples.json with metadata for all examples2. Hugo Build Time (Go Templates) - Renders HTML:
data/examples.json metadata{{< clients-example >}} shortcodes in Markdown filesexamples/ directory3. Browser Runtime (JavaScript) - Handles interactivity:
Key Insight: The Python build phase does the heavy lifting (parsing, processing), while Hugo simply renders pre-processed files. This separation allows Hugo to remain fast even with hundreds of examples.
Metadata Merging:
examples.json"Python", "Node.js")Generated Files (gitignored):
examples/ directory - processed code filesdata/examples.json - metadata for all examplesIn-Place Processing:
Example class modifies files in-place after copying to examples/local_examples/) remain unchangedNote: This section provides technical details about each component. For practical usage, see Working with Examples.
build/make.pyPurpose: Main orchestrator for the build process
Responsibilities:
Key Functions:
parse_args(): Parse command-line argumentsAll.apply() and process_local_examples()Inputs:
--stack: Path to stack definition (default: ./data/components/index.json)--skip-clone: Skip git clone operations--loglevel: Python logging level--tempdir: Temporary directory for cloning repositoriesOutputs:
examples/ directorydata/examples.json metadata filebuild/local_examples.pyPurpose: Process local example files from the local_examples/ directory
Responsibilities:
local_examples/ directory treeExample classexamples.jsonKey Functions:
process_local_examples(): Main processing functionget_language_from_extension(): Map file extensions to languagesget_client_name_from_language_and_path(): Determine client name with path-based overridesget_example_id_from_file(): Extract example ID from first linePath-Based Client Name Overrides:
Some languages have multiple client implementations (sync/async, different libraries). The system uses directory path to determine which variant:
lettuce-sync/ → Lettuce-Sync (Lettuce synchronous client)lettuce-async/ → Java-Async (Lettuce asynchronous client)lettuce-reactive/ → Java-Reactive (Lettuce reactive client)Java-Sync (Jedis synchronous client)rust-async/ → Rust-Asyncrust-sync/ → Rust-Syncasync/ → C#-Asyncsync/ → C#-SyncThis allows the same language to appear multiple times in the tab interface with different implementations. The order of checks matters: more specific paths (e.g., lettuce-sync) should be checked before generic ones (e.g., Java-Sync).
Outputs:
examples/{example_id}/local_{filename}data/examples.json with metadatabuild/components/component.pyPurpose: Handle remote example processing from GitHub repositories
Key Classes:
Component: Base class for all components
All: Main component orchestrator
data/components/index.jsondata/examples.jsonClient: Client library component handler
Example classKey Methods:
_git_clone(): Clone repositories from GitHub_copy_examples(): Extract and process examples from repositories_get_example_id_from_file(): Extract example ID from file header_get_default_branch(): Query GitHub API for default branch nameGitHub Integration:
build/components/example.pyPurpose: Parse and process individual example files
Special Comment Markers:
EXAMPLE: {id}: Defines the example identifier (required, must be first line)BINDER_ID {hash}: Defines the BinderHub commit hash for interactive notebook link (optional)HIDE_START / HIDE_END: Code blocks hidden by default (revealed with eye button)REMOVE_START / REMOVE_END: Code blocks completely removed from displaySTEP_START {name} / STEP_END: Named code blocks for step-by-step examplesBINDER_ID Marker:
The BINDER_ID marker provides a commit hash for BinderHub integration, allowing users to run examples in an interactive Jupyter notebook environment.
Syntax:
# EXAMPLE: example_id
# BINDER_ID 6bbed3da294e8de5a8c2ad99abf883731a50d4dd
Requirements:
EXAMPLE: marker (typically on line 2)# for Python, // for JavaScript)BINDER_ID per example fileUsage: The hash is used to construct a BinderHub URL like:
https://redis.io/binder/v2/gh/redis/binder-launchers/{hash}?urlpath=%2Fdoc%2Ftree%2Fdemo.ipynb
This allows documentation to include "Try this in Jupyter" links that launch interactive notebook environments with the example pre-loaded.
Processing Algorithm:
EXAMPLE: marker (line 1)BINDER_ID marker if present (typically line 2)STEP_START/STEP_ENDBINDER_ID Extraction Details:
The BINDER_ID marker allows example authors to specify a Git reference (branch name or commit SHA) from the redis/binder-launchers repository. This enables the Hugo templates to generate "Run this example in the browser" links that open the example in an interactive Jupyter notebook environment via BinderHub.
Quick Implementation Checklist:
BINDER_ID = 'BINDER_ID' (around line 11 in example.py)binder_id = None (around line 49 in Example class)binder = re.compile(...) (around line 94 in make_ranges())elif chain (around line 157 in main loop)build/local_examples.py (around line 183)build/components/component.py (around line 278)BINDER_ID line removed from processed outputbinderId appears in data/examples.jsonThe parser should implement the following logic in build/components/example.py:
1. Add Constant and Class Attribute:
First, add the constant at the top of the file with other marker constants:
BINDER_ID = 'BINDER_ID'
Add the attribute to the Example class:
class Example(object):
language = None
path = None
content = None
hidden = None
highlight = None
named_steps = None
binder_id = None # Add this
Initialize in __init__:
self.binder_id = None
2. Compile Regex Pattern:
In the make_ranges() method (around line 94), add the regex pattern compilation alongside other patterns (after exid pattern):
exid = re.compile(f'{PREFIXES[self.language]}\\s?{EXAMPLE}')
binder = re.compile(f'{PREFIXES[self.language]}\\s?{BINDER_ID}\\s+([a-zA-Z0-9_-]+)')
go_output = re.compile(f'{PREFIXES[self.language]}\\s?{GO_OUTPUT}')
Exact location: In build/components/example.py, class Example, method make_ranges(), in the section where regex patterns are compiled (after line 93).
Pattern explanation:
{PREFIXES[self.language]} - Language-specific comment prefix (e.g., # or //)\\s? - Optional whitespace after comment prefix{BINDER_ID} - The literal string "BINDER_ID"\\s+ - Required whitespace before identifier([a-zA-Z0-9_-]+) - Capture group for Git reference (commit SHA or branch name)
6bbed3da294e8de5a8c2ad99abf883731a50d4dd (commit SHA), python-landing (branch name), main, feature-123Why this pattern works:
([a-f0-9]{40}) only matched commit SHAs. The new pattern ([a-zA-Z0-9_-]+) matches commit SHAs (which are valid under the new pattern) AND branch names.3. Detection and Extraction:
Add detection logic in the main processing loop (around line 157), after the EXAMPLE: check and before the GO_OUTPUT check:
elif re.search(exid, l):
output = False
pass
elif re.search(binder, l):
# Extract BINDER_ID value (commit SHA or branch name)
match = re.search(binder, l)
if match:
self.binder_id = match.group(1)
logging.debug(f'Found BINDER_ID: {self.binder_id} in {self.path}:L{curr+1}')
output = False # CRITICAL: Skip this line from output
elif self.language == "go" and re.search(go_output, l):
# ... rest of processing
Exact location: In build/components/example.py, class Example, method make_ranges(), in the main while curr < len(self.content): loop, in the elif chain that handles special markers.
Critical implementation details:
output = False: This prevents the line from being added to the content arrayelif chain, not a separate if statementcontent.append(l): The line is skipped entirely, just like EXAMPLE: linesexid (EXAMPLE:) but before go_output to maintain proper precedence4. Storage in Metadata:
In build/local_examples.py, add the binderId field conditionally after creating the metadata dictionary:
example_metadata = {
'source': source_file,
'language': language,
'target': target_file,
'highlight': example.highlight,
'hidden': example.hidden,
'named_steps': example.named_steps,
'sourceUrl': None
}
# Add binderId only if it exists
if example.binder_id:
example_metadata['binderId'] = example.binder_id
examples_data[example_id][client_name] = example_metadata
In build/components/component.py, add similarly after setting other metadata fields:
example_metadata['highlight'] = e.highlight
example_metadata['hidden'] = e.hidden
example_metadata['named_steps'] = e.named_steps
example_metadata['sourceUrl'] = (
f'{ex["git_uri"]}/tree/{default_branch}/{ex["path"]}/{os.path.basename(f)}'
)
# Add binderId only if it exists
if e.binder_id:
example_metadata['binderId'] = e.binder_id
examples = self._root._examples
Why conditional addition:
binder_id is not Nonenull or empty string values in the metadata5. Line Processing Behavior:
The BINDER_ID line is removed from output through the same mechanism as other marker lines:
output = False prevents the line from reaching the else block that calls content.append(l)content, it doesn't affect line number calculations for steps, highlights, or hidden rangesCommon Pitfalls:
output = False: The line will appear in processed outputif instead of elif: Could cause multiple conditions to matchif match: Could cause AttributeError if regex doesn't match"binderId": null in JSON for examples without the marker[a-f0-9]{40} only matches commit SHAs, not branch names.* or .+ could match invalid characters or whitespacematch.group(0) returns the entire match including comment prefix, not just the value6. Complete Example Flow:
Here's a complete example showing how a file is processed:
Input file (local_examples/client-specific/redis-py/landing.py):
# EXAMPLE: landing
# BINDER_ID python-landing
import redis
# STEP_START connect
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# STEP_END
Processing steps:
EXAMPLE: detected → output = False → line skippedBINDER_ID detected → extract value python-landing → output = False → line skippedimport redis → no marker → added to content array at index 0content array at index 1STEP_START detected → record step start at line 3 (len(content) + 1) → line skippedcontent array at index 2STEP_END detected → record step range "3-3" → line skippedOutput file (examples/landing/local_client-specific_redis-py_landing.py):
import redis
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
Metadata (data/examples.json):
{
"landing": {
"Python": {
"source": "local_examples/client-specific/redis-py/landing.py",
"language": "python",
"target": "examples/landing/local_client-specific_redis-py_landing.py",
"highlight": ["1-3"],
"hidden": [],
"named_steps": {
"connect": "3-3"
},
"sourceUrl": null,
"binderId": "python-landing"
}
}
}
Key observations:
EXAMPLE: and BINDER_ID lines are removed from outputbinderId is stored at the language level, not the example set levelOutput Metadata (stored in examples.json):
highlight: Line ranges to highlight (e.g., ["1-10", "15-20"])hidden: Line ranges initially hidden (e.g., ["5-8"])named_steps: Map of step names to line ranges (e.g., {"connect": "1-5"})binderId: BinderHub commit hash (optional, e.g., "6bbed3da294e8de5a8c2ad99abf883731a50d4dd")Note: For language-specific configuration (comment prefixes, test markers), see Appendix: Language Mappings.
layouts/shortcodes/clients-example.htmlPurpose: Hugo shortcode for embedding code examples in Markdown
Parameters (Named):
set: Example set name (required) - matches the EXAMPLE: IDstep: Example step name (optional) - references a STEP_START blocklang_filter: Language filter (optional) - show only specific languagesmax_lines: Maximum lines shown by default (optional, default: 100)dft_tab_name: Custom first tab name (optional, default: ">_ Redis CLI")dft_tab_link_title: Custom first tab footer link title (optional)dft_tab_url: Custom first tab footer link URL (optional)show_footer: Show footer (optional, default: true)Parameters (Positional - for backward compatibility):
Functionality:
tabbed-clients-example.html partiallayouts/partials/tabbed-clients-example.htmlPurpose: Generate the tabbed interface HTML
Responsibilities:
data/examples.jsonconfig.toml)highlight functionData Sources:
$.Site.Data.examples: Loaded from data/examples.json$.Site.Params.clientsexamples: Language order from config.toml$.Site.Params.clientsconfig: Client configuration from config.tomlTab Generation Logic:
examples.jsontarget pathtabs/wrapper.html partiallayouts/partials/tabs/wrapper.htmlPurpose: Render the interactive tabbed interface HTML
Features:
JavaScript Integration:
The interactive features are implemented in JavaScript (location varies by theme):
toggleVisibleLinesForCodetabs(): Toggle hidden code visibilitycopyCodeToClipboardForCodetabs(): Copy code to clipboardNote: JavaScript implementation details are theme-specific and not covered in this specification.
Purpose: Provide interactive Jupyter notebook environment for running examples
Feature Description:
The code example boxes can display a "Run this example in the browser" link that launches the example in a BinderHub-powered Jupyter notebook environment. This link appears in the top bar of the example box, next to the three-dot menu icon.
Conditional Display:
binderId value in its metadatabinderId exists, the link is not rendered (no placeholder, no broken link)binderId is language-specific, so different languages in the same example set may have different BinderHub linksLink URL Format:
https://redis.io/binder/v2/gh/redis/binder-launchers/<binderId>?urlpath=%2Fdoc%2Ftree%2Fdemo.ipynb
URL Components:
https://redis.io/binder/v2/gh/redis/binder-launchers/binderId field (commit SHA or branch name)
6bbed3da294e8de5a8c2ad99abf883731a50d4dd)python-landing, main, feature-123)?urlpath=%2Fdoc%2Ftree%2Fdemo.ipynb (constant, URL-encoded path to notebook)demo.ipynb - do NOT change per exampleExamples:
# Using branch name
https://redis.io/binder/v2/gh/redis/binder-launchers/python-landing?urlpath=%2Fdoc%2Ftree%2Fdemo.ipynb
# Using commit SHA
https://redis.io/binder/v2/gh/redis/binder-launchers/6bbed3da294e8de5a8c2ad99abf883731a50d4dd?urlpath=%2Fdoc%2Ftree%2Fdemo.ipynb
Implementation in Hugo Templates:
The implementation spans two template files:
1. Extract and pass binderId in layouts/partials/tabbed-clients-example.html:
In the loop that builds tabs for each language, extract the binderId and include it in the tab dictionary:
{{ $clientExamples := index $.Site.Data.examples $id }}
{{ range $client := $.Site.Params.clientsexamples }}
{{ $example := index $clientExamples $client }}
{{ $clientConfig := index $.Site.Params.clientsconfig $client }}
{{ $language := index $example "language" }}
{{ $quickstartSlug := index $clientConfig "quickstartSlug" }}
{{ if and ($example) (or (eq $lang "") (strings.Contains $lang $client)) }}
{{ $examplePath := index $example "target" }}
{{ $options := printf "linenos=false" }}
{}
{{ if hasPrefix $language "java" }}{{ $language = "java"}}{{ end }}
{{ $params := dict "language" $language "contentPath" $examplePath "options" $options }}
{{ $content := partial "tabs/source.html" $params }}
{}
{{ $binderId := index $example "binderId" }}
{{ $tabs = $tabs | append (dict "title" $client "language" $client "quickstartSlug" $quickstartSlug "content" $content "sourceUrl" (index $example "sourceUrl") "binderId" $binderId) }}
{{ end }}
{{ end }}
Key points:
binderId using index $example "binderId"binderId doesn't exist, it will be nil (which is fine - handled later)2. Add link container in layouts/partials/tabs/wrapper.html top bar:
Insert the BinderHub link container between the language selector and the control buttons:
<!-- Language selector dropdown with controls -->
<div class="codetabs-header flex items-center justify-between px-4 py-2 bg-slate-900 rounded-t-lg">
<div class="flex items-center flex-1">
<label for="lang-select-{{ $id }}" class="text-xs text-slate-400 mr-3 whitespace-nowrap">Language:</label>
<select id="lang-select-{{ $id }}"
class="lang-selector max-w-xs px-3 py-2 text-sm bg-slate-700 text-white border border-slate-600 rounded-md cursor-pointer
hover:bg-slate-600 focus:outline-none
transition duration-150 ease-in-out appearance-none"
data-codetabs-id="{{ $id }}">
{}
</select>
</div>
{}
<div id="binder-link-container-{{ $id }}" class="flex items-center ml-4">
{}
</div>
<div class="flex items-center gap-2 ml-2">
{}
{}
</div>
</div>
Placement notes:
flex-1 div)ml-4 adds left margin to separate from language selectorml-2 on buttons div adds small gap between link and buttons3. Add binderId data attribute to tab panels:
In the tab panels loop, add the data-binder-id attribute if binderId exists:
<!-- Tab panels -->
{{ range $i, $tab := $tabs }}
{{ $tid := printf "%s_%s" (replace (replace (index $tab "title") "#" "sharp") "." "") $id }}
{{ $pid := printf "panel_%s" $tid }}
{{ $dataLang := replace (or (index $tab "language") "redis-cli") "C#" "dotnet" }}
{{ $dataLang := replace $dataLang "." "-" }}
{{ $binderId := index $tab "binderId" }}
<div class="panel {{ if ne $i 0 }}panel-hidden{{ end }} w-full mt-0 {{ if not $showFooter}}pb-8{{end}}"
id="{{ $pid }}"
data-lang="{{ $dataLang }}"
{{ if $binderId }}data-binder-id="{{ $binderId }}"{{ end }}
data-codetabs-id="{{ $id }}"
role="tabpanel"
tabindex="0"
aria-labelledby="lang-select-{{ $id }}">
{}
</div>
{{ end }}
Key points:
binderId from tab datadata-binder-id attribute if binderId exists (conditional)data-codetabs-id to match panels to their container4. Add JavaScript to handle link display and updates:
Add this script at the end of layouts/partials/tabs/wrapper.html (after the closing </div> of the codetabs container):
<script>
(function() {
// Initialize BinderHub link for this codetabs instance
const codetabsId = '{{ $id }}';
const container = document.getElementById('binder-link-container-' + codetabsId);
const langSelect = document.getElementById('lang-select-' + codetabsId);
function updateBinderLink() {
if (!container || !langSelect) return;
// Get the currently selected tab index
const selectedOption = langSelect.options[langSelect.selectedIndex];
const tabIndex = parseInt(selectedOption.getAttribute('data-index'));
// Find the corresponding panel
const panels = document.querySelectorAll('[data-codetabs-id="' + codetabsId + '"].panel');
if (!panels || tabIndex >= panels.length) return;
const currentPanel = panels[tabIndex];
const binderId = currentPanel.getAttribute('data-binder-id');
// Clear existing content
container.innerHTML = '';
// If binderId exists, create and show the link
if (binderId) {
const binderUrl = 'https://redis.io/binder/v2/gh/redis/binder-launchers/' +
binderId +
'?urlpath=%2Fdoc%2Ftree%2Fdemo.ipynb';
const link = document.createElement('a');
link.href = binderUrl;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.className = 'text-xs text-slate-300 hover:text-white hover:underline whitespace-nowrap flex items-center gap-1';
link.title = 'Run this example in an interactive Jupyter notebook';
// Add Binder icon (play icon)
link.innerHTML = `
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z"/>
</svg>
<span>Run in browser</span>
`;
container.appendChild(link);
}
}
// Initialize on page load
updateBinderLink();
// Update when language changes (in addition to existing language change handler)
if (langSelect) {
langSelect.addEventListener('change', updateBinderLink);
}
})();
</script>
JavaScript implementation details:
Function: updateBinderLink()
Step-by-step logic:
data-index attribute from selected optiondata-codetabs-iddata-binder-id attribute from current panelbinderId exists: Create link element with proper URL and append to containerbinderId is null/undefined: Container remains empty (no link shown)URL construction:
const binderUrl = 'https://redis.io/binder/v2/gh/redis/binder-launchers/' +
binderId +
'?urlpath=%2Fdoc%2Ftree%2Fdemo.ipynb';
%2F is the URL-encoded form of //doc/tree/demo.ipynbLink element properties:
target="_blank": Opens in new tabrel="noopener noreferrer": Security best practice for external linksclassName: Tailwind CSS classes for styling (small text, hover effects, flex layout)title: Tooltip text for accessibilityinnerHTML: SVG play icon + text labelEvent handling:
updateBinderLink()<select> element calls updateBinderLink()Why JavaScript instead of Hugo template:
binderId valuesData Flow:
BINDER_ID from source files → store in data/examples.jsonbinderId from $.Site.Data.examples[exampleSet][language].binderIdbinderId through tab data structuredata-binder-id attribute to panel HTML{{ $id }}codetabsIddata-binder-id from currently visible panelbinderId existsupdateBinderLink()data-binder-idImportant Notes:
binderIddemo.ipynb - the BinderHub launcher repository handles routing to the correct example?urlpath=%2Fdoc%2Ftree%2Fdemo.ipynb part is URL-encoded (%2F = /)redis/binder-launchers repository to be properly configured with the commit referenced by binderIdImplementation Gotchas and Edge Cases:
Empty container initialization:
<div id="binder-link-container-{{ $id }}"> is intentionally empty in the Hugo templateContainer clearing is critical:
container.innerHTML = '' must be called before checking binderIdbinderId to one without, the old link must be removedPlay icon instead of text-only:
<path d="M8 5v14l11-7z"/>)Importance of data-codetabs-id attribute:
data-codetabs-id="{{ $id }}" attributeIIFE (Immediately Invoked Function Expression):
(function() { ... })();codetabsId, container, langSelect, updateBinderLink are scoped to this instanceEvent listener doesn't replace existing handlers:
addEventListener('change', updateBinderLink) instead of onchange=Null/undefined binderId handling:
if (binderId) check handles both null and undefineddata-binder-id attribute (no binderId in metadata)getAttribute() returns null if attribute doesn't existCSS classes for responsive design:
text-xs: Small text size to fit in top barwhitespace-nowrap: Prevents text wrapping on narrow screensflex items-center gap-1: Aligns icon and text horizontally with small gaphover:text-white hover:underline: Visual feedback on hoverTiming of script execution:
DOMContentLoaded event, but not necessary hereHugo template variable in JavaScript:
const codetabsId = '{{ $id }}'; embeds Hugo variable in JavaScriptconst codetabsId = 'landing-stepconnect';Relationship to Manual Links:
Some documentation pages may have manual BinderHub links in the markdown content (e.g., "You can try this code out in a Jupyter notebook on Binder"). The automated link in the example box serves the same purpose but is:
Common Pitfall: Global Synchronization Across Multiple Codetabs Instances:
When a page contains multiple code example boxes (codetabs instances), a critical implementation detail can cause bugs: each instance must update independently, but all instances must stay synchronized when the user changes the language selector.
The Problem:
If each codetabs instance has its own independent updateBinderLink() function, and you only call that function when its own language selector changes, then:
Why this happens:
The codetabs.js library has a switchCodeTab() function that synchronizes all language selector dropdowns on the page when one is changed. However, it updates the dropdowns without triggering change events on them. This means:
Solution: Implement a global updateAllBinderLinks() function that updates ALL binder links on the page:
// Global function to update all binder links on the page
window.updateAllBinderLinks = window.updateAllBinderLinks || function() {
// Find all binder link containers
const containers = document.querySelectorAll('[id^="binder-link-container-"]');
containers.forEach((container) => {
// Extract the codetabs ID from the container ID
const codetabsId = container.id.replace('binder-link-container-', '');
const langSelect = document.getElementById('lang-select-' + codetabsId);
if (!langSelect) return;
// Get the currently selected tab index
const selectedOption = langSelect.options[langSelect.selectedIndex];
const tabIndex = parseInt(selectedOption.getAttribute('data-index'));
// Find the corresponding panel
const panels = document.querySelectorAll('[data-codetabs-id="' + codetabsId + '"].panel');
if (!panels || tabIndex >= panels.length) return;
const currentPanel = panels[tabIndex];
const binderId = currentPanel.getAttribute('data-binder-id');
// Clear existing content
container.innerHTML = '';
// Only show the link if the CURRENTLY SELECTED tab has a binderId
if (binderId) {
const binderUrl = 'https://redis.io/binder/v2/gh/redis/binder-launchers/' +
binderId +
'?urlpath=%2Fdoc%2Ftree%2Fdemo.ipynb';
const link = document.createElement('a');
link.href = binderUrl;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.className = 'text-xs text-slate-300 hover:text-white hover:underline whitespace-nowrap flex items-center gap-1';
link.title = 'Run this example in an interactive Jupyter notebook';
link.innerHTML = `
<svg class="w-4 h-4" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="currentColor">
<path d="M8 5v14l11-7z"/>
</svg>
<span>Run in browser</span>
`;
container.appendChild(link);
}
});
};
// Initialize on page load with a delay to allow codetabs.js to restore localStorage selection
setTimeout(() => {
window.updateAllBinderLinks();
}, 100);
// Update all binder links when ANY language selector changes
// Use a small delay to allow codetabs.js to synchronize all dropdowns first
document.querySelectorAll('.lang-selector').forEach((selector) => {
selector.addEventListener('change', () => {
setTimeout(window.updateAllBinderLinks, 10);
});
});
Key implementation details:
window object, not per-instancequerySelectorAll('[id^="binder-link-container-"]') to find every binder link containersetTimeout(..., 100) to allow codetabs.js to restore localStorage selection firstsetTimeout(..., 10) to allow codetabs.js to synchronize all dropdowns first.lang-selector dropdown, not just one per instanceWhy the delays matter:
codetabs.js is deferred and runs after DOM is ready. It restores the user's language preference from localStorage. Without the delay, our function runs before that restoration completes.codetabs.js has a switchCodeTab() function that updates all dropdowns. Without the delay, our function might run before all dropdowns are synchronized.This ensures:
layouts/partials/tabs/source.htmlPurpose: Read and highlight source code files
Functionality:
readFile to load example file from examples/ directoryUnderstanding the directory structure in context of the workflow:
docs/
├── build/ # Build scripts (Python)
│ ├── make.py # Main orchestrator - run this to process examples
│ ├── local_examples.py # Local example processor
│ ├── components/ # Processing logic
│ │ ├── component.py # Remote example processor
│ │ ├── example.py # Core parser - handles special comments
│ │ ├── util.py # Utility functions
│ │ └── structured_data.py # JSON/YAML/TOML handling
│ └── tcedocs/
│ └── README.md # User-facing documentation
│
├── local_examples/ # SOURCE: Local example files (committed)
│ ├── client-specific/ # Organized by client
│ │ ├── redis-py/ # Python examples
│ │ ├── nodejs/ # Node.js examples
│ │ └── ...
│ ├── cmds_generic/ # Organized by command type
│ └── cmds_hash/
│
├── examples/ # OUTPUT: Processed files (gitignored, generated)
│ └── {example_id}/ # One directory per example ID
│ ├── {client}_{filename} # Remote example (from GitHub)
│ └── local_{filename} # Local example (from local_examples/)
│
├── data/
│ ├── components/ # CONFIG: Component definitions (committed)
│ │ ├── index.json # Registry of all components
│ │ ├── redis_py.json # Python client config
│ │ ├── node_redis.json # Node.js client config
│ │ └── ...
│ └── examples.json # OUTPUT: Metadata (gitignored, generated)
│
├── layouts/ # TEMPLATES: Hugo rendering (committed)
│ ├── shortcodes/
│ │ └── clients-example.html # Shortcode entry point
│ └── partials/
│ ├── tabbed-clients-example.html # Main rendering logic
│ └── tabs/
│ ├── wrapper.html # Tab interface HTML
│ └── source.html # Source code loader
│
├── content/ # CONTENT: Documentation pages (committed)
│ └── develop/
│ ├── clients/ # Client documentation
│ │ ├── redis-py/
│ │ │ └── connect.md # Uses {{< clients-example >}}
│ │ └── ...
│ └── data-types/ # Data type documentation
│ └── hashes.md # Uses {{< clients-example >}}
│
└── config.toml # CONFIG: Hugo configuration (committed)
Key Directories:
local_examples/, data/components/, layouts/, content/, config.tomlexamples/, data/examples.json, public/build/ (committed, but outputs are generated)Example Source Files:
# EXAMPLE: {example_id} (or // for other languages)Processed Example Files:
{client_id}_{original_filename}
redis_py_home_vecsets.pylocal_{subdir}_{filename} or local_{filename}
local_client-specific_redis-py_home_vecsets.pyComponent Configuration Files:
data/components/{client_id}.jsondata/components/index.json# EXAMPLE: example_id
# STEP_START step_name
# REMOVE_START
import test_framework # This line will be removed
# REMOVE_END
# HIDE_START
# This code is hidden by default
setup_code()
# HIDE_END
# Visible code
def main():
# This is always visible
pass
# STEP_END
config.toml)Client Examples Order:
[params]
clientsExamples = ["Python", "Node.js", "Java-Sync", "Lettuce-Sync", "Java-Async", "Java-Reactive", "Go", "C", "C#-Sync", "C#-Async", "RedisVL", "PHP", "Rust-Sync", "Rust-Async"]
This controls:
Client Configuration:
[params.clientsConfig]
"Python"={quickstartSlug="redis-py"}
"Node.js"={quickstartSlug="nodejs"}
"Java-sync"={quickstartSlug="jedis"}
...
This maps:
data/components/{client}.json)Example for Python (redis_py.json):
{
"id": "redis_py",
"type": "client",
"name": "redis-py",
"language": "Python",
"label": "Python",
"repository": {
"git_uri": "https://github.com/redis/redis-py"
},
"examples": {
"git_uri": "https://github.com/redis/redis-py",
"path": "doctests",
"pattern": "*.py"
}
}
Fields:
id: Unique identifier for the componenttype: Component type (usually "client")name: Display namelanguage: Language name (must match config.toml)label: Tab label (usually same as language, except RedisVL)repository.git_uri: GitHub repository URLexamples.git_uri: Repository containing examplesexamples.path: Path within repository to search for examplesexamples.pattern: Glob pattern for example filesdata/components/index.json){
"id": "index",
"clients": [
"nredisstack_sync",
"nredisstack_async",
"go_redis",
"node_redis",
"php",
"redis_py",
...
],
"website": {
"path": "./",
"content": "content/",
"examples": "data/examples.json",
"examples_path": "examples"
}
}
Purpose: Registry of all components to process during build
Common Tasks:
# Full build (first time or after major changes)
make all
# Rebuild examples only (after changing example code)
python3 build/make.py
# Rebuild local examples only (fastest iteration)
python3 build/local_examples.py
# Serve docs locally (auto-reloads on content changes)
hugo serve
# Check if example was processed
grep "example_id" data/examples.json
# View processed example file
cat examples/example_id/processed_file.py
Example Naming Conventions:
hash_basic, json_query, vector_searchdt_hash_basic (data type), cmd_set (command)set_and_get not set-and-getconn_pool not connection_pooling_exampleWhat Makes a Good Example:
BINDER_ID for "Run in browser" functionality via Jupyter notebooks1. Write the Example Code:
Create a test file in the appropriate client library repository (or local_examples/ for quick iteration):
# EXAMPLE: my_new_example
# REMOVE_START
import redis
import pytest
# REMOVE_END
# STEP_START connect
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# STEP_END
# STEP_START set_value
r.set('mykey', 'myvalue')
# STEP_END
# STEP_START get_value
value = r.get('mykey')
print(value) # Output: myvalue
# STEP_END
2. Test the Example Locally:
Before committing, ensure the example works:
# For Python examples
cd /path/to/redis-py
python -m pytest doctests/my_new_example.py -v
# For Node.js examples
cd /path/to/node-redis
npm test -- doctests/my_new_example.js
# For local examples (create a simple test runner)
cd /path/to/docs
python3 local_examples/client-specific/redis-py/my_new_example.py
3. Add to Documentation:
Reference the example in a Markdown file:
Connect to Redis:
{{< clients-example set="my_new_example" step="connect" />}}
Set and retrieve a value:
{{< clients-example set="my_new_example" step="set_value" />}}
{{< clients-example set="my_new_example" step="get_value" />}}
4. Build and Verify:
# Process examples
python3 build/make.py
# Verify example appears in examples.json
cat data/examples.json | grep my_new_example
# Build and serve
hugo serve
5. Add BinderHub Support (Optional):
If you want to enable the "Run in browser" link for an example:
Step 1: Create or update the BinderHub launcher:
The redis/binder-launchers repository contains Jupyter notebooks for each example. Jupyter notebooks can run code in multiple languages (Python, Node.js, Java, etc.) through language kernels. You need to:
demo.ipynb) that runs your example in the appropriate languageredis/binder-launchers repositorypython-landing, main, or feature-xyzStep 2: Add BINDER_ID to your example:
Add the BINDER_ID marker as the second line of your example file (after EXAMPLE:).
Option A: Using a branch name (recommended for active development):
# EXAMPLE: my_new_example
# BINDER_ID python-landing
import redis
# STEP_START connect
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# STEP_END
Option B: Using a commit SHA (recommended for stable examples):
# EXAMPLE: my_new_example
# BINDER_ID 6bbed3da294e8de5a8c2ad99abf883731a50d4dd
import redis
# STEP_START connect
r = redis.Redis(host='localhost', port=6379, decode_responses=True)
# STEP_END
Choosing between branch name and commit SHA:
| Aspect | Branch Name | Commit SHA |
|---|---|---|
| Updates | Automatically uses latest commit on branch | Fixed to specific commit |
| Stability | May change if branch is updated | Immutable, always same version |
| Maintenance | Easy - just push to branch | Requires updating BINDER_ID after each change |
| Use case | Active development, frequently updated examples | Stable, production examples |
| Example | python-landing, main, dev | 6bbed3da294e8de5a8c2ad99abf883731a50d4dd |
Recommendation: Use branch names during development for easier iteration, then switch to commit SHAs when the example is stable and ready for production.
Step 3: Rebuild and verify:
# Process examples
python3 build/local_examples.py
# Verify binderId appears in metadata
python3 -c "import json; data = json.load(open('data/examples.json')); print(data['my_new_example']['Python'].get('binderId'))"
# Should output: python-landing (or your commit SHA)
# Verify BINDER_ID line is removed from processed file
cat examples/my_new_example/local_*.py | grep BINDER_ID
# Should output nothing (line removed)
# Build Hugo and check the page
hugo serve
# Navigate to the page and verify "Run this example in the browser" link appears
Step 4: Test both formats (recommended during development):
To ensure the regex pattern works correctly with both branch names and commit SHAs, create temporary test files:
# Test 1: Branch name
cat > local_examples/test_branch.py << 'EOF'
# EXAMPLE: test_branch
# BINDER_ID main
import redis
r = redis.Redis()
EOF
# Test 2: Commit SHA
cat > local_examples/test_sha.py << 'EOF'
# EXAMPLE: test_sha
# BINDER_ID 6bbed3da294e8de5a8c2ad99abf883731a50d4dd
import redis
r = redis.Redis()
EOF
# Process and verify both
python3 build/local_examples.py
# Check branch name extraction
python3 -c "import json; data = json.load(open('data/examples.json')); print('Branch:', data['test_branch']['Python'].get('binderId'))"
# Expected output: Branch: main
# Check commit SHA extraction
python3 -c "import json; data = json.load(open('data/examples.json')); print('SHA:', data['test_sha']['Python'].get('binderId'))"
# Expected output: SHA: 6bbed3da294e8de5a8c2ad99abf883731a50d4dd
# Verify both lines removed from processed files
grep BINDER_ID examples/test_branch/local_test_branch.py
grep BINDER_ID examples/test_sha/local_test_sha.py
# Both should output nothing
# Clean up test files
rm local_examples/test_branch.py local_examples/test_sha.py
python3 build/local_examples.py # Rebuild to remove from metadata
Important notes:
redis/binder-launchers repositorydemo.ipynb (hardcoded in the URL)binderId exists in the metadataBINDER_ID value whenever you want to point to a different version of the notebookFull rebuild required (make all or python3 build/make.py):
Hugo rebuild only (hugo serve auto-reloads):
No rebuild needed:
Example not appearing:
data/examples.json - is your example ID present?EXAMPLE: header matches the ID you're usingconfig.toml clientsExamplesWrong code displayed:
target path in examples.jsonexamples/{example_id}/HIDE_START or REMOVE_START markersPREFIXESHighlighting issues:
STEP_START/STEP_END markers are properly closedexamples.json for correct line rangesThe CLI Command Extraction feature automatically extracts Redis CLI commands from code examples and enriches them with metadata from data/commands_core.json. This metadata is exposed to AI agents and documentation systems to better understand what each code example demonstrates.
Why this matters:
┌─────────────────────────────────────────────────────────────────┐
│ CLI Command Extraction │
│ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 1. Parse CLI Content from {{< clients-example >}} blocks │ │
│ │ - Extract lines starting with ">" │ │
│ │ - Identify command names (first token after ">") │ │
│ │ - Handle multi-word commands (e.g., "ACL CAT") │ │
│ │ - Handle dot notation (e.g., "JSON.SET") │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 2. Normalize Command Names │ │
│ │ - Convert to uppercase │ │
│ │ - Handle aliases and variations │ │
│ │ - Deduplicate within same snippet │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 3. Enrich with Command Metadata │ │
│ │ - Look up in data/commands_core.json │ │
│ │ - Extract: summary, group, complexity, since │ │
│ │ - Generate command reference link │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 4. Store in Example Metadata │ │
│ │ - Add "cli_commands" field to examples.json │ │
│ │ - Structure: array of command objects │ │
│ │ - Include in both remote and local examples │ │
│ └──────────────────────────────────────────────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────────────────────────────────┐ │
│ │ 5. Expose to Templates and AI Systems │ │
│ │ - Available in Hugo templates via examples.json │ │
│ │ - Available to AI agents via metadata │ │
│ │ - Can be rendered in UI or used for search │ │
│ └──────────────────────────────────────────────────────────┘ │
│ │
└─────────────────────────────────────────────────────────────────┘
Location: New module build/components/cli_parser.py
Responsibilities:
{{< clients-example >}} shortcode blocks> or redis>ACL CAT, SCRIPT LOAD)JSON.SET, GRAPH.QUERY)Algorithm:
For each line in CLI content:
1. Check if line starts with ">" or "redis>" (redis-cli prompt)
2. Extract text after the prompt
3. Split on whitespace to get tokens
4. First token is the command name
5. Check if second token is also part of command (for multi-word commands)
6. Normalize to uppercase
7. Add to set (for deduplication)
Return sorted list of unique commands
Supported Prompt Formats:
> COMMAND - Standard redis-cli promptredis> COMMAND - Alternative redis-cli prompt format (commonly used in documentation)Edge Cases to Handle:
#)> prefixACL CAT vs ACL DELUSER)JSON.SET)Location: New module build/components/command_enricher.py
Responsibilities:
data/commands_core.jsonMetadata Schema (per command):
{
"name": "HSET",
"summary": "Creates or modifies the value of a field in a hash.",
"group": "hash",
"complexity": "O(1) for each field/value pair added, so O(N) to add N field/value pairs when the command is called with multiple field/value pairs.",
"since": "2.0.0",
"link": "/commands/hset"
}
Link Generation:
/commands/{command_name_lowercase}HSET → /commands/hsetACL CAT → /commands/acl-catJSON.SET → /commands/json.setLocation: Modifications to build/local_examples.py and build/components/component.py
Integration Points:
local_examples.py (around line 180-200):# After creating example_metadata dict
example_metadata = {
'source': source_file,
'language': language,
'target': target_file,
'highlight': example.highlight,
'hidden': example.hidden,
'named_steps': example.named_steps,
'sourceUrl': None
}
# NEW: Extract and enrich CLI commands if this is a CLI example
if language == 'cli': # or check if content contains CLI commands
cli_commands = extract_cli_commands(example.content)
enriched_commands = enrich_commands(cli_commands)
example_metadata['cli_commands'] = enriched_commands
examples_data[example_id][client_name] = example_metadata
component.py (around line 270-290):# After setting other metadata fields
example_metadata['highlight'] = e.highlight
example_metadata['hidden'] = e.hidden
example_metadata['named_steps'] = e.named_steps
# NEW: Extract and enrich CLI commands
cli_commands = extract_cli_commands(e.content)
enriched_commands = enrich_commands(cli_commands)
if enriched_commands:
example_metadata['cli_commands'] = enriched_commands
examples_data[example_id][client_name] = example_metadata
Location: data/examples.json
Updated Structure:
{
"hash_tutorial": {
"Python": {
"source": "...",
"language": "python",
"target": "...",
"highlight": ["1-10"],
"hidden": [],
"named_steps": {"connect": "1-5"},
"sourceUrl": "...",
"cli_commands": [
{
"name": "HSET",
"summary": "Creates or modifies the value of a field in a hash.",
"group": "hash",
"complexity": "O(1) for each field/value pair added...",
"since": "2.0.0",
"link": "/commands/hset"
},
{
"name": "HGET",
"summary": "Returns the value of a field in a hash.",
"group": "hash",
"complexity": "O(1)",
"since": "2.0.0",
"link": "/commands/hget"
}
]
}
}
}
Key Design Decisions:
cli_commands is an optional field (only present if commands found)For Hugo Templates:
{{ $example := index $clientExamples $client }}
{{ if isset $example "cli_commands" }}
{{ range $example.cli_commands }}
<div class="command-badge">
<a href="{{ .link }}">{{ .name }}</a>
<span class="summary">{{ .summary }}</span>
</div>
{{ end }}
{{ end }}
For AI Systems:
data/examples.json> SET key value
↓
Command: SET
> ACL CAT
↓
Command: ACL CAT
> SCRIPT LOAD "return 1"
↓
Command: SCRIPT LOAD
Multi-word command detection:
commands_core.jsonACL CAT, SCRIPT LOAD, CLIENT LIST, CONFIG GET> JSON.SET doc $ '{"a":1}'
↓
Command: JSON.SET
> GRAPH.QUERY mygraph "MATCH (n) RETURN n"
↓
Command: GRAPH.QUERY
Dot notation detection:
.)JSON.SET, JSON.GET, GRAPH.QUERY, GRAPH.DELETE> HSET bike:1 model Deimos brand Ergonom
↓
Command: HSET (arguments ignored)
> HINCRBY bike:1 price 100
↓
Command: HINCRBY (arguments ignored)
Argument handling:
Deprecated Commands:
HMSET is deprecated in favor of HSET)replaced_by field from commands_core.json if availableCommand Aliases:
SUBSTR is an alias for GETRANGE)Handling Missing Commands:
commands_core.json, still include itInput: Markdown file with CLI example
{{< clients-example set="hash_tutorial" step="set_get_all" >}}
> HSET bike:1 model Deimos brand Ergonom type 'Enduro bikes' price 4972
(integer) 4
> HGET bike:1 model
"Deimos"
> HGETALL bike:1
1) "model"
2) "Deimos"
...
{{< /clients-example >}}
Step 1: Parse CLI Content
Input lines:
"> HSET bike:1 model Deimos brand Ergonom type 'Enduro bikes' price 4972"
"(integer) 4"
"> HGET bike:1 model"
'"Deimos"'
"> HGETALL bike:1"
"1) "model""
...
Extracted commands:
["HSET", "HGET", "HGETALL"]
Step 2: Enrich with Metadata
Lookup in commands_core.json:
HSET → {summary: "Creates or modifies...", group: "hash", ...}
HGET → {summary: "Returns the value...", group: "hash", ...}
HGETALL → {summary: "Returns all fields...", group: "hash", ...}
Step 3: Store in Metadata
{
"hash_tutorial": {
"Python": {
"cli_commands": [
{"name": "HSET", "summary": "...", "link": "/commands/hset"},
{"name": "HGET", "summary": "...", "link": "/commands/hget"},
{"name": "HGETALL", "summary": "...", "link": "/commands/hgetall"}
]
}
}
}
Step 4: Use in Templates or AI Systems
Phase 1: CLI Parser
build/components/cli_parser.pyextract_cli_commands(content) functionPhase 2: Command Enricher
build/components/command_enricher.pyload_commands_metadata() functionenrich_commands(command_names) functionPhase 3: Integration
build/local_examples.py to call extractionbuild/components/component.py to call extractioncli_commands field is optionalPhase 4: Validation
data/examples.json contains cli_commands fieldPhase 5: Documentation
The Commands Display UI shows the Redis commands used in each code example in an interactive, non-intrusive way. This feature helps users quickly understand what commands an example demonstrates without reading through the code.
Design Goals:
Container: Footer bar of the code example box (layouts/partials/tabs/wrapper.html)
Position: Foldout section placed above the existing quickstart link
Visual Hierarchy:
┌─────────────────────────────────────────────────────┐
│ Code Example Box │
│ │
│ [Language Selector] [Run in Browser] [Copy] [...] │
│ ┌─────────────────────────────────────────────────┐│
│ │ Syntax-highlighted code ││
│ │ ││
│ └─────────────────────────────────────────────────┘│
│ ┌─────────────────────────────────────────────────┐│
│ │ ▼ Commands: HSET, HGET, HGETALL │ ← Foldout (closed)
│ │ 📚 Quick-Start: redis-py │
│ └─────────────────────────────────────────────────┘│
└─────────────────────────────────────────────────────┘
When expanded:
┌─────────────────────────────────────────────────────┐
│ ▼ Commands: HSET, HGET, HGETALL │ ← Foldout (open)
│ • HSET - Creates or modifies hash fields │
│ • HGET - Returns a hash field value │
│ • HGETALL - Returns all hash fields and values │
│ 📚 Quick-Start: redis-py │
└─────────────────────────────────────────────────────┘
Foldout Container (in footer):
<div class="commands-foldout">
<button class="commands-toggle" aria-expanded="false">
<span class="toggle-icon">▼</span>
<span class="commands-label">Commands:</span>
<span class="commands-list">HSET, HGET, HGETALL</span>
</button>
<div class="commands-details" hidden>
<ul class="commands-list-detailed">
<li><strong>HSET</strong> - Creates or modifies hash fields</li>
<li><strong>HGET</strong> - Returns a hash field value</li>
<li><strong>HGETALL</strong> - Returns all hash fields and values</li>
</ul>
</div>
</div>
CSS Classes (Tailwind):
commands-foldout: Container for entire foldout sectioncommands-toggle: Button that opens/closes the foldouttoggle-icon: Chevron/arrow icon that rotates on togglecommands-label: "Commands:" text labelcommands-list: Comma-separated command names (shown when closed)commands-details: Container for expanded details (hidden by default)commands-list-detailed: Unordered list of commands with descriptionsResponsive Design:
Visual Feedback:
Color Scheme:
Metadata Field: commands array in page metadata
codeExamples[].commands in page metadata JSON["HSET", "HGET", "HGETALL"])code-examples-json.html partial (extracts from examples.json)Extraction Logic (in layouts/partials/code-examples-json.html):
steps_commands in data/examples.jsoncommands field to example object in metadataToggle Functionality:
// Pseudo-code for toggle behavior
document.querySelectorAll('.commands-toggle').forEach(button => {
button.addEventListener('click', () => {
const details = button.nextElementSibling;
const isExpanded = button.getAttribute('aria-expanded') === 'true';
// Toggle visibility
details.hidden = isExpanded;
button.setAttribute('aria-expanded', !isExpanded);
// Rotate icon
const icon = button.querySelector('.toggle-icon');
icon.style.transform = isExpanded ? 'rotate(0deg)' : 'rotate(180deg)';
// Persist state (optional)
localStorage.setItem(`commands-expanded-${exampleId}`, !isExpanded);
});
});
State Persistence (Optional Enhancement):
commands-expanded-{exampleId}Language Tab Synchronization:
ARIA Attributes:
aria-expanded: Indicates if foldout is open or closedaria-label: Descriptive label for screen readersrole="button": Semantic role for toggle elementKeyboard Navigation:
Screen Reader Support:
Touch Targets:
Screen Space:
Orientation Changes:
Rendering:
Memory:
Phase 2 (Post-MVP):
Phase 3 (Advanced):
Phase 1: Metadata Extraction ✅ (Already Complete)
examples.json in code-examples-json.htmlcommands field to example metadataPhase 2: HTML and Styling
layouts/partials/tabs/wrapper.htmlPhase 3: JavaScript Interactivity
Phase 4: Accessibility
Phase 5: Testing and Refinement
This section documents patterns and best practices for enhancing the UI with metadata that's already available in the command data files (e.g., data/commands_core.json). This approach allows for rich, contextual UI features without requiring changes to the build pipeline.
Real-world example: Adding ACL category information to the commands display with individual links to category definitions.
When you want to display metadata from command data files and make it linkable, follow this pattern:
First, understand what metadata is available in your data files:
// data/commands_core.json
{
"HSET": {
"name": "HSET",
"summary": "Creates or modifies the value of a field in a hash.",
"group": "hash",
"complexity": "O(1) for each field/value pair...",
"since": "2.0.0",
"acl_categories": ["@keyspace", "@write", "@fast"],
"link": "/commands/hset"
}
}
Key insight: The command data already contains rich metadata. Check what's available before adding new data sources.
The command data is already loaded in Hugo as site.Data.commands_core, site.Data.commands_redisearch, etc. You can access it directly in templates:
{{ $cmdData := (index site.Data.commands_core $cmdName) }}
{{ if $cmdData.acl_categories }}
{{ range $cmdData.acl_categories }}
{{ . }} <!-- e.g., "@keyspace", "@write", "@fast" -->
{{ end }}
{{ end }}
For metadata that should be linkable, add anchor elements (<a id="..."></a>) in the relevant documentation page:
* <a id="keyspace"></a>**keyspace** - Writing or reading from keys...
* <a id="write"></a>**write** - Writing to keys...
* <a id="fast"></a>**fast** - Fast O(1) commands...
Important: Place anchors before the content you want to link to, not around it. This preserves the visual styling of the content while making it linkable.
In your template, loop through the metadata and generate individual links:
{{ if $cmdData.acl_categories }}
<span class="text-slate-500">
(
{{ range $idx, $category := $cmdData.acl_categories }}
{{ if gt $idx 0 }}<span class="text-slate-500">, </span>{{ end }}
{{ $categoryId := lower (replace $category "@" "") }}
<a href="{{ absURL (printf "path/to/page/#%s" $categoryId) }}"
class="text-slate-400 hover:text-slate-300 hover:underline"
title="{{ $category }}">
{{ $category }}
</a>
{{ end }}
)
</span>
{{ end }}
Key techniques:
range with index to handle separators (commas) between itemslower and replace to normalize IDs (remove "@" prefix, convert to lowercase)absURL to generate correct URLs regardless of site configurationprintf to construct URLs dynamicallytitle attributes for accessibilityWhen converting metadata to anchor IDs, normalize consistently:
{{ $categoryId := lower (replace $category "@" "") }}
<!-- "@keyspace" becomes "keyspace" -->
<!-- "@write" becomes "write" -->
Why this matters:
When metadata may not be available for all items, use conditional rendering:
{{ if $cmdData }}
{{ if $cmdData.acl_categories }}
<!-- Display ACL categories -->
{{ end }}
{{ else }}
<!-- Fallback for commands without metadata -->
<span class="font-semibold text-slate-200">{{ $cmdName }}</span>
{{ end }}
Benefits:
When displaying metadata alongside primary content, use visual hierarchy:
<div class="flex items-center gap-1">
<a href="..." class="font-mono text-slate-200 hover:text-white">
{{ $cmdName }}
</a>
{{ if $cmdData.acl_categories }}
<span class="text-slate-500">
(
<!-- Links with slightly less prominent color -->
<a href="..." class="text-slate-400 hover:text-slate-300">
{{ $category }}
</a>
)
</span>
{{ end }}
</div>
Design principles:
When implementing a new metadata-driven UI feature:
site.Data?| Problem | Solution |
|---|---|
Hugo template function not found (e.g., trimPrefix) | Use built-in functions only: lower, replace, printf, range, etc. Check Hugo docs for available functions. |
| Anchor IDs don't match generated links | Normalize consistently in both places. Use the same lower and replace operations. |
| Links appear but don't navigate | Verify anchor elements are placed correctly in the documentation (before content, not around it). |
| Metadata missing for some commands | Use if conditions to check for metadata before displaying. Provide fallback content. |
| Special characters in metadata break URLs | Normalize metadata before using in URLs. Remove prefixes, convert to lowercase, escape special characters. |
| Styling looks wrong on mobile | Use responsive Tailwind classes. Test on actual mobile devices, not just browser dev tools. |
See Appendix: Adding a Language for complete step-by-step instructions.
Quick checklist:
config.toml (clientsExamples, clientsConfig)data/components/data/components/index.jsonPREFIXES in build/components/example.py ⚠️ CRITICAL - DO NOT SKIPbuild/local_examples.pybuild/jupyterize/ if applicable)Tab Appearance: Edit layouts/partials/tabs/wrapper.html
Syntax Highlighting: Edit layouts/partials/tabbed-clients-example.html
highlight function optionsFooter Links: Edit layouts/partials/tabs/wrapper.html
The code example system includes automated tests for critical components. Tests are located in the build/ directory and can be run independently of the full build process.
Why testing matters:
| Test File | Purpose | Coverage |
|---|---|---|
build/test_cli_parser.py | CLI command extraction | Both > and redis> prompts, mixed formats |
build/test_command_lists.py | Command list feature | CLI parser, markdown parser, enricher, integration |
build/jupyterize/test_jupyterize.py | Jupyter notebook conversion | Language-specific boilerplate, code unwrapping |
build/test_railroad.py | Railroad diagram generation | Command syntax visualization |
# Run CLI parser tests
python3 build/test_cli_parser.py
# Run command list feature tests
python3 build/test_command_lists.py
# Run Jupyter notebook tests
python3 build/jupyterize/test_jupyterize.py
# Run railroad diagram tests
python3 build/test_railroad.py
# Run all tests
python3 build/test_cli_parser.py && python3 build/test_command_lists.py
Each test file follows this pattern:
#!/usr/bin/env python3
"""Test description."""
import sys
import os
# Add build directory to path
sys.path.insert(0, os.path.dirname(__file__))
from components.module_name import function_name
def test_feature_name():
"""Test description."""
# Arrange: Set up test data
input_data = "..."
# Act: Call the function
result = function_name(input_data)
# Assert: Verify the result
assert result == expected_value, f"Expected {expected_value}, got {result}"
print("✓ Test passed")
def main():
"""Run all tests."""
try:
test_feature_name()
print("✅ All tests passed!")
return 0
except AssertionError as e:
print(f"❌ Test failed: {e}")
return 1
if __name__ == '__main__':
sys.exit(main())
When adding or modifying CLI command extraction, test these scenarios:
1. Prompt Format Support:
# Test both prompt formats
content_with_gt = "> SET key value"
content_with_redis = "redis> SET key value"
# Both should extract 'SET'
2. Command Types:
# Single-word commands
"> SET key value" # Should extract: SET
# Multi-word commands
"> ACL CAT" # Should extract: ACL CAT
# Dot notation
"> JSON.SET doc $ '{}'" # Should extract: JSON.SET
3. Output Filtering:
# Output lines should be ignored
"> SET key value
(integer) 1
> GET key
\"value\""
# Should extract: SET, GET (not the output lines)
4. Deduplication:
# Duplicate commands should be deduplicated
"> SET key1 value1
> SET key2 value2
> GET key1"
# Should extract: SET, GET (SET appears only once)
5. Edge Cases:
# Empty lines and comments should be ignored
"> SET key value
# This is a comment
> GET key"
# Should extract: SET, GET
# Mixed prompt formats in same content
"> SET key value
redis> GET key"
# Should extract: SET, GET
When implementing a new feature (e.g., new command enrichment, new parser):
build/test_feature_name.py# Full build (clean + dependencies + components + hugo)
make all
# Build and serve locally
make serve
# Use local components only (skip GitHub cloning)
make localserve
# Just process components (useful for testing)
python3 build/make.py
# Process only local examples
python3 build/local_examples.py
The build process has strict dependencies - each step requires the previous step to complete:
1. Clean (make clean):
public/ (Hugo output)resources/ (Hugo cache)node_modules/ (Node.js packages)examples/ (processed examples)2. Install Dependencies (make deps):
npm install: Install Node.js dependencies (Tailwind CSS, PostCSS)pip3 install -r requirements.txt: Install Python dependencies (pytoml, PyYAML, requests)3. Process Components (make components):
python3 build/make.py--skip-clone)local_examples/examples/ directory (processed code files)data/examples.json (metadata)4. Build Hugo (make hugo):
hugo --gc --logLevel debugdata/examples.json)examples/ directory)public/examples/ and data/examples.json from step 3Build Optimization:
make all (all steps)python3 build/make.py then hugohugo serve (auto-reloads)hugo serve (auto-reloads)make all (Hugo needs restart)Workflow: .github/workflows/main.yml
Steps:
make allEnvironment Variables:
PRIVATE_ACCESS_TOKEN: GitHub token for private repositoriesREPOSITORY_URL: Current repository URL (for preview mode)REPO_DIR: Repository directory (for preview mode)When building from a specific repository (e.g., during PR preview):
REPOSITORY_URL environment variableREPO_DIR to repository path"Example not found" warning in Hugo build:
WARN [tabbed-clients-example] Example not found "my_example" for "content/page.md"
data/examples.jsonEXAMPLE: header in source file matches the IDpython3 build/make.pygrep my_example data/examples.json"Unknown language" error during build:
ERROR: Unknown language "newlang" for example /path/to/file
PREFIXES dictionarybuild/components/example.py PREFIXESGit clone failures:
ERROR: command failed: git clone https://github.com/...
PRIVATE_ACCESS_TOKEN environment variable--skip-clone flag to skip cloning during developmentPython import errors:
ModuleNotFoundError: No module named 'pytoml'
pip3 install -r requirements.txtExample shows test code/imports:
REMOVE_START/REMOVE_END markersexamples/{example_id}/Code highlighting wrong lines:
HIDE_START or STEP_START markershighlight array in data/examples.jsonWrong language variant shown (e.g., Java-Sync instead of Java-Async):
lettuce-async/)local_examples.py path override logicTab not appearing for a language:
config.toml or example doesn't exist for that languageclientsExamples arraydata/examples.json has entry for that languagelabel field matches exactly (case-sensitive)"Run in browser" link not appearing in all boxes when language changes:
codetabs.js synchronizes all dropdowns without triggering change events on the non-selected onesupdateAllBinderLinks() function (see Common Pitfall: Global Synchronization Across Multiple Codetabs Instances)/develop/data-types/hashes/)BINDER_ID not extracted or appearing in output:
Symptom 1: binderId field missing from data/examples.json
# BINDER_ID for Python, // BINDER_ID for JavaScriptpython-landing, main)6bbed3da294e8de5a8c2ad99abf883731a50d4dd)python3 build/local_examples.py --loglevel DEBUG{comment_prefix} BINDER_ID {git-reference}Symptom 2: BINDER_ID line appears in processed output file
output = False not set in detection logicelif re.search(binder, l): block sets output = Falseexamples/{example_id}/ - should not contain BINDER_ID lineSymptom 3: "binderId": null in metadata
example.binder_id is not None:
if example.binder_id:
example_metadata['binderId'] = example.binder_id
Symptom 4: Wrong value extracted
([a-zA-Z0-9_-]+)match.group(1) to extract the captured valueBinderHub "Run in browser" link issues:
Symptom 1: Link not appearing in example box
Cause 1: No binderId in metadata
# Check if binderId exists in metadata
python3 -c "import json; data = json.load(open('data/examples.json')); print(data['example_id']['Python'].get('binderId'))"
BINDER_ID marker to source file and rebuild with python3 build/local_examples.pyCause 2: data-binder-id attribute missing from HTML
panel)data-binder-id attribute<div class="panel" ... data-binder-id="6bbed3da294e8de5a8c2ad99abf883731a50d4dd" ...>binderId through tab datalayouts/partials/tabbed-clients-example.html includes "binderId" $binderId in tab dictCause 3: JavaScript not executing
document.getElementById('binder-link-container-landing-stepconnect') (replace with actual ID)nulllayouts/partials/tabs/wrapper.html has container div with correct ID formatCause 4: JavaScript can't find panels
document.querySelectorAll('[data-codetabs-id="landing-stepconnect"].panel') (replace with actual ID)data-codetabs-id attributedata-codetabs-id="{{ $id }}" to panel divs in wrapper templateSymptom 2: Link appears but URL is malformed
Cause 1: Missing URL encoding in JavaScript
href attribute valuehttps://redis.io/binder/v2/gh/redis/binder-launchers/6bbed3da294e8de5a8c2ad99abf883731a50d4dd?urlpath=%2Fdoc%2Ftree%2Fdemo.ipynb?urlpath=/doc/tree/demo.ipynb (missing %2F encoding)%2F directly (not %%2F - that's for Hugo templates)'?urlpath=%2Fdoc%2Ftree%2Fdemo.ipynb'Cause 2: Wrong notebook filename
demo.ipynbdemo.ipynb - do not change per exampleCause 3: binderId value is incorrect in JavaScript
document.querySelector('.panel:not(.panel-hidden)')panel.getAttribute('data-binder-id')data/examples.jsonBINDER_ID in source file is correct (matches what's in redis/binder-launchers repo)Symptom 3: Link opens but BinderHub shows error
binderId
redis/binder-launchers repositoryBINDER_ID in source file to valid commit SHAredis/binder-launchers repository for the commitSymptom 4: Link appears but example doesn't work in BinderHub
redis/binder-launchers repository has the necessary kernel configurationSymptom 5: Link doesn't update when changing languages
Cause 1: Event listener not attached
updateBinderLink() is called (add console.log to function)langSelect.addEventListener('change', updateBinderLink) is in scriptCause 2: Container not being cleared
binderId to one withoutcontainer.innerHTML = '' is called at start of updateBinderLink()Cause 3: Wrong panel being queried
console.log('Tab index:', tabIndex, 'Panel:', currentPanel)data-index attribute on <option> elements matches panel orderSymptom 6: Link text or styling is wrong
text-xs text-slate-300 hover:text-white hover:underline whitespace-nowrap flex items-center gap-1Symptom 7: Multiple links appear or wrong link shown
data-codetabs-iddocument.querySelectorAll('[id^="binder-link-container-"]'){{ $id }} valueSymptom 8: JavaScript errors in console
Cannot read property 'getAttribute' of undefined: Panel not found
data-codetabs-id matches between container and panelscontainer is null: Container div not rendered
langSelect is null: Language selector not found
id="lang-select-{{ $id }}" on select elementBuild takes too long:
--skip-clone during developmentHugo serve slow to reload:
python3 build/local_examples.py separatelydata/examples.json for metadataexamples/{example_id}/ for processed codefor-ais-only/tcedocs/README.md for author-focused documentationfor-ais-only/tcedocs/README.md - For documentation authorsMakefile for all available build commandsFile Extensions to Languages (build/local_examples.py):
{
'.py': 'python',
'.js': 'node.js',
'.go': 'go',
'.cs': 'c#',
'.java': 'java',
'.php': 'php',
'.rs': 'rust'
}
Comment Prefixes (build/components/example.py):
{
'python': '#',
'node.js': '//',
'java': '//',
'java-sync': '//',
'java-async': '//',
'java-reactive': '//',
'go': '//',
'c#': '//',
'c#-sync': '//',
'c#-async': '//',
'redisvl': '#',
'php': '//',
'rust': '//',
'rust-sync': '//',
'rust-async': '//'
}
Test Markers (removed from output):
{
'java': '@Test',
'java-sync': '@Test',
'java-async': '@Test',
'java-reactive': '@Test',
'c#': r'\[Fact]|\[SkipIfRedis\(.*\)]',
'c#-sync': r'\[Fact]|\[SkipIfRedis\(.*\)]',
'c#-async': r'\[Fact]|\[SkipIfRedis\(.*\)]',
'rust': r'#\[test]|#\[cfg\(test\)]|#\[tokio::test]'
}
Complete step-by-step guide for adding a new programming language to the system.
Prerequisites:
Step 1: Update Hugo Configuration
Edit config.toml:
[params]
# Add to the end of the array (or desired position)
clientsExamples = ["Python", "Node.js", ..., "NewLang"]
[params.clientsConfig]
# Add configuration for quickstart link
"NewLang"={quickstartSlug="newlang"}
Step 2: Create Component Configuration
Create data/components/newlang_client.json:
{
"id": "newlang_client",
"type": "client",
"name": "newlang-client",
"language": "NewLang",
"label": "NewLang",
"repository": {
"git_uri": "https://github.com/redis/newlang-client"
},
"examples": {
"git_uri": "https://github.com/redis/newlang-client",
"path": "doctests",
"pattern": "*.nl"
}
}
Field explanations:
id: Unique identifier (used in filenames)language: Must match clientsExamples in config.tomllabel: Display name in tabs (usually same as language)examples.path: Directory in repo containing examplesexamples.pattern: Glob pattern for example filesStep 3: Register Component
Edit data/components/index.json:
{
"clients": [
"nredisstack_sync",
...
"newlang_client" // Add here
]
}
Step 4: Update Example Parser
Edit build/components/example.py:
PREFIXES = {
'python': '#',
...
'newlang': '//', // Add comment prefix for the language
}
# Only if language has test markers to remove:
TEST_MARKER = {
'java': '@Test',
...
'newlang': r'@TestAnnotation', // Add test marker regex
}
Step 5: Update Local Examples Processor
Edit build/local_examples.py:
EXTENSION_TO_LANGUAGE = {
'.py': 'python',
...
'.nl': 'newlang', // Add file extension mapping
}
LANGUAGE_TO_CLIENT = {
'python': 'Python',
...
'newlang': 'NewLang', // Add language to client name mapping
}
Step 6: Test the Integration
# Clean and rebuild
make clean
make all
# Check that examples were processed
cat data/examples.json | grep NewLang
# Serve and verify in browser
hugo serve
Step 7: Add Example Code
In the client repository, create example files:
// EXAMPLE: newlang_basic
// REMOVE_START
import test_framework
// REMOVE_END
// STEP_START connect
client = new RedisClient("localhost", 6379)
// STEP_END
// STEP_START set_get
client.set("key", "value")
value = client.get("key")
// STEP_END
Step 8: Reference in Documentation
In Markdown files:
{{< clients-example set="newlang_basic" step="connect" />}}
data/examples.json Structure:
{
"example_id": {
"Language": {
"source": "path/to/original/file",
"language": "lowercase_language",
"target": "examples/example_id/processed_file",
"highlight": ["1-10", "15-20"],
"hidden": ["5-8"],
"named_steps": {
"step_name": "1-5"
},
"sourceUrl": "https://github.com/...",
"binderId": "6bbed3da294e8de5a8c2ad99abf883731a50d4dd"
}
}
}
Field descriptions:
source: Original file path (before processing)language: Lowercase language identifiertarget: Processed file path (what Hugo reads)highlight: Line ranges to highlight (1-based, inclusive)hidden: Line ranges initially hidden (revealed with eye button)named_steps: Map of step names to line rangessourceUrl: GitHub link to original source (null for local examples)binderId: Optional - BinderHub commit hash for interactive notebook link (string, only present if BINDER_ID marker exists in source file)Metadata Hierarchy:
binderId field is stored per-language, not per-example-setBINDER_ID marker is not present in the source file, the binderId field should be omitted entirely (not set to null or empty string)| Marker | Purpose | Example | Notes |
|---|---|---|---|
EXAMPLE: id | Define example ID | # EXAMPLE: home_vecsets | Required. Must be first line. Removed from processed output. |
BINDER_ID ref | Define BinderHub Git reference | # BINDER_ID python-landing | |
# BINDER_ID 6bbed3da294e8de5a8c2ad99abf883731a50d4dd | Optional. Typically line 2 (after EXAMPLE). Value can be a Git branch name (e.g., python-landing, main) or commit SHA (40 hex chars). Removed from processed output. Stored as binderId in metadata. Used to generate interactive Jupyter notebook links. | ||
HIDE_START | Start hidden block | # HIDE_START | Code hidden by default, revealed with eye button |
HIDE_END | End hidden block | # HIDE_END | Must close HIDE_START |
REMOVE_START | Start removed block | # REMOVE_START | Code completely removed from output |
REMOVE_END | End removed block | # REMOVE_END | Must close REMOVE_START |
STEP_START name | Start named step | # STEP_START connect | Name is lowercase. Removed from output. |
STEP_END | End named step | # STEP_END | Must close STEP_START. Removed from output. |
Important:
EXAMPLE:, BINDER_ID, STEP_START, STEP_END, HIDE_START, HIDE_END, REMOVE_START, REMOVE_END) are removed from the processed output file| Parameter | Type | Required | Default | Description |
|---|---|---|---|---|
set | string | Yes | - | Example set name (matches EXAMPLE: ID) |
step | string | No | "" | Named step to display (from STEP_START) |
lang_filter | string | No | "" | Comma-separated language filter |
max_lines | int | No | 100 | Max lines shown before "show more" |
dft_tab_name | string | No | ">_ Redis CLI" | Custom name for CLI tab |
dft_tab_link_title | string | No | - | Custom footer link text for CLI tab |
dft_tab_url | string | No | - | Custom footer link URL for CLI tab |
show_footer | bool | No | true | Show/hide footer with links |
Usage examples:
<!-- Basic usage -->
{{< clients-example set="example_id" />}}
<!-- With step -->
{{< clients-example set="example_id" step="connect" />}}
<!-- Filter to specific languages -->
{{< clients-example set="example_id" lang_filter="Python,Node.js" />}}
<!-- With redis-cli content -->
{{< clients-example set="example_id" step="" >}}
> SET key value
OK
> GET key
"value"
{{< /clients-example >}}
The lang_filter parameter uses exact matching on comma-separated language names:
Matching Logic:
"C#-Sync,C#-Async" → ["C#-Sync", "C#-Async"])config.toml, check if it exactly matches any value in the filter listExamples:
lang_filter="C#-Sync,C#-Async" → Shows only C# sync and async tabslang_filter="Python" → Shows only Python tablang_filter="Python,Node.js" → Shows Python and Node.js tabslang_filter="C" → Shows only C tab (does NOT match "C#-Sync" or "C#-Async")Important: Language names must match exactly as they appear in config.toml. This prevents accidental matches when one language name is a substring of another (e.g., "C" is a substring of "C#-Sync", but they are treated as distinct languages).
Implementation: See layouts/partials/tabbed-clients-example.html for the matching logic.
When adding the C client, a critical step was initially missed: adding the language to the PREFIXES dictionary in build/components/example.py.
Why this matters: The PREFIXES dictionary maps each language to its comment prefix character(s). This is used by the example parser to:
EXAMPLE:, STEP_START, HIDE_START, etc.What happens if you skip this step:
Unknown language "c" for example {path}The fix:
# In build/components/example.py, add to PREFIXES dictionary:
PREFIXES = {
...
'c': '//', # C uses // for comments
...
}
The original checklist was incomplete. Here's the comprehensive version:
Configuration Files:
config.toml - Add to clientsExamples list and clientsConfig sectiondata/components/{language}.json - Create component configurationdata/components/index.json - Register the componentBuild System:
4. ✅ build/components/example.py - CRITICAL: Add to PREFIXES dictionary
5. ✅ build/components/example.py - Add to TEST_MARKER dictionary (if language has test annotations)
6. ✅ build/local_examples.py - Add file extension mapping to EXTENSION_TO_LANGUAGE
7. ✅ build/local_examples.py - Add language to LANGUAGE_TO_CLIENT mapping
Optional (if Jupyter notebook support is needed):
8. ⚠️ build/jupyterize/jupyterize.py - Add to KERNEL_SPECS dictionary
9. ⚠️ build/jupyterize/jupyterize_config.json - Add language-specific boilerplate and unwrap patterns
Documentation:
10. ✅ for-ais-only/tcedocs/SPECIFICATION.md - Update examples and checklist
11. ✅ for-ais-only/tcedocs/README.md - Update tables and examples
Important: Before adding a new language, check if examples already exist in the repository:
local_examples/client-specific/{language}/ for local examplesFor C (hiredis), there was already a landing.c example in local_examples/client-specific/c/ that was ready to be processed once the language was properly configured.
Different languages use different comment styles. When adding a language, ensure the correct prefix is used:
| Language | Prefix | Example |
|---|---|---|
| Python | # | # EXAMPLE: my_example |
| C | // | // EXAMPLE: my_example |
| Java | // | // EXAMPLE: my_example |
| Go | // | // EXAMPLE: my_example |
| C# | // | // EXAMPLE: my_example |
| PHP | // | // EXAMPLE: my_example |
| Rust | // | // EXAMPLE: my_example |
| Node.js | // | // EXAMPLE: my_example |
Critical: The PREFIXES dictionary uses lowercase language names as keys, but the Example class converts the language to lowercase before accessing it (line 57 in example.py).
After adding a new language, verify the integration:
# 1. Check that the language is recognized
grep -r "c" build/components/example.py # Should find 'c': '//' in PREFIXES
# 2. Process examples
python3 build/local_examples.py
# 3. Verify examples were processed
grep -i "landing" data/examples.json | grep -i "c"
# 4. Check for errors in the build output
python3 build/make.py 2>&1 | grep -i "error\|unknown language"
# 5. Build and serve
hugo serve
Forgetting the PREFIXES entry: This is the most common mistake. The build will appear to succeed but examples won't be processed.
Case sensitivity: Language names in PREFIXES must be lowercase, but clientsExamples in config.toml uses proper case (e.g., "C" not "c").
Inconsistent naming: Ensure the language name is consistent across:
config.toml clientsExamples (proper case, e.g., "C")config.toml clientsConfig keys (proper case, e.g., "C")build/local_examples.py LANGUAGE_TO_CLIENT values (proper case, e.g., 'C')build/components/example.py PREFIXES keys (lowercase, e.g., 'c')Missing component registration: If the component isn't registered in data/components/index.json, remote examples won't be fetched.
Wrong file extension mapping: Ensure the file extension correctly maps to the language name in EXTENSION_TO_LANGUAGE.
Single-variant languages (Python, Go, PHP, C):
Multi-variant languages (Java, Rust, C#):
get_client_name_from_language_and_path()C is a single-variant language, so it doesn't require path-based overrides.