From 4901103c8ce0dbade0584aa05e557afda44d8fbf Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Fri, 27 Mar 2026 11:17:51 +0000 Subject: [PATCH 1/6] docs: add MkDocs website with automatic README splitting (#95) Implements a static documentation website using MkDocs that automatically splits README.md into multiple pages based on level 2 headings. The site is deployed to GitHub Pages via GitHub Actions on every merge to main. Closes #95 Co-Authored-By: Claude Sonnet 4.5 Signed-off-by: Philippe Martin --- .github/workflows/deploy-docs.yml | 79 ++++++++++++ .gitignore | 4 + website/WEBSITE.md | 41 ++++++ website/docs/.gitkeep | 2 + website/hooks.py | 205 ++++++++++++++++++++++++++++++ website/mkdocs.yml | 145 +++++++++++++++++++++ website/requirements.txt | 31 +++++ 7 files changed, 507 insertions(+) create mode 100644 .github/workflows/deploy-docs.yml create mode 100644 website/WEBSITE.md create mode 100644 website/docs/.gitkeep create mode 100644 website/hooks.py create mode 100644 website/mkdocs.yml create mode 100644 website/requirements.txt diff --git a/.github/workflows/deploy-docs.yml b/.github/workflows/deploy-docs.yml new file mode 100644 index 0000000..4c382f7 --- /dev/null +++ b/.github/workflows/deploy-docs.yml @@ -0,0 +1,79 @@ +# Copyright (C) 2026 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +name: deploy-docs + +on: + push: + branches: + - main + paths: + - 'README.md' + - 'website/**' + - '.github/workflows/deploy-docs.yml' + workflow_dispatch: + +permissions: + contents: read + pages: write + id-token: write + +# Concurrency control - only one deployment at a time +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + name: Build Documentation + runs-on: ubuntu-24.04 + steps: + - name: Checkout code + uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Set up Python + uses: actions/setup-python@0b93645e9fea7318ecaed2b359559ac225c90a2b # v5.3.0 + with: + python-version: '3.12' + cache: 'pip' + cache-dependency-path: website/requirements.txt + + - name: Install dependencies + working-directory: website + run: | + pip install --upgrade pip + pip install -r requirements.txt + + - name: Build MkDocs site + working-directory: website + run: mkdocs build --strict --verbose + + - name: Upload artifact + uses: actions/upload-pages-artifact@56afc609e74202658d3ffba0e8f6dda462b719fa # v3.0.1 + with: + path: ./website/site + + deploy: + name: Deploy to GitHub Pages + needs: build + runs-on: ubuntu-24.04 + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@d6db90164ac5ed86f2b6aed7e0febac5b3c0c03e # v4.0.5 diff --git a/.gitignore b/.gitignore index 82ddfc6..10916d6 100644 --- a/.gitignore +++ b/.gitignore @@ -18,3 +18,7 @@ go.work # Build artifacts /kortex-cli dist/ + +# MkDocs build output +website/site/ +website/docs/*.md diff --git a/website/WEBSITE.md b/website/WEBSITE.md new file mode 100644 index 0000000..4330a40 --- /dev/null +++ b/website/WEBSITE.md @@ -0,0 +1,41 @@ +# Website Documentation + +This directory contains the MkDocs configuration for automatically generating the kortex-cli documentation website from the repository's README.md. + +## How It Works + +1. **Single Source**: The project `README.md` (in the repository root) is the only file you edit +2. **Automatic Splitting**: The `hooks.py` script splits README.md by `##` headings into separate pages +3. **Build & Deploy**: GitHub Actions automatically builds and deploys the site on merge to main + +## Local Preview + +```bash +# From the website/ directory: +cd website + +# Create and activate virtual environment (first time only) +python3 -m venv venv +source venv/bin/activate # On Windows: venv\Scripts\activate + +# Install dependencies (first time only) +pip install -r requirements.txt + +# Start preview server +mkdocs serve + +# Open http://127.0.0.1:8000 in browser +``` + +## Files + +- **mkdocs.yml** - MkDocs configuration (theme, plugins, SEO settings) +- **hooks.py** - Custom hook that splits README.md into multiple pages +- **requirements.txt** - Python dependencies (MkDocs, Material theme, plugins) +- **docs/** - Temporary directory for generated markdown files (ignored by git) + +## Deployment + +The website is automatically deployed to GitHub Pages when changes are pushed to the `main` branch. + +Website URL: https://kortex-hub.github.io/kortex-cli/ diff --git a/website/docs/.gitkeep b/website/docs/.gitkeep new file mode 100644 index 0000000..a2c3fed --- /dev/null +++ b/website/docs/.gitkeep @@ -0,0 +1,2 @@ +# This directory is used by MkDocs during the build process. +# Markdown files are automatically generated from README.md via hooks.py diff --git a/website/hooks.py b/website/hooks.py new file mode 100644 index 0000000..a2e2e6f --- /dev/null +++ b/website/hooks.py @@ -0,0 +1,205 @@ +#!/usr/bin/env python3 +# Copyright (C) 2026 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +""" +MkDocs hooks for automatically splitting README.md into multiple pages. + +This hook reads README.md and creates separate pages for each ## (level 2) heading. +The pages are created as virtual files during the build process, so README.md remains +the single source of truth. +""" + +import re +import logging +from pathlib import Path + +logger = logging.getLogger("mkdocs.plugins") + + +def slugify(text): + """Convert heading text to a URL-friendly slug.""" + # Remove special characters and convert to lowercase + slug = re.sub(r'[^\w\s-]', '', text.lower()) + # Replace spaces with hyphens + slug = re.sub(r'[\s_]+', '-', slug) + # Remove leading/trailing hyphens + slug = slug.strip('-') + return slug + + +def split_readme_content(content): + """ + Split README.md content into sections based on ## headings. + + Returns: + list of dict: Each dict contains 'title', 'slug', and 'content' + """ + sections = [] + lines = content.split('\n') + + # First section: everything before first ## heading (becomes index.md) + current_section = { + 'title': 'Home', + 'slug': 'index', + 'content': [] + } + + i = 0 + # Capture content before first ## heading + while i < len(lines): + line = lines[i] + if line.startswith('## '): + break + current_section['content'].append(line) + i += 1 + + sections.append(current_section) + + # Process each ## section + while i < len(lines): + line = lines[i] + + if line.startswith('## '): + # Start new section + title = line[3:].strip() # Remove "## " + slug = slugify(title) + + current_section = { + 'title': title, + 'slug': slug, + 'content': [line] # Include the heading + } + sections.append(current_section) + else: + # Add to current section + current_section['content'].append(line) + + i += 1 + + # Join content lines back into strings + for section in sections: + section['content'] = '\n'.join(section['content']) + + return sections + + +def on_files(files, config): + """ + Called after files are collected from docs_dir. + Split README.md into multiple virtual files. + """ + from mkdocs.structure.files import File + + # Read README.md from project root + # docs_dir is website/docs -> parent is website -> parent is repo root + docs_dir_abs = Path(config['docs_dir']).resolve() + readme_path = docs_dir_abs.parent.parent / 'README.md' + + if not readme_path.exists(): + logger.warning(f"README.md not found at {readme_path}") + return files + + logger.info(f"Splitting README.md from {readme_path}") + + with open(readme_path, 'r', encoding='utf-8') as f: + readme_content = f.read() + + # Split README into sections + sections = split_readme_content(readme_content) + + logger.info(f"Found {len(sections)} sections in README.md") + + # Remove existing index.md symlink if it exists + files_to_remove = [f for f in files if f.src_path == 'index.md'] + for f in files_to_remove: + files.remove(f) + + # Create virtual files for each section + for section in sections: + filename = f"{section['slug']}.md" + + # Create a temporary file + temp_path = Path(config['docs_dir']) / filename + temp_path.write_text(section['content'], encoding='utf-8') + + # Create MkDocs File object + file_obj = File( + path=filename, + src_dir=config['docs_dir'], + dest_dir=config['site_dir'], + use_directory_urls=config['use_directory_urls'] + ) + + files.append(file_obj) + logger.debug(f"Created virtual file: {filename} ({section['title']})") + + return files + + +def on_nav(nav, config, files): + """ + Called after navigation is created. + Build navigation structure from split sections. + """ + from mkdocs.structure.nav import Navigation + + # Read README.md again to get section titles in order + readme_path = Path(config['docs_dir']).parent / 'README.md' + + if not readme_path.exists(): + return nav + + with open(readme_path, 'r', encoding='utf-8') as f: + readme_content = f.read() + + sections = split_readme_content(readme_content) + + # Build navigation items + nav_items = [] + for section in sections: + filename = f"{section['slug']}.md" + + # Find corresponding file + file_obj = None + for f in files: + if f.src_path == filename: + file_obj = f + break + + if file_obj: + from mkdocs.structure.pages import Page + page = Page(section['title'], file_obj, config) + nav_items.append(page) + + # Create new navigation + nav.items = nav_items + nav.pages = nav_items + + return nav + + +def on_post_build(config): + """ + Called after site is built. + Clean up temporary markdown files created during build. + """ + docs_dir = Path(config['docs_dir']) + + # Remove all .md files in docs_dir (they were created temporarily) + for md_file in docs_dir.glob('*.md'): + md_file.unlink() + logger.debug(f"Cleaned up temporary file: {md_file}") diff --git a/website/mkdocs.yml b/website/mkdocs.yml new file mode 100644 index 0000000..71d4187 --- /dev/null +++ b/website/mkdocs.yml @@ -0,0 +1,145 @@ +# Copyright (C) 2026 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +# Site metadata +site_name: kortex-cli Documentation +site_description: Command-line interface for launching and managing AI agents (Claude Code, Goose, Cursor) with custom configurations +site_author: Red Hat +site_url: https://kortex-hub.github.io/kortex-cli/ + +# Repository configuration +repo_name: kortex-hub/kortex-cli +repo_url: https://github.com/kortex-hub/kortex-cli +edit_uri: edit/main/ + +# Copyright +copyright: Copyright © 2026 Red Hat, Inc. Licensed under Apache License 2.0 + +# Documentation source +docs_dir: docs +site_dir: site + +# Hooks for automatic README.md splitting +hooks: + - hooks.py + +# Theme configuration +theme: + name: material + language: en + palette: + - media: "(prefers-color-scheme: light)" + scheme: default + primary: red + accent: red + toggle: + icon: material/brightness-7 + name: Switch to dark mode + - media: "(prefers-color-scheme: dark)" + scheme: slate + primary: red + accent: red + toggle: + icon: material/brightness-4 + name: Switch to light mode + font: + text: Red Hat Text + code: Red Hat Mono + features: + - navigation.instant + - navigation.tracking + - navigation.top + - navigation.footer + - search.suggest + - search.highlight + - search.share + - content.code.copy + - content.code.annotate + - content.action.edit + - toc.follow + icon: + repo: fontawesome/brands/github + +# Plugins for SEO and functionality +plugins: + - search: + separator: '[\s\-,:!=\[\]()"/]+|(?!\b)(?=[A-Z][a-z])|\.(?!\d)|&[lg]t;' + - minify: + minify_html: true + minify_js: true + minify_css: true + htmlmin_opts: + remove_comments: true + cache_safe: true + - git-revision-date-localized: + enable_creation_date: true + type: timeago + fallback_to_build_date: true + +# Markdown extensions for better rendering +markdown_extensions: + - abbr + - admonition + - attr_list + - def_list + - footnotes + - md_in_html + - tables + - toc: + permalink: true + permalink_title: Anchor link to this section + toc_depth: 3 + - pymdownx.arithmatex: + generic: true + - pymdownx.betterem: + smart_enable: all + - pymdownx.caret + - pymdownx.details + - pymdownx.emoji: + emoji_index: !!python/name:material.extensions.emoji.twemoji + emoji_generator: !!python/name:material.extensions.emoji.to_svg + - pymdownx.highlight: + anchor_linenums: true + line_spans: __span + pygments_lang_class: true + - pymdownx.inlinehilite + - pymdownx.keys + - pymdownx.mark + - pymdownx.smartsymbols + - pymdownx.superfences: + custom_fences: + - name: mermaid + class: mermaid + format: !!python/name:pymdownx.superfences.fence_code_format + - pymdownx.tabbed: + alternate_style: true + - pymdownx.tasklist: + custom_checkbox: true + - pymdownx.tilde + +# SEO and social media optimization +extra: + social: + - icon: fontawesome/brands/github + link: https://github.com/kortex-hub/kortex-cli + name: kortex-cli on GitHub + generator: false # Don't show "Made with Material for MkDocs" + +# Additional CSS and JavaScript (if needed in future) +# extra_css: +# - assets/extra.css +# extra_javascript: +# - assets/extra.js diff --git a/website/requirements.txt b/website/requirements.txt new file mode 100644 index 0000000..73a8eac --- /dev/null +++ b/website/requirements.txt @@ -0,0 +1,31 @@ +# Copyright (C) 2026 Red Hat, Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 + +# MkDocs core - static site generator +mkdocs==1.6.1 + +# Material theme - modern, responsive design with built-in SEO features +mkdocs-material==9.5.44 + +# Minify plugin - compress HTML/CSS/JS for better performance +mkdocs-minify-plugin==0.8.0 + +# Git revision date plugin - show last updated timestamps +mkdocs-git-revision-date-localized-plugin==1.3.0 + +# Dependencies for social cards and icons (used by Material theme) +pillow==11.1.0 +cairosvg==2.7.1 From b4efcacee832a8ce3116da93acf60a5e9c32f3bf Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Fri, 27 Mar 2026 11:57:26 +0000 Subject: [PATCH 2/6] fix(website): remove git-revision-date plugin causing build warnings The git-revision-date-localized plugin was generating warnings in strict mode because the dynamically generated markdown files have no git history. Since we split README.md at build time, revision dates for individual pages don't make sense anyway. Signed-off-by: Philippe Martin --- website/mkdocs.yml | 4 ---- website/requirements.txt | 3 --- 2 files changed, 7 deletions(-) diff --git a/website/mkdocs.yml b/website/mkdocs.yml index 71d4187..847afb6 100644 --- a/website/mkdocs.yml +++ b/website/mkdocs.yml @@ -84,10 +84,6 @@ plugins: htmlmin_opts: remove_comments: true cache_safe: true - - git-revision-date-localized: - enable_creation_date: true - type: timeago - fallback_to_build_date: true # Markdown extensions for better rendering markdown_extensions: diff --git a/website/requirements.txt b/website/requirements.txt index 73a8eac..11937ff 100644 --- a/website/requirements.txt +++ b/website/requirements.txt @@ -23,9 +23,6 @@ mkdocs-material==9.5.44 # Minify plugin - compress HTML/CSS/JS for better performance mkdocs-minify-plugin==0.8.0 -# Git revision date plugin - show last updated timestamps -mkdocs-git-revision-date-localized-plugin==1.3.0 - # Dependencies for social cards and icons (used by Material theme) pillow==11.1.0 cairosvg==2.7.1 From e87378377793ec4d138d665bf893159766c1dc3a Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Fri, 27 Mar 2026 12:16:02 +0000 Subject: [PATCH 3/6] fix(website): use consistent README path in on_nav hook The on_nav function was using a different path calculation than on_files, which could cause it to fail finding README.md in some scenarios. Now both functions use the same resolved path calculation for consistency. Signed-off-by: Philippe Martin --- website/hooks.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/website/hooks.py b/website/hooks.py index a2e2e6f..bfca2e6 100644 --- a/website/hooks.py +++ b/website/hooks.py @@ -158,7 +158,9 @@ def on_nav(nav, config, files): from mkdocs.structure.nav import Navigation # Read README.md again to get section titles in order - readme_path = Path(config['docs_dir']).parent / 'README.md' + # Use same path calculation as on_files for consistency + docs_dir_abs = Path(config['docs_dir']).resolve() + readme_path = docs_dir_abs.parent.parent / 'README.md' if not readme_path.exists(): return nav From 7d4233fc57fb3f30387cf835849d2abf92ca1a47 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Fri, 27 Mar 2026 12:16:48 +0000 Subject: [PATCH 4/6] fix(deps): update pillow and cairosvg to patched versions Updated dependencies to address high-severity vulnerabilities: - pillow: 11.1.0 -> 12.1.1 (security patches) - cairosvg: 2.7.1 -> 2.9.0 (security patches) These dependencies are used by Material theme for social card generation. Signed-off-by: Philippe Martin --- website/requirements.txt | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/website/requirements.txt b/website/requirements.txt index 11937ff..efc7583 100644 --- a/website/requirements.txt +++ b/website/requirements.txt @@ -24,5 +24,5 @@ mkdocs-material==9.5.44 mkdocs-minify-plugin==0.8.0 # Dependencies for social cards and icons (used by Material theme) -pillow==11.1.0 -cairosvg==2.7.1 +pillow==12.1.1 +cairosvg==2.9.0 From 5959645a44b67ea5b224cddd3b4809e85e1ae7b7 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Fri, 27 Mar 2026 12:23:16 +0000 Subject: [PATCH 5/6] fix(website): point edit links to README.md source Changed edit_uri from 'edit/main/' to 'blob/main/README.md' so that 'Edit this page' links point to the actual source file (README.md) instead of non-existent generated files like 'introduction.md'. Since all documentation pages are dynamically generated from README.md by hooks.py, all edit links should point to the single source of truth. Signed-off-by: Philippe Martin --- website/mkdocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/mkdocs.yml b/website/mkdocs.yml index 847afb6..b7bcaf9 100644 --- a/website/mkdocs.yml +++ b/website/mkdocs.yml @@ -23,7 +23,7 @@ site_url: https://kortex-hub.github.io/kortex-cli/ # Repository configuration repo_name: kortex-hub/kortex-cli repo_url: https://github.com/kortex-hub/kortex-cli -edit_uri: edit/main/ +edit_uri: blob/main/README.md # Copyright copyright: Copyright © 2026 Red Hat, Inc. Licensed under Apache License 2.0 From b11bc07fc9645d02642d52aed219406d32726061 Mon Sep 17 00:00:00 2001 From: Philippe Martin Date: Fri, 27 Mar 2026 12:34:51 +0000 Subject: [PATCH 6/6] fix(website): remove duplicate heading on chapter pages Don't include the ## heading in page content since MkDocs Material theme already displays the page title at the top. This prevents the heading from appearing twice on each page. Signed-off-by: Philippe Martin --- website/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/website/hooks.py b/website/hooks.py index bfca2e6..72d47e5 100644 --- a/website/hooks.py +++ b/website/hooks.py @@ -81,7 +81,7 @@ def split_readme_content(content): current_section = { 'title': title, 'slug': slug, - 'content': [line] # Include the heading + 'content': [] # Don't include the heading (MkDocs shows it as page title) } sections.append(current_section) else: