diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index eb26d880..38dfc419 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -44,6 +44,15 @@ jobs: - name: Build site run: pelican content -o output -s publishconf.py + - name: Build search index (Pagefind) + # Static same-origin search: writes the UI assets and chunked index to + # output/pagefind/, all fetched same-origin, so a strict script-src / + # connect-src 'self' CSP keeps working. Indexing is scoped by + # data-pagefind-body (articles + content pages only), which keeps the + # noindexed /drafts/ pages OUT of the search index. Version pinned; + # the runner's preinstalled Node is sufficient. + run: npx --yes pagefind@1.5.2 --site output + - name: Add CNAME file run: echo rivassec.com > output/CNAME diff --git a/pelicanconf.py b/pelicanconf.py index 8a60914d..929516d2 100644 --- a/pelicanconf.py +++ b/pelicanconf.py @@ -127,7 +127,8 @@ def _asset_version() -> str: # tags.html and archives.html are noindex,follow (see base.html), so keep # them out of the sitemap too - never advertise a URL we ask Google not to # index. Surfaced by GSC 'Discovered - not indexed' 2026-08-16. - 'exclude': ['tag/', 'tags.html', 'archives.html'], + # search/ is a noindex client-side search shell (see pagefind_search.html). + 'exclude': ['tag/', 'tags.html', 'archives.html', 'search/'], } STATIC_PATHS.append('extra') # Client-side tools ship verbatim: content/tools/ -> /tools/ (no remap, no @@ -187,6 +188,7 @@ def _asset_version() -> str: MENUITEMS = [ ('About', 'https://rivassec.com/pages/about.html'), ('Categories', 'https://rivassec.com/categories.html'), + ('Search', 'https://rivassec.com/search/'), ('Tools', 'https://rivassec.com/tools/iam-blast-radius/'), ('GitHub', 'https://github.com/rivassec'), ] @@ -199,6 +201,9 @@ def _asset_version() -> str: 'llms_txt.html': 'llms.txt', 'llms_full_txt.html': 'llms-full.txt', 'llms_txt_wellknown.html': '.well-known/llms.txt', + # Client-side search shell; the index under /pagefind/ is generated at + # deploy time (see deploy.yml's Pagefind step). noindex + sitemap-excluded. + 'pagefind_search.html': 'search/index.html', } # Single source of truth for the llms.txt header blockquote, referenced by all diff --git a/themes/Flex/static/js/search-init.js b/themes/Flex/static/js/search-init.js new file mode 100644 index 00000000..c853ae99 --- /dev/null +++ b/themes/Flex/static/js/search-init.js @@ -0,0 +1,13 @@ +/* Initializes the Pagefind UI on /search/. Lives in an external file on + * purpose: the site ships zero inline scripts (script-src 'self', + * precondition enforced by scripts/csp_audit.py in CI). Loaded with defer + * after pagefind-ui.js, so PagefindUI is defined and the DOM is parsed. */ +(function () { + "use strict"; + if (typeof PagefindUI === "undefined") { return; } + new PagefindUI({ + element: "#search", + showSubResults: true, + showImages: false, + }); +})(); diff --git a/themes/Flex/templates/article.html b/themes/Flex/templates/article.html index 0003e3d6..a3bcbef8 100644 --- a/themes/Flex/templates/article.html +++ b/themes/Flex/templates/article.html @@ -61,7 +61,10 @@

{{ article.title }}

{% endif %} -
+ {# data-pagefind-body scopes the search index: with the attribute present + anywhere, Pagefind indexes ONLY elements carrying it - so drafts (which + also render through this template) stay out of the search index. #} +
{{ article.content }}
diff --git a/themes/Flex/templates/page.html b/themes/Flex/templates/page.html index 78731939..baaf08ca 100644 --- a/themes/Flex/templates/page.html +++ b/themes/Flex/templates/page.html @@ -24,7 +24,7 @@ {{ translations.translations_for(page) }}

{{ page.title }}

-
+ {% block before_content %}{% endblock %} {% block page_content %}{{ page.content }}{% endblock %} {% block after_content %}{% endblock %} diff --git a/themes/Flex/templates/pagefind_search.html b/themes/Flex/templates/pagefind_search.html new file mode 100644 index 00000000..5ea8e086 --- /dev/null +++ b/themes/Flex/templates/pagefind_search.html @@ -0,0 +1,31 @@ +{% extends "base.html" %} +{% block title %}Search - {{ SITETITLE|default(SITENAME) }}{% endblock %} +{% block canonical %}{% endblock %} +{% block meta %} + + + {# Search shells are thin content; keep out of the index but followable. + Matched by the sitemap exclude for search/ so the canonical/noindex + guard stays satisfied. #} + + +{% endblock %} +{% block content %} +
+
+

Search

+
+

Search every published post. Results are computed in your browser from a + static index; no query leaves this page.

+ + +
+{% endblock %} +{% block additional_js %} + + +{% endblock %}