Skip to content
Merged
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
9 changes: 9 additions & 0 deletions .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
7 changes: 6 additions & 1 deletion pelicanconf.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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'),
]
Expand All @@ -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
Expand Down
13 changes: 13 additions & 0 deletions themes/Flex/static/js/search-init.js
Original file line number Diff line number Diff line change
@@ -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,
});
})();
5 changes: 4 additions & 1 deletion themes/Flex/templates/article.html
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,10 @@ <h1 id="{{ article.slug }}" itemprop="headline">{{ article.title }}</h1>
</aside>
{% endif %}

<div itemprop="articleBody">
{# 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. #}
<div itemprop="articleBody"{% if article.status != 'draft' %} data-pagefind-body{% endif %}>
{{ article.content }}
</div>
</article>
Expand Down
2 changes: 1 addition & 1 deletion themes/Flex/templates/page.html
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@
{{ translations.translations_for(page) }}
<h1 id="{{ page.slug }}">{{ page.title }}</h1>
</header>
<div>
<div{% if page.status != 'draft' and page not in hidden_pages %} data-pagefind-body{% endif %}>
{% block before_content %}{% endblock %}
{% block page_content %}{{ page.content }}{% endblock %}
{% block after_content %}{% endblock %}
Expand Down
31 changes: 31 additions & 0 deletions themes/Flex/templates/pagefind_search.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{% extends "base.html" %}
{% block title %}Search - {{ SITETITLE|default(SITENAME) }}{% endblock %}
{% block canonical %}<link rel="canonical" href="{{ SITEURL }}/search/">{% endblock %}
{% block meta %}
<meta name="author" content="{{ AUTHOR }}" />
<meta name="description" content="Search the posts on rivassec.com." />
{# 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. #}
<meta name="robots" content="noindex, follow" />
<link rel="stylesheet" href="{{ SITEURL }}/pagefind/pagefind-ui.css" />
{% endblock %}
{% block content %}
<article class="single">
<header>
<h1 id="search-heading">Search</h1>
</header>
<p>Search every published post. Results are computed in your browser from a
static index; no query leaves this page.</p>
<div id="search" aria-labelledby="search-heading"></div>
<noscript>
<p>Search needs JavaScript. Browse by
<a href="{{ SITEURL }}/categories.html">category</a> or read the
<a href="{{ SITEURL }}/devsecops-guide.html">DevSecOps Guide</a> instead.</p>
</noscript>
</article>
{% endblock %}
{% block additional_js %}
<script src="{{ SITEURL }}/pagefind/pagefind-ui.js" defer></script>
<script src="{{ SITEURL }}/theme/js/search-init.js" defer></script>
{% endblock %}
Loading