diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 92b841f4..706b710f 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -236,6 +236,10 @@ jobs: run: | tar -C "spec${baseURL}" --strip-components=1 -xzf openapi.tar.gz + - name: "🔍 pagefind indexing" + run: | + npx -y pagefind --site "spec${baseURL}" + - name: "📦 Tarball creation" run: | cd spec diff --git a/assets/js/offline-search.js b/assets/js/offline-search.js new file mode 100644 index 00000000..0a5dd267 --- /dev/null +++ b/assets/js/offline-search.js @@ -0,0 +1,164 @@ +/* +Copyright 2026 The Matrix.org Foundation C.I.C. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +/* +Adapted from [1] to combine Docsy's built-in search UI with the Pagefind +search backend. + + [1]: https://github.com/matrix-org/docsy/blob/71d103ebb20ace3d528178c4b6d92b6cc4f7fd53/assets/js/offline-search.js +*/ + +(function ($) { + 'use strict'; + + $(document).ready(async function () { + const pagefind = await import("/pagefind/pagefind.js"); + const $searchInput = $('.td-search input'); + + // + // Lazily initialise Pagefind only when the user is about to start a search. + // + + $searchInput.focus(() => { + pagefind.init(); + }); + + // + // Register handler + // + + $searchInput.on('change', (event) => { + render($(event.target)); + + // Hide keyboard on mobile browser + $searchInput.blur(); + }); + + // Prevent reloading page by enter key on sidebar search. + $searchInput.closest('form').on('submit', () => { + return false; + }); + + // + // Pagefind + // + + const render = async ($targetSearchInput) => { + // + // Dispose existing popover + // + + { + let popover = bootstrap.Popover.getInstance($targetSearchInput[0]); + if (popover !== null) { + popover.dispose(); + } + } + + // + // Search + // + + const searchQuery = $targetSearchInput.val(); + if (searchQuery === '') { + return; + } + + const search = await pagefind.debouncedSearch(searchQuery); + if (search === null) { + // A more recent search call has been made, nothing to do. + return; + } + const results = await Promise.all(search.results.slice(0, 20).map(r => r.data())); + + // + // Make result html + // + + const $html = $('
'); + + $html.append( + $('
') + .css({ + display: 'flex', + justifyContent: 'space-between', + marginBottom: '1em', + }) + .append( + $('').text('Search results').css({ fontWeight: 'bold' }) + ) + .append( + $('').addClass('td-offline-search-results__close-button') + ) + ); + + const $searchResultBody = $('
').css({ + maxHeight: `calc(100vh - ${ + $targetSearchInput.offset().top - $(window).scrollTop() + 180 + }px)`, + overflowY: 'auto', + }); + $html.append($searchResultBody); + + if (results.length === 0) { + $searchResultBody.append( + $('

').text(`No results found for query "${searchQuery}"`) + ); + } else { + results.forEach((r) => { + r.sub_results.forEach((s) => { + const href = s.url; + + const $entry = $('

').addClass('mt-4'); + + $entry.append( + $('').addClass('d-block text-body-secondary').text(r.meta.title) + ); + + $entry.append( + $('') + .addClass('d-block') + .css({ + fontSize: '1.2rem', + }) + .attr('href', href) + .text(s.title) + ); + + $entry.append($('

').html(s.excerpt)); + + $searchResultBody.append($entry); + }); + }); + } + + $targetSearchInput.one('shown.bs.popover', () => { + $('.td-offline-search-results__close-button').on('click', () => { + $targetSearchInput.val(''); + $targetSearchInput.trigger('change'); + }); + }); + + const popover = new bootstrap.Popover($targetSearchInput, { + content: $html[0], + html: true, + customClass: 'td-offline-search-results', + placement: 'bottom', + }); + popover.show(); + }; + }); +})(jQuery); diff --git a/assets/scss/_styles_project.scss b/assets/scss/_styles_project.scss index cd9937a9..a2daa0ae 100644 --- a/assets/scss/_styles_project.scss +++ b/assets/scss/_styles_project.scss @@ -661,3 +661,8 @@ dd { margin: 0; } } + +/* Style for the page search widget */ +.td-offline-search-results { + max-width: 460px; +} diff --git a/changelogs/internal/newsfragments/2331.feature b/changelogs/internal/newsfragments/2331.feature new file mode 100644 index 00000000..eb94a36e --- /dev/null +++ b/changelogs/internal/newsfragments/2331.feature @@ -0,0 +1 @@ +Add page search widget. diff --git a/config/_default/hugo.toml b/config/_default/hugo.toml index a33f803b..8c8d9228 100644 --- a/config/_default/hugo.toml +++ b/config/_default/hugo.toml @@ -66,6 +66,7 @@ description = "Home of the Matrix specification for decentralised communication" [params] copyright = "The Matrix.org Foundation C.I.C." +offlineSearch = true [params.version] # must be one of "unstable", "current", "historical" @@ -151,7 +152,8 @@ sidebar_menu_compact = true [server.headers.values] # `style-src 'unsafe-inline'` is needed to correctly render the maths in the Olm spec: # https://github.com/KaTeX/KaTeX/issues/4096 - Content-Security-Policy = "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:; media-src 'self'; child-src 'self'; form-action 'self'; object-src 'self'" + # TODO: Figure out CSP to allow loading the Pagefind Wasm + #Content-Security-Policy = "default-src 'self'; style-src 'self' 'unsafe-inline'; script-src 'self'; img-src 'self' data:; connect-src 'self'; font-src 'self' data:; media-src 'self'; child-src 'self'; form-action 'self'; object-src 'self'" X-XSS-Protection = "1; mode=block" X-Content-Type-Options = "nosniff" # Strict-Transport-Security = "max-age=31536000; includeSubDomains; preload" diff --git a/layouts/_partials/search-input.html b/layouts/_partials/search-input.html new file mode 100644 index 00000000..1b9b82e1 --- /dev/null +++ b/layouts/_partials/search-input.html @@ -0,0 +1,10 @@ +