diff --git a/docs/css/custom.css b/docs/css/custom.css index fe9b85528a..f53104744f 100644 --- a/docs/css/custom.css +++ b/docs/css/custom.css @@ -158,7 +158,7 @@ body { } .md-typeset h1 { - margin: 0 0 1rem; + margin: 0 0 0.5rem; font-size: 24px; line-height: 34px; } @@ -649,3 +649,94 @@ div.path { [hidden] { display: none !important; } + +.page-actions { + display: none; + gap: 0; + margin: 12px 0 0 0; + padding-bottom: 6px; + border-bottom: 1px solid #e3e8ee; +} + +.page-action-btn { + display: inline-flex; + align-items: center; + gap: 6px; + padding: 4px 12px; + background: transparent; + border: none; + text-decoration: none; + color: #425466; + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: color 0.15s ease; + white-space: nowrap; + position: relative; +} + +.page-action-btn:hover { + background: transparent; + text-decoration: none; + color: #0a2540; +} + +.page-action-btn:active { + color: #0a2540; +} + +.page-action-btn:link, +.page-action-btn:visited { + color: #425466; + text-decoration: none; +} + +.page-action-btn:hover:link, +.page-action-btn:hover:visited { + color: #0a2540; +} + +.page-action-btn+.page-action-btn::before { + content: ''; + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + height: 16px; + width: 1px; + background: #e3e8ee; +} + +.page-action-btn svg { + width: 14px; + height: 14px; + fill: currentColor; + flex-shrink: 0; +} + +.page-action-btn.external::after, +.page-action-btn[rel="nofollow"]::after { + display: none !important; +} + +@media (max-width: 768px) { + .page-actions { + gap: 0; + } +} + +.page-actions--visible { + display: flex; +} + +.page-action-btn--success { + background: #d4edda !important; + border-color: #c3e6cb !important; + color: #155724 !important; +} + +.page-action-btn--info { + background: #d1ecf1 !important; + border-color: #bee5eb !important; + color: #0c5460 !important; +} diff --git a/docs/personalization/api_reference/api_reference.md b/docs/personalization/api_reference/api_reference.md index 7910d07941..add35808d6 100644 --- a/docs/personalization/api_reference/api_reference.md +++ b/docs/personalization/api_reference/api_reference.md @@ -1,6 +1,7 @@ --- description: Explore Personalization API sets that let you manage item data, track events, combine tracking data with users and render recommendations. page_type: landing_page +exclude_from_llmstxt: true --- # Personalization API diff --git a/docs/personalization/api_reference/content_api.md b/docs/personalization/api_reference/content_api.md index 2477740618..6cdde12523 100644 --- a/docs/personalization/api_reference/content_api.md +++ b/docs/personalization/api_reference/content_api.md @@ -1,5 +1,6 @@ --- description: Personalization server can use external information about the items. Use HTTP methods to create, update or get items from the data store. +exclude_from_llmstxt: true --- # Content API diff --git a/docs/personalization/api_reference/recommendation_api.md b/docs/personalization/api_reference/recommendation_api.md index 42727e5e43..bb5a386043 100644 --- a/docs/personalization/api_reference/recommendation_api.md +++ b/docs/personalization/api_reference/recommendation_api.md @@ -1,6 +1,7 @@ --- description: Use HTTP GET request method to render recommendations. month_change: false +exclude_from_llmstxt: true --- # Recommendation API diff --git a/docs/personalization/api_reference/tracking_api.md b/docs/personalization/api_reference/tracking_api.md index 36d7c5058c..0aeaac3a73 100644 --- a/docs/personalization/api_reference/tracking_api.md +++ b/docs/personalization/api_reference/tracking_api.md @@ -1,5 +1,6 @@ --- description: Allows to track items based on an ID. It covers many content types with the same ID configured for tracking. +exclude_from_llmstxt: true --- # Tracking API diff --git a/docs/personalization/api_reference/user_api.md b/docs/personalization/api_reference/user_api.md index 710baa385a..04500defb4 100644 --- a/docs/personalization/api_reference/user_api.md +++ b/docs/personalization/api_reference/user_api.md @@ -1,5 +1,6 @@ --- description: Use HTTP methods to correlate metadata with user data and combine users into clusters of certain type. +exclude_from_llmstxt: true --- # User API diff --git a/docs/personalization/attribute_search_in_elasticsearch.md b/docs/personalization/attribute_search_in_elasticsearch.md index 01c2b6a710..da6eb2d6fe 100644 --- a/docs/personalization/attribute_search_in_elasticsearch.md +++ b/docs/personalization/attribute_search_in_elasticsearch.md @@ -1,5 +1,6 @@ --- description: Attribute search uses Elasticsearch database to display dynamically taken values in scenario and model previews. +exclude_from_llmstxt: true --- # Attribute search in Elasticsearch database diff --git a/docs/personalization/enable_personalization.md b/docs/personalization/enable_personalization.md index 7d3d2b4f17..2b46e89f3c 100644 --- a/docs/personalization/enable_personalization.md +++ b/docs/personalization/enable_personalization.md @@ -1,6 +1,7 @@ --- description: Configure your project files to enable Personalization and set up items you want to track. month_change: false +exclude_from_llmstxt: true --- # Enable Personalization diff --git a/docs/personalization/how_it_works.md b/docs/personalization/how_it_works.md index ee28481dce..5038284a70 100644 --- a/docs/personalization/how_it_works.md +++ b/docs/personalization/how_it_works.md @@ -1,5 +1,6 @@ --- description: Integrate recommendation service into your website. +exclude_from_llmstxt: true --- # How Personalization works diff --git a/docs/personalization/importing_historical_user_tracking_data.md b/docs/personalization/importing_historical_user_tracking_data.md index fd655a8e59..c3e3921f95 100644 --- a/docs/personalization/importing_historical_user_tracking_data.md +++ b/docs/personalization/importing_historical_user_tracking_data.md @@ -1,5 +1,6 @@ --- description: Use historical user tracking data to build user profiles and generate better recommendations. +exclude_from_llmstxt: true --- # Importing historical user tracking data diff --git a/docs/personalization/integrate_recommendation_service.md b/docs/personalization/integrate_recommendation_service.md index a2049f526c..9d38c2e394 100644 --- a/docs/personalization/integrate_recommendation_service.md +++ b/docs/personalization/integrate_recommendation_service.md @@ -1,6 +1,7 @@ --- description: Integrate recommendation service into your website. month_change: false +exclude_from_llmstxt: true --- # Integrate recommendation service diff --git a/docs/personalization/legacy_recommendation_api.md b/docs/personalization/legacy_recommendation_api.md index 5551d4323b..1886fbc792 100644 --- a/docs/personalization/legacy_recommendation_api.md +++ b/docs/personalization/legacy_recommendation_api.md @@ -1,5 +1,6 @@ --- description: An old method of fetching recommendations from the system using recommendation requests. +exclude_from_llmstxt: true --- # Legacy Recommendation API diff --git a/docs/personalization/personalization.md b/docs/personalization/personalization.md index 68ba2eafda..3a29c7fb9f 100644 --- a/docs/personalization/personalization.md +++ b/docs/personalization/personalization.md @@ -1,6 +1,7 @@ --- description: Personalization tracks consumed content and suggests targeted content to your website visitors. page_type: landing_page +exclude_from_llmstxt: true --- # Personalization diff --git a/docs/personalization/personalization_guide.md b/docs/personalization/personalization_guide.md index 0bb32baff3..ef8d041ea5 100644 --- a/docs/personalization/personalization_guide.md +++ b/docs/personalization/personalization_guide.md @@ -1,5 +1,6 @@ --- description: Discover Personalization - a cloud-based service that tracks and analyzes customer behaviors. +exclude_from_llmstxt: true --- # Personalization product guide diff --git a/docs/personalization/recommendation_integration.md b/docs/personalization/recommendation_integration.md index 769f3f1671..d53c930419 100644 --- a/docs/personalization/recommendation_integration.md +++ b/docs/personalization/recommendation_integration.md @@ -1,5 +1,6 @@ --- description: Methods for REST call with Personalization server. +exclude_from_llmstxt: true --- # Recommendation integration diff --git a/docs/personalization/tracking_integration.md b/docs/personalization/tracking_integration.md index 3ab8b63542..1ae6fa5f7b 100644 --- a/docs/personalization/tracking_integration.md +++ b/docs/personalization/tracking_integration.md @@ -1,6 +1,7 @@ --- description: See the methods of event tracking integration using tracking from server or from client-side. month_change: false +exclude_from_llmstxt: true --- # Tracking integration diff --git a/docs/personalization/tracking_with_ibexa-tracker.md b/docs/personalization/tracking_with_ibexa-tracker.md index 8357133ab7..11bacee7c9 100644 --- a/docs/personalization/tracking_with_ibexa-tracker.md +++ b/docs/personalization/tracking_with_ibexa-tracker.md @@ -1,6 +1,7 @@ --- description: Integrate tracking with a Google-style JavaScript. month_change: false +exclude_from_llmstxt: true --- # Track events with ibexa-tracker.js diff --git a/hooks.py b/hooks.py new file mode 100644 index 0000000000..51e9c8e117 --- /dev/null +++ b/hooks.py @@ -0,0 +1,33 @@ +""" +MkDocs hooks for Ibexa developer documentation. + +Automatically keeps the llmstxt plugin's ``sections`` config in sync with the ``nav`` defined in ``mkdocs.yml`` +""" + +from __future__ import annotations + +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from mkdocs.config.defaults import MkDocsConfig + + +def on_config(config: "MkDocsConfig") -> None: + """Populate llmstxt sections from nav before the build starts. + + The llmstxt plugin reads ``config.sections`` in its ``on_files`` event, + which fires after ``on_config``, so injecting here is the right place. + """ + nav = config.get("nav") + if not nav: + return + + llmstxt = config["plugins"].get("llmstxt") + if llmstxt is None: + return + + from pathlib import Path + from update_llmstxt_config import convert_nav_to_llmstxt_sections + + docs_dir = Path(config["docs_dir"]) + llmstxt.config.sections = convert_nav_to_llmstxt_sections(nav, docs_dir) diff --git a/llmstxt_preprocess.py b/llmstxt_preprocess.py new file mode 100644 index 0000000000..37341f632e --- /dev/null +++ b/llmstxt_preprocess.py @@ -0,0 +1,241 @@ +import html as html_module +import os +import re +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from bs4 import BeautifulSoup + +PILL_CLASS_TO_EDITION = { + "pill--lts-update": "LTS Update", + "pill--experience": "Experience", + "pill--commerce": "Commerce", + "pill--headless": "Headless", +} + +FRONTMATTER_EDITION_DISPLAY = { + "lts-update": "LTS Update", + "experience": "Experience", + "commerce": "Commerce", + "headless": "Headless", +} + + +def preprocess(soup: "BeautifulSoup", output: str) -> None: + """ + Preprocess HTML to improve markdown conversion. + + Runs with autoclean disabled so we can control the order: + 1. Expand tabbed sets with labels before autoclean removes tabbed-labels. + 2. Run autoclean-equivalent cleanup. + 3. Inject frontmatter edition info read from the source file. + 4. Replace inline edition badge spans with readable text. + 5. Remove release notes filter UI. + 6. Convert card macros to markdown lists. + """ + _process_tabbed_sets(soup) + _autoclean(soup) + _process_frontmatter_editions(soup, output) + _process_inline_pills(soup) + _process_release_notes_filters(soup) + _process_cards(soup) + + +# --------------------------------------------------------------------------- +# Tabbed sets +# --------------------------------------------------------------------------- + +def _process_tabbed_sets(soup: "BeautifulSoup") -> None: + """Prepend each tab label as bold text before its content block.""" + for tabbed_set in soup.find_all("div", class_="tabbed-set"): + labels_div = tabbed_set.find("div", class_="tabbed-labels") + content_div = tabbed_set.find("div", class_="tabbed-content") + + if not labels_div or not content_div: + tabbed_set.unwrap() + continue + + labels = [label.get_text(strip=True) for label in labels_div.find_all("label")] + blocks = content_div.find_all("div", class_="tabbed-block", recursive=False) + + wrapper = soup.new_tag("div") + for i, block in enumerate(blocks): + if i < len(labels): + label_tag = soup.new_tag("p") + strong = soup.new_tag("strong") + strong.string = labels[i] + label_tag.append(strong) + wrapper.append(label_tag) + for child in list(block.children): + wrapper.append(child.extract()) + + tabbed_set.replace_with(wrapper) + + +# --------------------------------------------------------------------------- +# Autoclean equivalent (mirrors mkdocs-llmstxt autoclean, minus tabbed-labels) +# --------------------------------------------------------------------------- + +def _autoclean(soup: "BeautifulSoup") -> None: + """Replicate the plugin's autoclean so we can run it after tab processing.""" + from bs4 import BeautifulSoup as Soup, NavigableString + + def _should_remove(tag) -> bool: + if tag.name in {"img", "svg"}: + return True + if tag.name == "a" and tag.find("img"): + return True + classes = tag.get("class") or () + if tag.name == "a" and "headerlink" in classes: + return True + if "twemoji" in classes: + return True + # tabbed-labels are already consumed by _process_tabbed_sets, but + # handle any stragglers defensively. + if "tabbed-labels" in classes: + return True + return False + + for element in soup.find_all(_should_remove): + element.decompose() + + for element in soup.find_all("autoref"): + element.replace_with(NavigableString(element.get_text())) + + for element in soup.find_all("div", attrs={"class": "doc-md-description"}): + element.replace_with(NavigableString(element.get_text().strip())) + + for element in soup.find_all("span", attrs={"class": "doc-labels"}): + element.decompose() + + for element in soup.find_all("table", attrs={"class": "highlighttable"}): + code_elem = element.find("code") + if code_elem: + element.replace_with( + Soup(f"
{html_module.escape(code_elem.get_text())}
", "html.parser") + ) + + +# --------------------------------------------------------------------------- +# Frontmatter editions +# --------------------------------------------------------------------------- + +def _get_editions_from_source(output: str) -> list: + """Return edition display names read from the source markdown frontmatter.""" + # Map output path back to source: .../site/some/page/index.md -> .../docs/some/page.md + if "/site/" not in output: + return [] + + path = output.replace("/site/", "/docs/", 1) + if path.endswith("/index.md"): + path = path[: -len("/index.md")] + ".md" + + if not os.path.exists(path): + return [] + + try: + from mkdocs.utils import meta as mkdocs_meta + + with open(path, encoding="utf-8") as f: + content = f.read() + _, frontmatter = mkdocs_meta.get_data(content) + + edition = frontmatter.get("edition") + editions = frontmatter.get("editions") or [] + + if isinstance(edition, str): + all_editions = [edition] + (editions if isinstance(editions, list) else []) + elif isinstance(edition, list): + all_editions = edition + (editions if isinstance(editions, list) else []) + else: + all_editions = editions if isinstance(editions, list) else [] + + return [FRONTMATTER_EDITION_DISPLAY.get(e, e) for e in all_editions if e] + except Exception: + return [] + + +def _process_frontmatter_editions(soup: "BeautifulSoup", output: str) -> None: + """Inject 'Editions: X, Y' paragraph after the first h1.""" + editions = _get_editions_from_source(output) + if not editions: + return + + p = soup.new_tag("p") + p.string = "Editions: " + ", ".join(editions) + + h1 = soup.find("h1") + if h1: + h1.insert_after(p) + else: + soup.insert(0, p) + + +# --------------------------------------------------------------------------- +# Inline edition badge spans (from snippet includes) +# --------------------------------------------------------------------------- + +def _process_inline_pills(soup: "BeautifulSoup") -> None: + """Replace inline edition pill spans with readable text, e.g. '(Experience)'.""" + for span in soup.find_all("span", class_="pill--inline"): + span_classes = span.get("class", []) + for pill_cls, edition_name in PILL_CLASS_TO_EDITION.items(): + if pill_cls in span_classes: + span.replace_with(soup.new_string(f" ({edition_name})")) + break + + +# --------------------------------------------------------------------------- +# Release notes filters +# --------------------------------------------------------------------------- + +def _process_release_notes_filters(soup: "BeautifulSoup") -> None: + """Remove interactive release-notes filter UI elements.""" + for container in soup.find_all("div", class_="release-notes-filters"): + container.decompose() + + +# --------------------------------------------------------------------------- +# Card macros +# --------------------------------------------------------------------------- + +def _process_cards(soup: "BeautifulSoup") -> None: + """Convert card macro HTML structures into markdown-friendly lists with links.""" + for cards_div in soup.find_all("div", class_=lambda c: c and c.startswith("cards ")): + card_wrappers = cards_div.find_all("div", class_="card-wrapper") + + if not card_wrappers: + continue + + ul = soup.new_tag("ul") + + for card_wrapper in card_wrappers: + link = card_wrapper.find("a", class_="card") + if not link: + continue + + href = link.get("href", "") + if href.startswith("//"): + href = "https:" + href + + title_elem = link.find("p", class_="title") + description_elem = link.find("p", class_="description") + + if not title_elem: + continue + + title = title_elem.get_text(strip=True) + description = description_elem.get_text(strip=True) if description_elem else "" + + li = soup.new_tag("li") + link_tag = soup.new_tag("a", href=href) + link_tag.string = title + li.append(link_tag) + + if description: + li.append(soup.new_string(" - ")) + li.append(soup.new_string(description)) + + ul.append(li) + + cards_div.replace_with(ul) diff --git a/mkdocs.yml b/mkdocs.yml index a2082cc594..3f5f242903 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -2,6 +2,7 @@ INHERIT: plugins.yml site_name: Developer Documentation repo_url: https://github.com/ibexa/documentation-developer +edit_uri: edit/5.0/docs/ site_url: https://doc.ibexa.co/en/latest/ copyright: "Copyright 1999-2026 Ibexa AS and others" validation: @@ -1092,3 +1093,6 @@ markdown_extensions: custom_checkbox: true - pymdownx.tilde - pymdownx.details + +hooks: + - hooks.py diff --git a/plugins.yml b/plugins.yml index 77c3fd4879..6ffe173de5 100644 --- a/plugins.yml +++ b/plugins.yml @@ -586,3 +586,13 @@ plugins: 'ai_actions/install_ai_actions.md': 'ai_actions/configure_ai_actions.md' 'discounts/install_discounts.md': 'discounts/configure_discounts.md' 'content_management/collaborative_editing/install_collaborative_editing.md': 'content_management/collaborative_editing/configure_collaborative_editing.md' + + - llmstxt: + preprocess: llmstxt_preprocess.py + + autoclean: false + full_output: llms-full.txt + sections: + # Built automatically when running MkDocs build based on MkDocs nav configuration, don't change + Ibexa DXP developer documentation: + - index.md diff --git a/requirements.txt b/requirements.txt index c03e3d1a35..688d9446a1 100644 --- a/requirements.txt +++ b/requirements.txt @@ -7,3 +7,4 @@ mkdocs-macros-plugin==1.3.7 mkdocs-redirects==1.2.2 mkdocs-autolinks-plugin==0.7.1 Jinja2==3.1.6 +mkdocs-llmstxt diff --git a/theme/assets/page-actions.js b/theme/assets/page-actions.js new file mode 100644 index 0000000000..51faa495f1 --- /dev/null +++ b/theme/assets/page-actions.js @@ -0,0 +1,55 @@ +/** + * Page Actions JavaScript + * Handles functionality for page action buttons (Copy as Markdown, View as Markdown, Edit on GitHub) + */ + +async function copyPageForLLM() { + const mdPath = document.querySelector('meta[name="markdown-path"]').content; + + try { + const response = await fetch(mdPath); + if (!response.ok) { + throw new Error(`HTTP error! status: ${response.status}`); + } + const markdownContent = await response.text(); + await navigator.clipboard.writeText(markdownContent); + showButtonFeedback('success', 'Copied!', '📋'); + } catch (error) { + console.error('Failed to copy content:', error); + window.open(mdPath, '_blank'); + showButtonFeedback('info', 'Opened in tab', '🔗'); + } +} + +function showButtonFeedback(type, message, icon) { + const button = document.querySelector('button[onclick="copyPageForLLM()"]'); + if (!button) return; + + const originalHTML = button.innerHTML; + button.innerHTML = `${icon} ${message}`; + + if (type === 'success') { + button.classList.add('page-action-btn--success'); + } else if (type === 'error') { + button.classList.add('page-action-btn--error'); + } else if (type === 'info') { + button.classList.add('page-action-btn--info'); + } + + setTimeout(() => { + button.innerHTML = originalHTML; + button.classList.remove('page-action-btn--success', 'page-action-btn--error', 'page-action-btn--info'); + }, 2000); +} + +document.addEventListener('DOMContentLoaded', function() { + console.log('Page actions initialized'); + + const pageActions = document.getElementById('page-actions'); + const firstH1 = document.querySelector('.bootstrap-iso h1, h1'); + + if (pageActions && firstH1) { + firstH1.insertAdjacentElement('afterend', pageActions); + pageActions.classList.add('page-actions--visible'); + } +}); diff --git a/theme/main.html b/theme/main.html index 66ce7dc87d..e7051c6f61 100644 --- a/theme/main.html +++ b/theme/main.html @@ -13,6 +13,13 @@ + {% if config.repo_url and page.edit_url %} + + + {% endif %} + + + {% endblock %} {% block site_nav %} {% if nav %} @@ -75,7 +82,36 @@ {% endif %} {% include "partials/eol_warning.html" %} - {{ page.content }} + +
+ {{ page.content }} + + + {% if config.repo_url and page.edit_url and not page.is_homepage %} +
+ + + + + + + View as Markdown + + + + + + Edit on GitHub + +
+ {% endif %} +
+ {% include "partials/tags.html" %} {% endblock %} diff --git a/theme/partials/header.html b/theme/partials/header.html index ba5ca0931d..80a4345297 100644 --- a/theme/partials/header.html +++ b/theme/partials/header.html @@ -20,10 +20,5 @@ {% include ".icons/material/magnify.svg" %} {% include "partials/search.html" %} - {% if config.repo_url %} -
- {% include "partials/source.html" %} -
- {% endif %} diff --git a/theme/partials/source.html b/theme/partials/source.html deleted file mode 100644 index 76e885dc14..0000000000 --- a/theme/partials/source.html +++ /dev/null @@ -1,7 +0,0 @@ -{% import "partials/language.html" as lang with context %} - -
- {% include ".icons/fontawesome/brands/github-alt.svg" %} -
- View on GitHub -
diff --git a/update_llmstxt_config.py b/update_llmstxt_config.py new file mode 100644 index 0000000000..8b6d35d4b3 --- /dev/null +++ b/update_llmstxt_config.py @@ -0,0 +1,150 @@ +#!/usr/bin/env python3 +""" +Update the llmstxt plugin configuration in plugins.yml based on mkdocs.yml nav structure. +This script converts the mkdocs navigation into a format suitable for the llmstxt plugin. + +Files can be excluded from the llmstxt output by adding the following to their YAML frontmatter: + + exclude_from_llmstxt: true + +Files without this property are included by default. +""" + +import re +import yaml +from pathlib import Path + + +def read_frontmatter(file_path): + """ + Parse YAML frontmatter from a markdown file. + Returns a dict of frontmatter values, or an empty dict if none is found. + Handles files that begin with HTML comments before the frontmatter block. + """ + try: + content = Path(file_path).read_text(encoding='utf-8') + except (OSError, UnicodeDecodeError): + return {} + + # Strip leading HTML comments (e.g. ) + content = re.sub(r'\A(\s*\s*)+', '', content, flags=re.DOTALL) + + if not content.startswith('---'): + return {} + + end = content.find('\n---', 3) + if end == -1: + return {} + + try: + return yaml.safe_load(content[3:end]) or {} + except yaml.YAMLError: + return {} + + +def is_excluded(file_path, docs_dir): + """ + Return True if the file has 'exclude_from_llmstxt: true' in its frontmatter. + file_path is relative to docs_dir (as written in mkdocs nav). + """ + full_path = Path(docs_dir) / file_path + fm = read_frontmatter(full_path) + return fm.get('exclude_from_llmstxt', False) is True + + +def convert_nav_to_llmstxt_sections(nav_list, docs_dir): + """ + Convert mkdocs nav list to llmstxt sections format. + Returns a dict mapping top-level section names to flat lists of file paths. + Files with 'exclude_from_llmstxt: true' in their frontmatter are skipped. + Sections that contain no remaining files are omitted. + """ + sections = {} + + def extract_files(item): + """Recursively extract included file paths from a nav item.""" + files = [] + if isinstance(item, str): + if not item.endswith('.html') and not is_excluded(item, docs_dir): + files.append(item) + elif isinstance(item, list): + for subitem in item: + files.extend(extract_files(subitem)) + elif isinstance(item, dict): + for value in item.values(): + if isinstance(value, str): + if not value.endswith('.html') and not is_excluded(value, docs_dir): + files.append(value) + elif isinstance(value, list): + for subitem in value: + files.extend(extract_files(subitem)) + return files + + for item in nav_list: + if isinstance(item, dict): + for section_name, section_content in item.items(): + files = extract_files({section_name: section_content}) + if files: + sections[section_name] = files + elif isinstance(item, str): + if not item.endswith('.html') and not is_excluded(item, docs_dir): + sections.setdefault('Ibexa Developer Documentation', []).append(item) + + return sections + + +def update_plugins_yml(plugins_path, mkdocs_path): + """ + Update the llmstxt plugin configuration in plugins.yml based on mkdocs.yml nav. + """ + with open(plugins_path, 'r') as f: + plugins_data = yaml.safe_load(f) + + with open(mkdocs_path, 'r') as f: + mkdocs_data = yaml.safe_load(f) + + # docs/ directory is resolved relative to mkdocs.yml location + docs_dir = Path(mkdocs_path).parent / mkdocs_data.get('docs_dir', 'docs') + + nav = mkdocs_data.get('nav', []) + new_sections = convert_nav_to_llmstxt_sections(nav, docs_dir) + + plugins_list = plugins_data.get('plugins', []) + for plugin in plugins_list: + if isinstance(plugin, dict) and 'llmstxt' in plugin: + plugin['llmstxt']['sections'] = new_sections + print(f"✓ Updated llmstxt plugin configuration") + print(f" Total sections: {len(new_sections)}") + break + else: + print("✗ llmstxt plugin not found in plugins.yml") + return False + + with open(plugins_path, 'w') as f: + yaml.dump(plugins_data, f, default_flow_style=False, sort_keys=False, + allow_unicode=True, width=120) + + print(f"✓ Updated {plugins_path}") + return True + + +if __name__ == '__main__': + script_dir = Path(__file__).parent + plugins_path = script_dir / 'plugins.yml' + mkdocs_path = script_dir / 'mkdocs.yml' + + if not plugins_path.exists(): + print(f"✗ plugins.yml not found at {plugins_path}") + exit(1) + + if not mkdocs_path.exists(): + print(f"✗ mkdocs.yml not found at {mkdocs_path}") + exit(1) + + print("Updating llmstxt configuration...") + print(f"Reading from: {mkdocs_path}") + print(f"Updating: {plugins_path}") + print() + + success = update_plugins_yml(plugins_path, mkdocs_path) + exit(0 if success else 1)