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 }}
+
+