Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
90 changes: 65 additions & 25 deletions .github/workflows/update-algolia.yml
Original file line number Diff line number Diff line change
@@ -1,39 +1,79 @@
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: "hugo"

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

- name: Build Hugo Algolia export
run: hugo --gc --minify --cleanDestinationDir --environment production

- name: Validate generated Algolia records
run: npm run algolia:index -- --dry-run
working-directory: tools

- 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
working-directory: tools
2 changes: 1 addition & 1 deletion config/_default/hugo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
31 changes: 31 additions & 0 deletions layouts/index.algolia.json
Original file line number Diff line number Diff line change
@@ -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 }}
]
}
90 changes: 90 additions & 0 deletions test/algolia-index.test.mjs
Original file line number Diff line number Diff line change
@@ -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: `
<h2 id="install">Install</h2>
<p>Use <code>precice-config</code> to configure the adapter.</p>
<table><tbody><tr><td>Version</td><td>3.4</td></tr></tbody></table>
<script>ignored content</script>
`,
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, /^<p>/);
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 = `
<div class="alert alert-info" role="alert"><p>Note: Download the case files.</p></div>
<p>This tutorial introduces the partitioned heat-conduction case.</p>
`;
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 = `<p>${"word ".repeat(400)}</p>`;
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/);
});
Loading
Loading