From c0a0f9eebb8d8492990aaa17a6281299b09993b2 Mon Sep 17 00:00:00 2001 From: MuhammadAashirAslam Date: Mon, 10 Aug 2026 09:39:37 +0500 Subject: [PATCH 1/5] Add Hugo Algolia indexing --- .github/workflows/update-algolia.yml | 85 +++-- layouts/index.algolia.json | 31 ++ package-lock.json | 534 +++++++++++++++++++++++++++ package.json | 16 + test/algolia-index.test.mjs | 90 +++++ tools/algolia-index.mjs | 510 +++++++++++++++++++++++++ 6 files changed, 1241 insertions(+), 25 deletions(-) create mode 100644 layouts/index.algolia.json create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 test/algolia-index.test.mjs create mode 100644 tools/algolia-index.mjs diff --git a/.github/workflows/update-algolia.yml b/.github/workflows/update-algolia.yml index 2521d09ff04..1b00cdf3817 100644 --- a/.github/workflows/update-algolia.yml +++ b/.github/workflows/update-algolia.yml @@ -1,39 +1,74 @@ name: Update the Algolia search index + on: workflow_dispatch: schedule: - cron: "0 2 * * *" +permissions: + contents: read + +concurrency: + group: update-algolia + cancel-in-progress: false + +env: + GO_VERSION: "1.26.3" + HUGO_VERSION: "0.163.3" + NODE_VERSION: "24.18.0" + ALGOLIA_APP_ID: "LIT6P0EW26" + ALGOLIA_INDEX_NAME: "jekyll" + jobs: - build: + index: + name: Build and publish index runs-on: ubuntu-latest + timeout-minutes: 30 + steps: - name: Checkout repository - uses: actions/checkout@v6 + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7 with: - submodules: true + fetch-depth: 0 + submodules: false lfs: false - path: website - - uses: ruby/setup-ruby@v1 + + - name: Set up Go + uses: actions/setup-go@924ae3a1cded613372ab5595356fb5720e22ba16 # v6 + with: + go-version: ${{ env.GO_VERSION }} + cache-dependency-path: go.sum + + - name: Set up Hugo + uses: peaceiris/actions-hugo@2752ce1d29631191ea3f27c23495fa06139a5b78 # v3 with: - bundler-cache: true - working-directory: website - - name: Check recent commit - id: checkcommit - working-directory: website - run: | - LATEST_COMMIT=$(git log -1 --format=%ct) - CURRENT_TIME=$(date +%s) - TIME_DIFF=$((CURRENT_TIME - LATEST_COMMIT)) - if [ $TIME_DIFF -gt 86400 ]; then - echo "No commit within 24 hours." - echo "recent_commit=false" >> "$GITHUB_OUTPUT" - else - echo "recent_commit=true" >> "$GITHUB_OUTPUT" - fi - - name: Build website and update Algolia index - if: ${{ github.event_name == 'workflow_dispatch' || steps.checkcommit.outputs.recent_commit == 'true' }} - working-directory: website + hugo-version: ${{ env.HUGO_VERSION }} + extended: true + + - name: Set up Node.js + uses: actions/setup-node@249970729cb0ef3589644e2896645e5dc5ba9c38 # v6 + with: + node-version: ${{ env.NODE_VERSION }} + cache: npm + + - name: Install Node.js dependencies + run: npm ci + + - name: Test Algolia indexing + run: npm run test:algolia + + - name: Verify Hugo modules + run: hugo mod verify + + - name: Build Hugo Algolia export + run: hugo --gc --minify --cleanDestinationDir --environment production + + - name: Validate generated Algolia records + run: npm run algolia:index -- --dry-run + + - name: Replace Algolia records env: - JEKYLL_ENV: production - run: ALGOLIA_API_KEY=${{ secrets.ALGOLIA_API_KEY }} bundle exec jekyll algolia + ALGOLIA_APP_ID: ${{ env.ALGOLIA_APP_ID }} + ALGOLIA_WRITE_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} + ALGOLIA_INDEX_NAME: ${{ env.ALGOLIA_INDEX_NAME }} + run: npm run algolia:index diff --git a/layouts/index.algolia.json b/layouts/index.algolia.json new file mode 100644 index 00000000000..a292d4930e3 --- /dev/null +++ b/layouts/index.algolia.json @@ -0,0 +1,31 @@ +{{- $pages := slice -}} +{{- range site.Pages -}} + {{- if and (or (eq .Kind "page") (eq .Kind "section")) (ne (.Param "search") "exclude") -}} + {{- $pages = $pages | append . -}} + {{- end -}} +{{- end -}} +{{- $pages = sort $pages "RelPermalink" -}} +{ + "schemaVersion": 1, + "nodesToIndex": {{ site.Params.algolia.nodes_to_index | jsonify }}, + "maxRecordSize": {{ site.Params.algolia.max_record_size }}, + "pages": [ + {{- range $index, $page := $pages }} + {{- if $index }},{{ end }} + {{- /* Keep the search export on the same rendered-content path as page layouts. */ -}} + {{- $content := partial "compat_content.html" $page -}} + { + "title": {{ $page.Title | jsonify }}, + "url": {{ $page.RelPermalink | jsonify }}, + "html": {{ $content | jsonify }}, + "plain": {{ $content | plainify | jsonify }}, + "section": {{ $page.Section | jsonify }}, + "kind": {{ $page.Kind | jsonify }}, + "tags": {{ $page.Params.tags | jsonify }}, + "categories": {{ $page.Params.categories | jsonify }}, + "keywords": {{ $page.Params.keywords | jsonify }}, + "date": {{ if ne ($page.Lastmod.Format "2006") "0001" }}{{ $page.Lastmod.Unix }}{{ else }}null{{ end }} + } + {{- end }} + ] +} diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 00000000000..912ec4bc100 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,534 @@ +{ + "name": "precice-website-tools", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "precice-website-tools", + "dependencies": { + "algoliasearch": "^5.0.0", + "cheerio": "^1.0.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@algolia/abtesting": { + "version": "1.22.0", + "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.22.0.tgz", + "integrity": "sha512-BFR6zNowNKcY7Ou7TaJc9QWexES4YKPbmf/OTFofpdsdhz4x6q0lbxp3duO0EHnyrN7rE4ba/TSXuY+BDGu4+g==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-abtesting": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.56.0.tgz", + "integrity": "sha512-7r4Z3NC7yU1oAQVWJNA2HX7tX481F3pJvCGyLIXiTdBcthz4Q/o21jwcMYDFkuI92UWTNBQQmHYgwHo1zS5dzg==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-analytics": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.56.0.tgz", + "integrity": "sha512-avmjXQSq+jadFO8Xl2em05/uQdQnEmHsJyOAdVbZkmVgpMfxL12aJwVVfGNwYr9nulcpuJN1X0lTaQ5wxuNGcA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-common": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.56.0.tgz", + "integrity": "sha512-v2TPStUhY//ripPjIVclZ8AWc7DEGooXULZGFlFu37zNatgHjw34oZZ+OSbbc/YHO+xZwPl62I1k8xH1m4S2eg==", + "license": "MIT", + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-insights": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.56.0.tgz", + "integrity": "sha512-P0ehROpM4Sem3Sqo5x2cKPgj67D3G3jy0rh1Amwkcvsfr6tkvIcdCmerieanqTF7NxUMPNFLkpIFeMO8Rpa50w==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-personalization": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.56.0.tgz", + "integrity": "sha512-SXK3Vn3WVxyzbm31oePZBJkp1wpOyuWdd4B/Pv7n0aXDxmeSWhC1R1FC1517mMrFAIaPH4Rt0x6RUe7ZNjz8FA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-query-suggestions": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.56.0.tgz", + "integrity": "sha512-5+ZdX8garFnmycnZgKhtXHePEaLj5zqDxI/0lkhhluzCcvTn0/PvvTirTg8hHYetQHvn7GDyeAiqTAieMvMW4A==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/client-search": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.56.0.tgz", + "integrity": "sha512-+mKUdYvqOi0BcvpAEyCEw49vSBptufIcfibtHz2bdr1pI789M46Yt0uQEk/sxtK3teh71OQvVFHaTDzShUWewQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/ingestion": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.56.0.tgz", + "integrity": "sha512-9g/zj+AZx5moFcdFIrYQoVrueXivjUcc3MQHtCYT8WhIuk1lUh1AyEhvJCS0XBZld09cLvd1AZ3BvDBpVpX2UA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/monitoring": { + "version": "1.56.0", + "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.56.0.tgz", + "integrity": "sha512-Qf3Sr6f9A9uxCZUf3MXS0d2b877uYzEB5yxqpVGXAhcJnBCQjrRRon0KvefpGkxy+BshrIJs96OUoMtGqXTFDA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/recommend": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.56.0.tgz", + "integrity": "sha512-GXWG1rWc5wu8hY4N33Y3b6ernY6sAdAvmKWN/zHAiACOx40WnpG0TVX5YazCAr/9gOYGInSiM2A0y2jy2xbiDA==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-browser-xhr": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.56.0.tgz", + "integrity": "sha512-7t24cBxaInS3mZb7ddEaZT/tp6q+/aR4YttsQVyP1/i+LmwPR34atO35KjaLFCcRVrlP7sYOAqkCfg6lIRB+ew==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-fetch": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.56.0.tgz", + "integrity": "sha512-R7ePHgVYmDFjZpvrsVAfbDz/d4RxKAYZ5/vgLfIsCVRZRryjWl/3INOxpOICzitehQ5FjNtNjcLQTrmHPTcHBQ==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@algolia/requester-node-http": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.56.0.tgz", + "integrity": "sha512-PIOUXlSnrqM0S+WOgDRb4RzotydJH7ZoT6tOyL7tAO7qJOfvX5wsEW8Pe+PMKMwvuI4/gIyK9cg2H7lJXqnc4Q==", + "license": "MIT", + "dependencies": { + "@algolia/client-common": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/algoliasearch": { + "version": "5.56.0", + "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.56.0.tgz", + "integrity": "sha512-PrqppUmhT4ENdas2pH9caE7efUcxy6EcSFhWzosiVuQBzu2tQ5yLTI6jwomT/1cuBnivzGfxiJCqDNN9FRRh+Q==", + "license": "MIT", + "dependencies": { + "@algolia/abtesting": "1.22.0", + "@algolia/client-abtesting": "5.56.0", + "@algolia/client-analytics": "5.56.0", + "@algolia/client-common": "5.56.0", + "@algolia/client-insights": "5.56.0", + "@algolia/client-personalization": "5.56.0", + "@algolia/client-query-suggestions": "5.56.0", + "@algolia/client-search": "5.56.0", + "@algolia/ingestion": "1.56.0", + "@algolia/monitoring": "1.56.0", + "@algolia/recommend": "5.56.0", + "@algolia/requester-browser-xhr": "5.56.0", + "@algolia/requester-fetch": "5.56.0", + "@algolia/requester-node-http": "5.56.0" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "license": "ISC" + }, + "node_modules/cheerio": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", + "integrity": "sha512-WDrybc/gKFpTYQutKIK6UvfcuxijIZfMfXaYm8NMsPQxSYvf+13fXUJ4rztGGbJcBQ/GF55gvrZ0Bc0bj/mqvg==", + "license": "MIT", + "dependencies": { + "cheerio-select": "^2.1.0", + "dom-serializer": "^2.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "encoding-sniffer": "^0.2.1", + "htmlparser2": "^10.1.0", + "parse5": "^7.3.0", + "parse5-htmlparser2-tree-adapter": "^7.1.0", + "parse5-parser-stream": "^7.1.2", + "undici": "^7.19.0", + "whatwg-mimetype": "^4.0.0" + }, + "engines": { + "node": ">=20.18.1" + }, + "funding": { + "url": "https://github.com/cheeriojs/cheerio?sponsor=1" + } + }, + "node_modules/cheerio-select": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", + "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-select": "^5.1.0", + "css-what": "^6.1.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-select": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", + "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-what": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", + "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "BSD-2-Clause" + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "license": "BSD-2-Clause", + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.2.2", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", + "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", + "license": "BSD-2-Clause", + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/encoding-sniffer": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/encoding-sniffer/-/encoding-sniffer-0.2.1.tgz", + "integrity": "sha512-5gvq20T6vfpekVtqrYQsSCFZ1wEg5+wW0/QaZMWkFr6BqD3NfKs0rLCx4rrVlSWJeZb5NBJgVLswK/w2MWU+Gw==", + "license": "MIT", + "dependencies": { + "iconv-lite": "^0.6.3", + "whatwg-encoding": "^3.1.1" + }, + "funding": { + "url": "https://github.com/fb55/encoding-sniffer?sponsor=1" + } + }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/htmlparser2": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", + "integrity": "sha512-VTZkM9GWRAtEpveh7MSF6SjjrpNVNNVJfFup7xTY3UpFtm67foy9HDVXneLtFVt4pMz5kZtgNcvCniNFb1hlEQ==", + "funding": [ + "https://github.com/fb55/htmlparser2?sponsor=1", + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ], + "license": "MIT", + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "entities": "^7.0.1" + } + }, + "node_modules/htmlparser2/node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "license": "MIT", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "license": "BSD-2-Clause", + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "license": "MIT", + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", + "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", + "license": "MIT", + "dependencies": { + "domhandler": "^5.0.3", + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5-parser-stream": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz", + "integrity": "sha512-JyeQc9iwFLn5TbvvqACIF/VXG6abODeB3Fwmv/TGdLk2LfbWkaySGY72at4+Ty7EkPZj854u4CrICqNk2qIbow==", + "license": "MIT", + "dependencies": { + "parse5": "^7.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "license": "MIT" + }, + "node_modules/undici": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", + "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/whatwg-encoding": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz", + "integrity": "sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==", + "deprecated": "Use @exodus/bytes instead for a more spec-conformant and faster implementation", + "license": "MIT", + "dependencies": { + "iconv-lite": "0.6.3" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/whatwg-mimetype": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-4.0.0.tgz", + "integrity": "sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==", + "license": "MIT", + "engines": { + "node": ">=18" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 00000000000..fec1754f6c1 --- /dev/null +++ b/package.json @@ -0,0 +1,16 @@ +{ + "name": "precice-website-tools", + "private": true, + "type": "module", + "engines": { + "node": ">=20" + }, + "scripts": { + "algolia:index": "node tools/algolia-index.mjs", + "test:algolia": "node --test test/algolia-index.test.mjs" + }, + "dependencies": { + "algoliasearch": "^5.0.0", + "cheerio": "^1.0.0" + } +} diff --git a/test/algolia-index.test.mjs b/test/algolia-index.test.mjs new file mode 100644 index 00000000000..1924b45e4f5 --- /dev/null +++ b/test/algolia-index.test.mjs @@ -0,0 +1,90 @@ +import assert from "node:assert/strict"; +import test from "node:test"; +import { + createRecords, + parseCliArgs, + recordByteSize, + validateSearchExport, +} from "../tools/algolia-index.mjs"; + +const searchExport = { + schemaVersion: 1, + nodesToIndex: "p,code,table", + maxRecordSize: 20_000, + pages: [ + { + title: "Example guide", + url: "/docs/example/", + html: ` +

Install

+

Use precice-config to configure the adapter.

+
Version3.4
+ + `, + plain: "Use precice-config to configure the adapter. Version 3.4", + section: "docs", + kind: "page", + tags: ["adapter"], + categories: [], + keywords: "configuration", + date: 1_784_198_400, + }, + ], +}; + +test("creates Jekyll-compatible records with heading and anchor metadata", () => { + const records = createRecords(searchExport); + + assert.equal(records.length, 3); + assert.equal(records[0].title, "Example guide"); + assert.equal(records[0].url, "/docs/example/"); + assert.equal(records[0].anchor, "install"); + assert.deepEqual(records[0].headings, ["Install"]); + assert.match(records[0].html, /^

/); + assert.equal(records[1].content, "precice-config"); + assert.equal(records[2].content, "Version3.4"); + assert.equal(records[0].custom_ranking.heading, 80); + assert.match(records[0].objectID, /^[a-f0-9]{64}$/); + assert.equal(records.some((record) => record.content.includes("ignored content")), false); +}); + +test("creates deterministic object IDs", () => { + const first = createRecords(searchExport).map((record) => record.objectID); + const second = createRecords(searchExport).map((record) => record.objectID); + + assert.deepEqual(first, second); +}); + +test("excludes callouts so a tutorial result starts with its introduction", () => { + const calloutExport = structuredClone(searchExport); + calloutExport.nodesToIndex = "p"; + calloutExport.pages[0].html = ` +

+

This tutorial introduces the partitioned heat-conduction case.

+ `; + calloutExport.pages[0].plain = "Note: Download the case files. This tutorial introduces the partitioned heat-conduction case."; + + const records = createRecords(calloutExport); + + assert.equal(records.length, 1); + assert.equal(records[0].content, "This tutorial introduces the partitioned heat-conduction case."); + assert.equal(records[0].content.includes("Download the case files"), false); +}); + +test("splits oversized records without exceeding the configured limit", () => { + const longExport = structuredClone(searchExport); + longExport.nodesToIndex = "p"; + longExport.pages[0].html = `

${"word ".repeat(400)}

`; + longExport.pages[0].plain = "word ".repeat(400); + + const records = createRecords(longExport, { maxRecordSize: 500 }); + + assert.ok(records.length > 1); + assert.ok(records.every((record) => recordByteSize(record) <= 500)); +}); + +test("rejects invalid Hugo exports and command-line arguments", () => { + assert.throws(() => validateSearchExport({ schemaVersion: 2 }), /Unsupported Algolia export schema/); + assert.throws(() => parseCliArgs(["--batch-size", "0"]), /positive integer/); + assert.throws(() => parseCliArgs(["--unexpected"]), /Unknown option/); +}); diff --git a/tools/algolia-index.mjs b/tools/algolia-index.mjs new file mode 100644 index 00000000000..ee8f8d44fb2 --- /dev/null +++ b/tools/algolia-index.mjs @@ -0,0 +1,510 @@ +#!/usr/bin/env node + +import { createHash } from "node:crypto"; +import { readFile } from "node:fs/promises"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; +import { load } from "cheerio"; + +const HEADING_SELECTOR = "h1,h2,h3,h4,h5,h6"; +const EXCLUDED_SELECTOR = "script,style,iframe"; +const CALLOUT_SELECTOR = ".alert,[role='alert']"; + +// Preserve the production Algolia ranking and highlighting contract. +export const INDEX_SETTINGS = Object.freeze({ + searchableAttributes: [ + "title", + "headings", + "unordered(content)", + "collection,categories,tags", + ], + customRanking: [ + "desc(date)", + "desc(custom_ranking.heading)", + "asc(custom_ranking.position)", + ], + unretrievableAttributes: ["custom_ranking"], + attributesToHighlight: [ + "title", + "headings", + "content", + "html", + "collection", + "categories", + "tags", + ], + highlightPreTag: '', + highlightPostTag: "", + attributesToSnippet: ["content:55"], + snippetEllipsisText: "…", + distinct: true, + attributeForDistinct: "url", + attributesForFaceting: [ + "type", + "searchable(collection)", + "searchable(categories)", + "searchable(tags)", + "searchable(title)", + ], +}); + +export function parseCliArgs(args) { + const options = { + batchSize: 1000, + dryRun: false, + input: "public/algolia.json", + maxRecordSize: undefined, + }; + + for (let index = 0; index < args.length; index += 1) { + const argument = args[index]; + + if (argument === "--dry-run") { + options.dryRun = true; + continue; + } + + if (argument === "--help" || argument === "-h") { + options.help = true; + continue; + } + + if (argument === "--input" || argument === "--batch-size" || argument === "--max-record-size") { + const value = args[index + 1]; + if (!value || value.startsWith("--")) { + throw new Error(`Missing value for ${argument}.`); + } + index += 1; + + if (argument === "--input") { + options.input = value; + } else if (argument === "--batch-size") { + options.batchSize = positiveInteger(value, argument); + } else { + options.maxRecordSize = positiveInteger(value, argument); + } + continue; + } + + throw new Error(`Unknown option: ${argument}`); + } + + return options; +} + +export function validateSearchExport(searchExport) { + if (!searchExport || typeof searchExport !== "object" || Array.isArray(searchExport)) { + throw new Error("Algolia export must be a JSON object."); + } + if (searchExport.schemaVersion !== 1) { + throw new Error(`Unsupported Algolia export schema: ${searchExport.schemaVersion}.`); + } + if (!Array.isArray(searchExport.pages)) { + throw new Error("Algolia export must contain a pages array."); + } + if (typeof searchExport.nodesToIndex !== "string" || !searchExport.nodesToIndex.trim()) { + throw new Error("Algolia export must define nodesToIndex."); + } + if (!Number.isInteger(searchExport.maxRecordSize) || searchExport.maxRecordSize <= 0) { + throw new Error("Algolia export must define a positive integer maxRecordSize."); + } + + for (const page of searchExport.pages) { + if (!page || typeof page !== "object") { + throw new Error("Algolia export contains an invalid page."); + } + if (typeof page.title !== "string" || typeof page.url !== "string") { + throw new Error("Every Algolia export page must contain title and url strings."); + } + if (typeof page.html !== "string" || typeof page.plain !== "string") { + throw new Error("Every Algolia export page must contain html and plain strings."); + } + } + + return searchExport; +} + +export function createRecords(searchExport, options = {}) { + validateSearchExport(searchExport); + + const maxRecordSize = options.maxRecordSize ?? searchExport.maxRecordSize; + if (!Number.isInteger(maxRecordSize) || maxRecordSize <= 0) { + throw new Error("maxRecordSize must be a positive integer."); + } + + const records = []; + for (const page of searchExport.pages) { + const pageRecords = extractPageRecords(page, searchExport.nodesToIndex); + for (const record of pageRecords) { + records.push(...fitRecord(record, maxRecordSize)); + } + } + + if (records.length === 0) { + throw new Error("The Algolia export did not produce any indexable records."); + } + + return records; +} + +export function recordByteSize(record) { + return Buffer.byteLength(JSON.stringify(record), "utf8"); +} + +export async function readSearchExport(inputPath) { + let source; + try { + source = await readFile(inputPath, "utf8"); + } catch (error) { + throw new Error(`Unable to read Algolia export at ${inputPath}: ${error.message}`); + } + + try { + return validateSearchExport(JSON.parse(source)); + } catch (error) { + if (error instanceof SyntaxError) { + throw new Error(`Algolia export at ${inputPath} is not valid JSON: ${error.message}`); + } + throw error; + } +} + +export async function uploadRecords({ appId, apiKey, batchSize, indexName, records }) { + const { algoliasearch } = await import("algoliasearch"); + const client = algoliasearch(appId, apiKey); + + const settingsResponse = await client.setSettings({ + indexName, + indexSettings: INDEX_SETTINGS, + }); + await client.waitForTask({ indexName, taskID: settingsResponse.taskID }); + + return client.replaceAllObjects({ + indexName, + objects: records, + batchSize, + scopes: ["settings", "rules", "synonyms"], + }); +} + +export async function run(options, environment = process.env, logger = console) { + const inputPath = path.resolve(options.input); + const searchExport = await readSearchExport(inputPath); + const records = createRecords(searchExport, options); + const maxRecordSize = options.maxRecordSize ?? searchExport.maxRecordSize; + + logger.log( + `Prepared ${records.length} Algolia records from ${searchExport.pages.length} pages ` + + `(maximum record size: ${maxRecordSize} bytes).`, + ); + + if (options.dryRun) { + logger.log("Dry run complete. No Algolia request was made."); + return records; + } + + const appId = environment.ALGOLIA_APP_ID; + const apiKey = environment.ALGOLIA_WRITE_API_KEY; + const indexName = environment.ALGOLIA_INDEX_NAME; + const missing = [ + ["ALGOLIA_APP_ID", appId], + ["ALGOLIA_WRITE_API_KEY", apiKey], + ["ALGOLIA_INDEX_NAME", indexName], + ] + .filter(([, value]) => !value) + .map(([name]) => name); + + if (missing.length > 0) { + throw new Error(`Missing required environment variables: ${missing.join(", ")}.`); + } + + await uploadRecords({ + appId, + apiKey, + batchSize: options.batchSize, + indexName, + records, + }); + logger.log(`Replaced all records in Algolia index ${indexName}.`); + + return records; +} + +function extractPageRecords(page, nodesToIndex) { + const $ = load(page.html); + let nodes; + try { + nodes = $(`${HEADING_SELECTOR},${nodesToIndex}`).toArray(); + } catch (error) { + throw new Error(`Invalid nodesToIndex selector ${JSON.stringify(nodesToIndex)}: ${error.message}`); + } + + const hierarchy = Array(6).fill(null); + const shared = sharedPageFields(page); + const records = []; + let anchor = ""; + let headingLevel = null; + let position = 0; + + for (const node of nodes) { + const element = $(node); + if (isExcludedFromSearch(element)) { + continue; + } + + const tagName = node.tagName?.toLowerCase(); + + if (tagName && /^h[1-6]$/.test(tagName)) { + headingLevel = Number(tagName[1]) - 1; + hierarchy[headingLevel] = normaliseWhitespace(element.text()); + hierarchy.fill(null, headingLevel + 1); + anchor = findAnchor($, element) || anchor; + } + + if (!element.is(nodesToIndex)) { + continue; + } + + const searchableElement = element.clone(); + searchableElement.find(EXCLUDED_SELECTOR).remove(); + const content = normaliseWhitespace(searchableElement.text()); + if (!content) { + continue; + } + + records.push({ + ...shared, + anchor, + content, + custom_ranking: { + heading: headingWeight(headingLevel), + position, + }, + headings: hierarchy.filter(Boolean), + html: $.html(searchableElement).trim(), + }); + position += 1; + } + + if (records.length > 0) { + return records; + } + + const fallbackContent = searchableDocumentText($) || page.title; + return [ + { + ...shared, + anchor: "", + content: fallbackContent, + custom_ranking: { heading: 100, position: 0 }, + headings: [], + html: escapeHtml(fallbackContent), + }, + ]; +} + +function isExcludedFromSearch(element) { + return element.is(EXCLUDED_SELECTOR) || element.closest(`${EXCLUDED_SELECTOR},${CALLOUT_SELECTOR}`).length > 0; +} + +function searchableDocumentText($) { + const document = $.root().clone(); + document.find(`${EXCLUDED_SELECTOR},${CALLOUT_SELECTOR}`).remove(); + return normaliseWhitespace(document.text()); +} + +function sharedPageFields(page) { + const record = { + categories: normaliseArray(page.categories), + collection: page.section || undefined, + date: Number.isInteger(page.date) ? page.date : undefined, + keywords: normaliseSearchValue(page.keywords), + tags: normaliseArray(page.tags), + title: page.title, + type: page.kind || "page", + url: page.url, + }; + + return removeUndefined(record); +} + +function fitRecord(record, maxRecordSize) { + const completed = withObjectId(record); + if (recordByteSize(completed) <= maxRecordSize) { + return [completed]; + } + + const baseRecord = { ...record }; + const fragments = []; + const words = record.content.match(/\S+/gu) ?? []; + let fragment = ""; + + for (const word of words) { + const candidate = fragment ? `${fragment} ${word}` : word; + if (fitsAsTextRecord(baseRecord, candidate, fragments.length, maxRecordSize)) { + fragment = candidate; + continue; + } + + if (fragment) { + fragments.push(textRecord(baseRecord, fragment, fragments.length)); + fragment = ""; + } + + if (fitsAsTextRecord(baseRecord, word, fragments.length, maxRecordSize)) { + fragment = word; + continue; + } + + fragments.push(...splitLongWord(baseRecord, word, fragments.length, maxRecordSize)); + } + + if (fragment) { + fragments.push(textRecord(baseRecord, fragment, fragments.length)); + } + + if (fragments.length === 0) { + throw new Error(`Record for ${record.url} cannot fit within ${maxRecordSize} bytes.`); + } + + return fragments; +} + +function fitsAsTextRecord(baseRecord, content, fragmentIndex, maxRecordSize) { + return recordByteSize(textRecord(baseRecord, content, fragmentIndex)) <= maxRecordSize; +} + +function splitLongWord(baseRecord, word, startIndex, maxRecordSize) { + const fragments = []; + let fragment = ""; + + for (const character of word) { + const candidate = `${fragment}${character}`; + if (fitsAsTextRecord(baseRecord, candidate, startIndex + fragments.length, maxRecordSize)) { + fragment = candidate; + continue; + } + + if (!fragment) { + throw new Error(`Record for ${baseRecord.url} cannot fit within ${maxRecordSize} bytes.`); + } + + fragments.push(textRecord(baseRecord, fragment, startIndex + fragments.length)); + fragment = character; + } + + if (fragment) { + fragments.push(textRecord(baseRecord, fragment, startIndex + fragments.length)); + } + + return fragments; +} + +function textRecord(baseRecord, content, fragmentIndex) { + return withObjectId({ + ...baseRecord, + content, + html: escapeHtml(content), + custom_ranking: { + ...baseRecord.custom_ranking, + position: baseRecord.custom_ranking.position + fragmentIndex / 1000, + }, + }); +} + +function withObjectId(record) { + const objectID = createHash("sha256") + .update(JSON.stringify(record)) + .digest("hex"); + return { ...record, objectID }; +} + +function findAnchor($, element) { + const directAnchor = element.attr("name") || element.attr("id"); + if (directAnchor) { + return directAnchor; + } + + const descendant = element.find("[name],[id]").first(); + return descendant.attr("name") || descendant.attr("id") || ""; +} + +function headingWeight(level) { + return level === null ? 100 : 100 - (level + 1) * 10; +} + +function normaliseWhitespace(value) { + return String(value ?? "").replace(/\s+/gu, " ").trim(); +} + +function normaliseArray(value) { + if (value === null || value === undefined || value === "") { + return []; + } + return Array.isArray(value) ? value.map(String).filter(Boolean) : [String(value)]; +} + +function normaliseSearchValue(value) { + if (value === null || value === undefined || value === "") { + return undefined; + } + return Array.isArray(value) ? value.map(String).filter(Boolean) : String(value); +} + +function removeUndefined(object) { + return Object.fromEntries(Object.entries(object).filter(([, value]) => value !== undefined)); +} + +function escapeHtml(value) { + return value.replace(/[&<>"']/gu, (character) => { + const replacements = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }; + return replacements[character]; + }); +} + +function positiveInteger(value, optionName) { + const number = Number(value); + if (!Number.isInteger(number) || number <= 0) { + throw new Error(`${optionName} must be a positive integer.`); + } + return number; +} + +function printHelp() { + console.log(`Usage: npm run algolia:index -- [options] + +Options: + --input Hugo export to read (default: public/algolia.json) + --dry-run Validate and transform records without uploading + --max-record-size Override the Hugo export record-size limit + --batch-size Records per Algolia batch (default: 1000) + -h, --help Show this help text + +Environment variables for uploads: + ALGOLIA_APP_ID + ALGOLIA_WRITE_API_KEY + ALGOLIA_INDEX_NAME`); +} + +async function main() { + const options = parseCliArgs(process.argv.slice(2)); + if (options.help) { + printHelp(); + return; + } + await run(options); +} + +const executedAsCli = process.argv[1] && path.resolve(process.argv[1]) === fileURLToPath(import.meta.url); +if (executedAsCli) { + main().catch((error) => { + console.error(`Algolia indexing failed: ${error.message}`); + process.exitCode = 1; + }); +} From 0267c3f7cc8eaf8d7aa9037058f7bc635dd974d6 Mon Sep 17 00:00:00 2001 From: MuhammadAashirAslam Date: Tue, 11 Aug 2026 09:43:06 +0500 Subject: [PATCH 2/5] Use Hugo Algolia index --- .github/workflows/update-algolia.yml | 2 +- config/_default/hugo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/update-algolia.yml b/.github/workflows/update-algolia.yml index 1b00cdf3817..0f3c7503bc9 100644 --- a/.github/workflows/update-algolia.yml +++ b/.github/workflows/update-algolia.yml @@ -17,7 +17,7 @@ env: HUGO_VERSION: "0.163.3" NODE_VERSION: "24.18.0" ALGOLIA_APP_ID: "LIT6P0EW26" - ALGOLIA_INDEX_NAME: "jekyll" + ALGOLIA_INDEX_NAME: "hugo" jobs: index: diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml index e912bc4055d..2c5afcfcbca 100644 --- a/config/_default/hugo.toml +++ b/config/_default/hugo.toml @@ -26,7 +26,7 @@ precice_v2_citations_url = "https://scholar.google.com/scholar?oi=bibs&hl=en&cit [params.algolia] application_id = "LIT6P0EW26" -index_name = "jekyll" +index_name = "hugo" search_only_api_key = "760ed6be3e165d08b4a798ac4aa82fe4" nodes_to_index = "p,code,table" max_record_size = 20000 From eedcb4cdcad534e9a404181b02611e390643df58 Mon Sep 17 00:00:00 2001 From: MuhammadAashirAslam Date: Fri, 14 Aug 2026 18:13:38 +0500 Subject: [PATCH 3/5] Exclude callout content from Algolia records Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- tools/algolia-index.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/algolia-index.mjs b/tools/algolia-index.mjs index ee8f8d44fb2..576faa7477b 100644 --- a/tools/algolia-index.mjs +++ b/tools/algolia-index.mjs @@ -266,7 +266,7 @@ function extractPageRecords(page, nodesToIndex) { } const searchableElement = element.clone(); - searchableElement.find(EXCLUDED_SELECTOR).remove(); + searchableElement.find(`${EXCLUDED_SELECTOR},${CALLOUT_SELECTOR}`).remove(); const content = normaliseWhitespace(searchableElement.text()); if (!content) { continue; From 3a7ea5c88a6b9e0369f44ababf55f38572e687b4 Mon Sep 17 00:00:00 2001 From: MuhammadAashirAslam Date: Fri, 14 Aug 2026 18:24:03 +0500 Subject: [PATCH 4/5] Copilot's review changes --- package-lock.json | 2 +- package.json | 2 +- tools/algolia-index.mjs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/package-lock.json b/package-lock.json index 912ec4bc100..e105ebcecd9 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,7 +10,7 @@ "cheerio": "^1.0.0" }, "engines": { - "node": ">=20" + "node": ">=20.18.1" } }, "node_modules/@algolia/abtesting": { diff --git a/package.json b/package.json index fec1754f6c1..8a34eb661a6 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "private": true, "type": "module", "engines": { - "node": ">=20" + "node": ">=20.18.1" }, "scripts": { "algolia:index": "node tools/algolia-index.mjs", diff --git a/tools/algolia-index.mjs b/tools/algolia-index.mjs index 576faa7477b..3f8a52271c6 100644 --- a/tools/algolia-index.mjs +++ b/tools/algolia-index.mjs @@ -258,7 +258,7 @@ function extractPageRecords(page, nodesToIndex) { headingLevel = Number(tagName[1]) - 1; hierarchy[headingLevel] = normaliseWhitespace(element.text()); hierarchy.fill(null, headingLevel + 1); - anchor = findAnchor($, element) || anchor; + anchor = findAnchor($, element); } if (!element.is(nodesToIndex)) { From ebb11ec4a0a0f4977f516e6e2443c780eebad3ba Mon Sep 17 00:00:00 2001 From: MuhammadAashirAslam Date: Fri, 14 Aug 2026 18:35:15 +0500 Subject: [PATCH 5/5] Move Node tooling into tools --- .github/workflows/build.yml | 4 ++++ .github/workflows/update-algolia.yml | 5 +++++ package-lock.json => tools/package-lock.json | 0 package.json => tools/package.json | 4 ++-- 4 files changed, 11 insertions(+), 2 deletions(-) rename package-lock.json => tools/package-lock.json (100%) rename package.json => tools/package.json (65%) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 38f5cc9d310..7cd610604e6 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -76,12 +76,15 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} cache: npm + cache-dependency-path: tools/package-lock.json - name: Install Node.js dependencies run: npm ci + working-directory: tools - name: Test Algolia indexing run: npm run test:algolia + working-directory: tools - name: Verify Hugo modules run: hugo mod verify @@ -91,6 +94,7 @@ jobs: - name: Validate Algolia records run: npm run algolia:index -- --dry-run + working-directory: tools - name: Install XML validation tools run: sudo apt-get update && sudo apt-get install --yes libxml2-utils diff --git a/.github/workflows/update-algolia.yml b/.github/workflows/update-algolia.yml index 0f3c7503bc9..e9d5253db71 100644 --- a/.github/workflows/update-algolia.yml +++ b/.github/workflows/update-algolia.yml @@ -50,12 +50,15 @@ jobs: with: node-version: ${{ env.NODE_VERSION }} cache: npm + cache-dependency-path: tools/package-lock.json - name: Install Node.js dependencies run: npm ci + working-directory: tools - name: Test Algolia indexing run: npm run test:algolia + working-directory: tools - name: Verify Hugo modules run: hugo mod verify @@ -65,6 +68,7 @@ jobs: - name: Validate generated Algolia records run: npm run algolia:index -- --dry-run + working-directory: tools - name: Replace Algolia records env: @@ -72,3 +76,4 @@ jobs: ALGOLIA_WRITE_API_KEY: ${{ secrets.ALGOLIA_API_KEY }} ALGOLIA_INDEX_NAME: ${{ env.ALGOLIA_INDEX_NAME }} run: npm run algolia:index + working-directory: tools diff --git a/package-lock.json b/tools/package-lock.json similarity index 100% rename from package-lock.json rename to tools/package-lock.json diff --git a/package.json b/tools/package.json similarity index 65% rename from package.json rename to tools/package.json index 8a34eb661a6..c33488ba84d 100644 --- a/package.json +++ b/tools/package.json @@ -6,8 +6,8 @@ "node": ">=20.18.1" }, "scripts": { - "algolia:index": "node tools/algolia-index.mjs", - "test:algolia": "node --test test/algolia-index.test.mjs" + "algolia:index": "node algolia-index.mjs", + "test:algolia": "node --test ../test/algolia-index.test.mjs" }, "dependencies": { "algoliasearch": "^5.0.0",