diff --git a/.github/workflows/link_check.yaml b/.github/workflows/link_check.yaml new file mode 100644 index 0000000000..363f60ced1 --- /dev/null +++ b/.github/workflows/link_check.yaml @@ -0,0 +1,91 @@ +name: "Check documentation links" + +on: + push: + branches: + - master + - "[0-9]+.[0-9]+" + workflow_dispatch: + inputs: + force_recheck: + description: "Clear lychee cache and recheck all links from scratch" + type: boolean + default: false + pull_request: ~ + schedule: + - cron: "0 6 * * *" + +jobs: + link-check: + runs-on: ubuntu-latest + strategy: + matrix: + python-version: [3.13] + + steps: + - uses: actions/checkout@v4 + + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v5 + with: + python-version: ${{ matrix.python-version }} + + - name: Install MkDocs dependencies + run: | + python -m pip install --upgrade pip + pip install -r requirements.txt + + - name: Build documentation + run: mkdocs build --strict + + - name: Clone versioned repositories for link remap + run: | + mkdir -p repositories + git clone --depth=1 --branch 4.6 https://github.com/ibexa/documentation-developer.git repositories/devdoc-4.6 & + git clone --depth=1 --branch 5.0 https://github.com/ibexa/documentation-developer.git repositories/devdoc-5.0 & + git clone --depth=1 --branch 4.6 https://github.com/ibexa/documentation-user.git repositories/userdoc-4.6 & + git clone --depth=1 --branch 5.0 https://github.com/ibexa/documentation-user.git repositories/userdoc-5.0 & + wait + + - name: Build versioned repositories for link remap + run: | + for dir in repositories/devdoc-4.6 repositories/devdoc-5.0 repositories/userdoc-4.6 repositories/userdoc-5.0; do + (cd "$dir" && pip install -q -r requirements.txt && mkdocs build --quiet) & + done + wait + + - name: Update remap paths for CI environment + run: | + sed -i "s|/Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker|$GITHUB_WORKSPACE|g" lychee.toml + + - name: Restore lychee cache + if: ${{ !inputs.force_recheck }} + uses: actions/cache@v4 + with: + path: .lycheecache + key: lychee-${{ github.ref_name }}-${{ hashFiles('lychee.toml') }} + restore-keys: | + lychee-${{ github.ref_name }}- + lychee- + + - name: Check links + uses: lycheeverse/lychee-action@v2 + with: + args: >- + --config lychee.toml + --cache + --cache-exclude-status "400.." + site + output: lychee-report.md + jobSummary: false + fail: true + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + + - name: Upload link-check report + if: always() + uses: actions/upload-artifact@v4 + with: + name: lychee-report + path: lychee-report.md + if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index d243a010bd..7acb48f49e 100644 --- a/.gitignore +++ b/.gitignore @@ -12,3 +12,5 @@ auth.json yarn.lock docs/css/*.map .deptrac.cache +.lycheecache +/repositories/ diff --git a/docs/js/custom.js b/docs/js/custom.js index d67814e91e..1a50125e76 100644 --- a/docs/js/custom.js +++ b/docs/js/custom.js @@ -5,7 +5,7 @@ $(document).ready(function() { const latestVersionNumber = '5.0'; // replace edit url - let branchName = 'master'; + let branchName = '5.0'; const branchNameRegexp = /\/en\/([a-z0-9-_.]*)\//g.exec(document.location.href); const eolVersions = window.eol_versions ?? []; @@ -21,7 +21,7 @@ $(document).ready(function() { } if (!/^\d+\.\d+$/.test(branchName) && branchName !== 'latest') { - branchName = 'master'; + branchName = '5.0'; } // Insert version into header links diff --git a/lychee.toml b/lychee.toml new file mode 100644 index 0000000000..7329a5b7f1 --- /dev/null +++ b/lychee.toml @@ -0,0 +1,168 @@ +############################# Display ############################# + +# Verbose program output +verbose = "error" + +# Output format +format = "markdown" + +# Path to report output file +output = "lychee-report.md" + +# Don't show interactive progress bar while checking links. +no_progress = false + +############################# Cache ############################### + +# Enable link caching to avoid re-checking identical links across runs. +cache = true + +# Discard cached results older than this duration. +max_cache_age = "1d" + +############################# Runtime ############################# + +# Maximum number of allowed redirects. Set to 0 to fail on any redirect — +# a redirect usually signals moved or reorganised content that should be +# updated at the source. +max_redirects = 0 + +# Maximum number of allowed retries before a link is declared dead. +max_retries = 3 + +# Minimum wait time in seconds between retries of failed requests. +retry_wait_time = 2 + +# Maximum number of concurrent link checks across all hosts. +max_concurrency = 8 + +############################# Requests ############################ + +# Website timeout from connect to response finished (seconds). +timeout = 20 + +# Comma-separated list of accepted status codes for valid links. +# 429 = Too Many Requests (rate-limited, treat as valid). +accept = ["200", "202", "429"] + +# Proceed for server connections considered insecure (invalid TLS). +insecure = false + +# Check https/http and file:// (used by remap rules below). +# Relative and root-relative internal links are still skipped because they +# don't match any scheme — internal links are validated by mkdocs build --strict. +scheme = ["https", "http", "file"] + +# Use HEAD requests — much faster than GET since no body is downloaded. +# Fragment checking requires GET, so include_fragments is disabled; +# internal anchor links are already validated by `mkdocs build --strict`. +method = "GET" + +# Mimic a browser to avoid Cloudflare bot-detection (403) on sites like doc.ibexa.co. +user_agent = "Mozilla/5.0 (compatible; lychee link checker)" + +# Do NOT check anchor fragments — requires full GET downloads for every URL. +include_fragments = true + +# Do NOT check links inside and
 blocks.
+include_verbatim = false
+
+#############################  Exclusions  ##########################
+
+# Exclude URLs from checking (treated as regular expressions).
+exclude = [
+    # LinkedIn blocks automated requests
+    "^https?://(www\\.)?linkedin\\.com",
+    # Localhost and loopback addresses
+    "^https?://localhost",
+    "^https?://127\\.0\\.0\\.1",
+    # Placeholder/example domains
+    "^https?://example\\.com",
+    # GitHub login/auth pages often rate-limit or redirect bots
+    "^https?://github\\.com/login",
+    # Known redirects (302) that are intentional
+    "^https://support\\.ibexa\\.co/",
+    "^https://redocly\\.com/redoc/",
+    "^https://updates\\.ibexa.co",
+    "^https://console\\.cloud\\.google\\.com",
+    # Versionless project root links (e.g. /projects/connect, /projects/userguide) appear in
+    # the MkDocs theme sidebar as cross-project navigation and are not real content links.
+    # The https form appears as absolute links; the file:// form appears after root_dir resolution
+    # of root-relative hrefs like /projects/connect in the built HTML.
+    "^https?://doc\\.ibexa\\.co/projects/[^/]+/?$",
+    "^file://.*?/site/projects/[^/]+/?$",
+]
+
+# Exclude these input paths from being scanned.
+exclude_path = [
+    # Search index, assets and sitemap contain no meaningful external links
+    "site/search/search_index.json",
+    "site/assets",
+    "site/404.html",
+    "site/sitemap.xml",
+    "site/robots.txt",
+    "site/update_and_migration/migrate_to_ibexa_dxp",
+    "site/update_and_migration/from_1.x_2.x/",
+]
+
+# Check the specified file extensions
+extensions = ["html"]
+
+# Exclude all private IPs from checking.
+exclude_all_private = true
+
+#############################  Local files  #########################
+
+# Required to resolve root-relative links (e.g. href="/") found in every page.
+# Combined with scheme = ["https", "http"], the resulting file:// paths are
+# silently skipped — no HTTP check, no error.
+root_dir = "site"
+
+#############################  Remap  ###############################
+
+# Rewrite doc.ibexa.co links to locally-built MkDocs sites, avoiding HTTP
+# requests to Cloudflare-protected hosts.
+#
+# Two patterns per version:
+#   1. Trailing-slash pages  → /site//index.html
+#   2. Direct .html files    → /site/.html  (PHP/REST API refs)
+#
+# The `repositories/` directory must be populated via:
+#   git clone --depth=1 --branch 4.6 https://github.com/ibexa/documentation-developer.git repositories/devdoc-4.6
+#   (and similarly for devdoc-5.0, userdoc-4.6, userdoc-5.0)
+# Then build each with: python3 -m mkdocs build
+remap = [
+    # devdoc 4.6 — three patterns in priority order:
+    #   1. direct .html files (PHP/REST API reference)
+    #   2. directory paths with trailing slash → index.html
+    #   3. directory paths without trailing slash → index.html
+    "https://doc\\.ibexa\\.co/en/4\\.6/(.+\\.html)$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/devdoc-4.6/site/$1",
+    "https://doc\\.ibexa\\.co/en/4\\.6/(.+)/$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/devdoc-4.6/site/$1/index.html",
+    "https://doc\\.ibexa\\.co/en/4\\.6/([^#]+[^/#])$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/devdoc-4.6/site/$1/index.html",
+    # devdoc 5.0
+    "https://doc\\.ibexa\\.co/en/5\\.0/(.+\\.html)$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/devdoc-5.0/site/$1",
+    "https://doc\\.ibexa\\.co/en/5\\.0/(.+)/$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/devdoc-5.0/site/$1/index.html",
+    "https://doc\\.ibexa\\.co/en/5\\.0/([^#]+[^/#])$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/devdoc-5.0/site/$1/index.html",
+    # userdoc 4.6
+    "https://doc\\.ibexa\\.co/projects/userguide/en/4\\.6/(.+\\.html)$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/userdoc-4.6/site/$1",
+    "https://doc\\.ibexa\\.co/projects/userguide/en/4\\.6/(.+)/$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/userdoc-4.6/site/$1/index.html",
+    "https://doc\\.ibexa\\.co/projects/userguide/en/4\\.6/([^#]+[^/#])$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/userdoc-4.6/site/$1/index.html",
+    # userdoc 5.0
+    "https://doc\\.ibexa\\.co/projects/userguide/en/5\\.0/(.+\\.html)$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/userdoc-5.0/site/$1",
+    "https://doc\\.ibexa\\.co/projects/userguide/en/5\\.0/(.+)/$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/userdoc-5.0/site/$1/index.html",
+    "https://doc\\.ibexa\\.co/projects/userguide/en/5\\.0/([^#]+[^/#])$ file:///Users/marek/Desktop/repos/mnocon/documentation-developer/link-checker/repositories/userdoc-5.0/site/$1/index.html",
+]
+
+#############################  Hosts  ###############################
+
+# Global limit: at most 2 simultaneous requests to any single host.
+host_concurrency = 2
+
+# Global minimum interval between requests to the same host.
+host_request_interval = "500ms"
+
+# Stricter throttling for Cloudflare-protected hosts.
+# [hosts] tables must come last in the file (TOML section scoping).
+[hosts."doc.ibexa.co"]
+concurrency = 1
+request_interval = "1s"
diff --git a/mkdocs.yml b/mkdocs.yml
index a2082cc594..87bc6a0ac8 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:
diff --git a/tools/api_refs/.phpdoc/template/components/source.html.twig b/tools/api_refs/.phpdoc/template/components/source.html.twig
index d4f5238e3e..1fa22c9048 100644
--- a/tools/api_refs/.phpdoc/template/components/source.html.twig
+++ b/tools/api_refs/.phpdoc/template/components/source.html.twig
@@ -2,7 +2,7 @@
 
 {% block content %}
 
- {% set href = 'https://github.com/ibexa/documentation-developer/tree/master/docs/api/php_api' %} + {% set href = 'https://github.com/ibexa/documentation-developer/tree/5.0/docs/api/php_api' %} {% if node.file is not null %} {% set path = node.file.path|split('/', 4) %} {% set package = path|slice(1, 2)|join('/') %}