Implement article search with PostgreSQL full-text search #41

Closed
opened 2026-03-03 20:54:07 +00:00 by mark · 3 comments
Owner

Summary

The wireframe includes a search box on the article list view (line 337-338 of wireframe.html), but search was never implemented. We need to add article search functionality to the site, including replacing the "Subscribe" CTA in the desktop/mobile navigation with a search box.

Current State

  • Wagtail search backend is installed (wagtail.search in INSTALLED_APPS)
  • ArticlePage.search_fields indexes title (inherited from Page) + summary — but no frontend search exists
  • No WAGTAILSEARCH_BACKENDS setting is configured (defaults to Wagtail DB backend)
  • PostgreSQL is the database — full-text search support is built-in
  • No search view, URL, or template exists
  • The desktop nav has a "Subscribe" button that links to #newsletter — this can be replaced with a search input
  • The mobile menu has an inline newsletter form — search can be added above it

Proposed Strategy: PostgreSQL Full-Text Search via Wagtail

Use Wagtail's built-in wagtail.search.backends.database (PostgreSQL) backend rather than adding Elasticsearch/Meilisearch. This is the right fit because:

  1. Zero new infrastructure — PostgreSQL is already the database
  2. Built-in trigram + full-textpg_trgm gives us fuzzy/partial matching; tsvector gives us ranked full-text search
  3. Sufficient scale — a blog site with hundreds/low-thousands of articles does not need a dedicated search service
  4. Wagtail native — the database backend integrates with search_fields already defined on ArticlePage

Scope: Article Search (not site-wide)

This is article search only — querying ArticlePage objects. We are not searching across all page types (AboutPage, LegalPages, etc.). If cross-page-model search is needed later, we can add a unified query strategy as a follow-up.

Database Indexing

Wagtail's database search backend creates its own index tables (wagtailsearch_indexentry) that use PostgreSQL FTS internally. We should:

  1. Add django.contrib.postgres to INSTALLED_APPS (required for full Postgres search features per Wagtail docs)
  2. Configure WAGTAILSEARCH_BACKENDS to use the database backend explicitly with english search config
  3. Expand ArticlePage.search_fields to include body (StreamField text content, excluding code blocks) and tags via RelatedFields
  4. Add AutocompleteField on title for future type-ahead support
  5. Add FilterField on category and published_date for filtered search
  6. Run ./manage.py update_index to build the search index (automated in deploy, not manual one-off)

Implementation Plan

  • Add django.contrib.postgres to INSTALLED_APPS in config/settings/base.py
  • Add WAGTAILSEARCH_BACKENDS to config/settings/base.py:
    WAGTAILSEARCH_BACKENDS = {
        "default": {
            "BACKEND": "wagtail.search.backends.database",
            "SEARCH_CONFIG": "english",
        }
    }
    
  • Expand ArticlePage.search_fields using correct patterns for related data:
    search_fields = Page.search_fields + [
        index.SearchField("summary"),
        index.SearchField("body"),
        index.AutocompleteField("title"),
        index.RelatedFields("tags", [
            index.SearchField("name"),
        ]),
        index.FilterField("category"),
        index.FilterField("published_date"),
    ]
    
  • Note: index.RelatedFields is the correct pattern for tag/category name matching — index.SearchField("tags") would not work properly.

2. StreamField Body Indexing: Exclude Code Blocks

  • Indexing body (StreamField) will by default include all block text including code blocks, which produces noisy results (matching random code tokens like variable names).
  • Add a get_search_text() method (or use Wagtail's search_index.get_searchable_content() override) to extract only prose text from the StreamField, skipping code blocks.
  • This keeps search results relevant to article content, not code samples.

3. Backend: Search View

  • Add a search view in apps/blog/views.py that:
    • Accepts ?q= query parameter
    • Strips/normalises the query — handle empty/whitespace-only queries explicitly (return empty results, not a full table scan)
    • Max query length guard (e.g., 200 chars) to protect against pathological requests
    • Uses ArticlePage.objects.live().search(query) (Wagtail search API)
    • If category/date filters are also applied: filter the queryset first, then call .search() (Wagtail docs require this ordering)
    • Paginates results (12 per page, matching article index)
    • Preserves q on pagination links (e.g., ?q=llm&page=2)
    • Returns rendered HTML (server-side, no JS framework needed)
  • Wire up URL: path("search/", search_view, name="search") in config/urls.py
    • ⚠️ Must be added before path("", include(wagtail_urls)) or Wagtail's catch-all will swallow it
  • Create search results template matching the article list design

4. Frontend: Navigation Search Box (Desktop)

  • Replace the "Subscribe" <a> button in templates/components/nav.html with a search form:
    <form action="{% url 'search' %}" method="get" class="relative" role="search">
      <svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-400">...</svg>
      <input type="search" name="q" placeholder="Search articles..."
        class="w-48 bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-300
               dark:border-zinc-700 pl-9 pr-3 py-2 font-mono text-sm focus:outline-none
               focus:border-brand-cyan focus:ring-1 focus:ring-brand-cyan transition-shadow" />
    </form>
    
  • Style to match the wireframe's design language (mono font, cyan focus ring)

5. Frontend: Navigation Search Box (Mobile)

  • Add a search form to the mobile menu in nav.html, above the newsletter form
  • Same form action/styling, full-width on mobile

6. Frontend: Article Index Page Search

  • Add the search box from the wireframe to templates/blog/article_index_page.html alongside the category/tag filters
  • This can submit to /search/ or filter inline with a ?q= param on the article index

7. Search Results Page

  • Create templates/blog/search_results.html:
    • Page header showing query and result count
    • Reuse article_card.html component for results
    • "No results" state with suggestions
    • Pagination matching article index style (preserving ?q= across pages)

8. Reindex Lifecycle

  • ./manage.py update_index must be automated as part of deploy, not a one-off manual step
  • Add to the deploy/entrypoint script so existing content is guaranteed indexed after every deploy
  • This ensures new search_fields additions are always reflected

9. Tests (including existing test updates)

New tests:

  • Unit tests for the search view (query parsing, empty query, whitespace handling, max length, pagination with ?q= preserved)
  • Test that search_fields indexes the expected fields
  • Test search results include matches from title, summary, and body (but not code blocks)
  • E2E test: type in nav search box → see results page

Existing tests that need updating (due to Subscribe → Search nav change):

  • apps/blog/tests/test_views.py::test_newsletter_forms_render_in_nav_and_footer — currently asserts href="#newsletter" in nav; needs updating to expect search form instead
  • e2e/test_home.py::test_nav_subscribe_cta_present — currently asserts Subscribe link visible in nav; needs updating to expect search input
  • Newsletter E2E tests (e2e/test_newsletter.py) — these test the sidebar newsletter form, not the nav CTA, so should be unaffected
  • e2e/test_article_detail.py::test_article_newsletter_aside_present — tests sidebar, should be unaffected

Design Notes

  • Follow the existing design language: mono font inputs, cyan focus rings, dark/light theme support
  • The nav search box should be compact (w-48 on desktop) to not crowd the nav
  • Search results page should reuse the article card component for consistency
  • The newsletter "Subscribe" CTA remains in the footer and sidebar — only the nav placement changes
  • Use type="search" and role="search" for accessibility

Out of Scope (for now)

  • Real-time/autocomplete search (type-ahead as you type) — can be added later with HTMX or Alpine.js
  • Search analytics/tracking
  • Elasticsearch/Meilisearch — overkill for current scale
  • Search within comments or author bios
  • Cross-page-model "site-wide" search (AboutPage, LegalPages, etc.)
## Summary The wireframe includes a search box on the article list view (line 337-338 of `wireframe.html`), but search was never implemented. We need to add article search functionality to the site, including replacing the "Subscribe" CTA in the desktop/mobile navigation with a search box. ## Current State - **Wagtail search backend** is installed (`wagtail.search` in `INSTALLED_APPS`) - **`ArticlePage.search_fields`** indexes `title` (inherited from Page) + `summary` — but no frontend search exists - **No `WAGTAILSEARCH_BACKENDS`** setting is configured (defaults to Wagtail DB backend) - **PostgreSQL** is the database — full-text search support is built-in - **No search view, URL, or template** exists - The desktop nav has a **"Subscribe" button** that links to `#newsletter` — this can be replaced with a search input - The mobile menu has an **inline newsletter form** — search can be added above it ## Proposed Strategy: PostgreSQL Full-Text Search via Wagtail Use Wagtail's built-in `wagtail.search.backends.database` (PostgreSQL) backend rather than adding Elasticsearch/Meilisearch. This is the right fit because: 1. **Zero new infrastructure** — PostgreSQL is already the database 2. **Built-in trigram + full-text** — `pg_trgm` gives us fuzzy/partial matching; `tsvector` gives us ranked full-text search 3. **Sufficient scale** — a blog site with hundreds/low-thousands of articles does not need a dedicated search service 4. **Wagtail native** — the `database` backend integrates with `search_fields` already defined on `ArticlePage` ### Scope: Article Search (not site-wide) This is **article search only** — querying `ArticlePage` objects. We are not searching across all page types (AboutPage, LegalPages, etc.). If cross-page-model search is needed later, we can add a unified query strategy as a follow-up. ### Database Indexing Wagtail's `database` search backend creates its own index tables (`wagtailsearch_indexentry`) that use PostgreSQL FTS internally. We should: 1. Add `django.contrib.postgres` to `INSTALLED_APPS` (required for full Postgres search features per Wagtail docs) 2. Configure `WAGTAILSEARCH_BACKENDS` to use the `database` backend explicitly with `english` search config 3. Expand `ArticlePage.search_fields` to include `body` (StreamField text content, excluding code blocks) and tags via `RelatedFields` 4. Add `AutocompleteField` on `title` for future type-ahead support 5. Add `FilterField` on `category` and `published_date` for filtered search 6. Run `./manage.py update_index` to build the search index (automated in deploy, not manual one-off) ## Implementation Plan ### 1. Backend: Configure Wagtail PostgreSQL Search - Add `django.contrib.postgres` to `INSTALLED_APPS` in `config/settings/base.py` - Add `WAGTAILSEARCH_BACKENDS` to `config/settings/base.py`: ```python WAGTAILSEARCH_BACKENDS = { "default": { "BACKEND": "wagtail.search.backends.database", "SEARCH_CONFIG": "english", } } ``` - Expand `ArticlePage.search_fields` using correct patterns for related data: ```python search_fields = Page.search_fields + [ index.SearchField("summary"), index.SearchField("body"), index.AutocompleteField("title"), index.RelatedFields("tags", [ index.SearchField("name"), ]), index.FilterField("category"), index.FilterField("published_date"), ] ``` - **Note:** `index.RelatedFields` is the correct pattern for tag/category name matching — `index.SearchField("tags")` would not work properly. ### 2. StreamField Body Indexing: Exclude Code Blocks - Indexing `body` (StreamField) will by default include all block text including code blocks, which produces noisy results (matching random code tokens like variable names). - Add a `get_search_text()` method (or use Wagtail's `search_index.get_searchable_content()` override) to extract only prose text from the StreamField, skipping `code` blocks. - This keeps search results relevant to article content, not code samples. ### 3. Backend: Search View - Add a search view in `apps/blog/views.py` that: - Accepts `?q=` query parameter - **Strips/normalises** the query — handle empty/whitespace-only queries explicitly (return empty results, not a full table scan) - **Max query length guard** (e.g., 200 chars) to protect against pathological requests - Uses `ArticlePage.objects.live().search(query)` (Wagtail search API) - If category/date filters are also applied: **filter the queryset first, then call `.search()`** (Wagtail docs require this ordering) - Paginates results (12 per page, matching article index) - **Preserves `q` on pagination links** (e.g., `?q=llm&page=2`) - Returns rendered HTML (server-side, no JS framework needed) - Wire up URL: `path("search/", search_view, name="search")` in `config/urls.py` - ⚠️ **Must be added before `path("", include(wagtail_urls))`** or Wagtail's catch-all will swallow it - Create search results template matching the article list design ### 4. Frontend: Navigation Search Box (Desktop) - Replace the "Subscribe" `<a>` button in `templates/components/nav.html` with a search form: ```html <form action="{% url 'search' %}" method="get" class="relative" role="search"> <svg class="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4 text-zinc-400">...</svg> <input type="search" name="q" placeholder="Search articles..." class="w-48 bg-brand-surfaceLight dark:bg-brand-surfaceDark border border-zinc-300 dark:border-zinc-700 pl-9 pr-3 py-2 font-mono text-sm focus:outline-none focus:border-brand-cyan focus:ring-1 focus:ring-brand-cyan transition-shadow" /> </form> ``` - Style to match the wireframe's design language (mono font, cyan focus ring) ### 5. Frontend: Navigation Search Box (Mobile) - Add a search form to the mobile menu in `nav.html`, above the newsletter form - Same form action/styling, full-width on mobile ### 6. Frontend: Article Index Page Search - Add the search box from the wireframe to `templates/blog/article_index_page.html` alongside the category/tag filters - This can submit to `/search/` or filter inline with a `?q=` param on the article index ### 7. Search Results Page - Create `templates/blog/search_results.html`: - Page header showing query and result count - Reuse `article_card.html` component for results - "No results" state with suggestions - Pagination matching article index style (preserving `?q=` across pages) ### 8. Reindex Lifecycle - `./manage.py update_index` must be **automated as part of deploy**, not a one-off manual step - Add to the deploy/entrypoint script so existing content is guaranteed indexed after every deploy - This ensures new `search_fields` additions are always reflected ### 9. Tests (including existing test updates) **New tests:** - Unit tests for the search view (query parsing, empty query, whitespace handling, max length, pagination with `?q=` preserved) - Test that `search_fields` indexes the expected fields - Test search results include matches from title, summary, and body (but not code blocks) - E2E test: type in nav search box → see results page **Existing tests that need updating** (due to Subscribe → Search nav change): - `apps/blog/tests/test_views.py::test_newsletter_forms_render_in_nav_and_footer` — currently asserts `href="#newsletter"` in nav; needs updating to expect search form instead - `e2e/test_home.py::test_nav_subscribe_cta_present` — currently asserts `Subscribe` link visible in nav; needs updating to expect search input - Newsletter E2E tests (`e2e/test_newsletter.py`) — these test the **sidebar** newsletter form, not the nav CTA, so should be unaffected - `e2e/test_article_detail.py::test_article_newsletter_aside_present` — tests sidebar, should be unaffected ## Design Notes - Follow the existing design language: mono font inputs, cyan focus rings, dark/light theme support - The nav search box should be compact (`w-48` on desktop) to not crowd the nav - Search results page should reuse the article card component for consistency - The newsletter "Subscribe" CTA remains in the footer and sidebar — only the nav placement changes - Use `type="search"` and `role="search"` for accessibility ## Out of Scope (for now) - Real-time/autocomplete search (type-ahead as you type) — can be added later with HTMX or Alpine.js - Search analytics/tracking - Elasticsearch/Meilisearch — overkill for current scale - Search within comments or author bios - Cross-page-model "site-wide" search (AboutPage, LegalPages, etc.)
Owner

Thanks for writing this up — the overall direction (Wagtail DB backend on Postgres, no extra search infra) looks right for this project size. I’d support this with a few important adjustments to avoid subtle bugs.

  1. Required app for Postgres search
  • We should include in when using the Postgres backend (per Wagtail docs), otherwise Postgres search features can be incomplete.
  1. shape for related data
  • is risky/not the right pattern for tag text matching.
  • Prefer so searches match tag names.
  • Same idea if we want category name/slug searchable.
  1. URL ordering
  • must be added before , or Wagtail’s catch-all will swallow it.
  1. Scope mismatch: “site-wide” vs article-only
  • Current plan/query examples are article-only (). That’s fine, but then we should call it article search.
  • If we truly want site-wide, we need an explicit cross-page model/query strategy.
  1. Query handling + pagination edge cases
  • Strip/normalize , handle empty query explicitly (don’t run full search on blank/whitespace).
  • Preserve on pagination links.
  • Add a max query length guard (cheap protection against pathological requests).
  1. Search + filters ordering
  • If we add category/date filters, apply queryset filters first, then call (Wagtail docs note this ordering).
  1. StreamField body indexing quality
  • Indexing is useful, but this will likely include code block text too. That may produce noisy results (e.g., matching random code tokens).
  • We should decide now whether code blocks should be searchable; if not, mark code block text as non-indexed.
  1. Reindex lifecycle
  • should be part of rollout/backfill, not just a one-off manual step, so existing content is guaranteed indexed after deploy.
  1. Test impact in this repo
  • Replacing nav “Subscribe” with search will break existing unit/e2e assertions that currently expect that CTA; plan should explicitly include those test updates.

If helpful, I can split this into a concrete implementation checklist (model/search_fields, URL/view, templates, and tests) to reduce risk during build.

Thanks for writing this up — the overall direction (Wagtail DB backend on Postgres, no extra search infra) looks right for this project size. I’d support this with a few important adjustments to avoid subtle bugs. 1. Required app for Postgres search - We should include in when using the Postgres backend (per Wagtail docs), otherwise Postgres search features can be incomplete. 2. shape for related data - is risky/not the right pattern for tag text matching. - Prefer so searches match tag names. - Same idea if we want category name/slug searchable. 3. URL ordering - must be added **before** , or Wagtail’s catch-all will swallow it. 4. Scope mismatch: “site-wide” vs article-only - Current plan/query examples are article-only (). That’s fine, but then we should call it article search. - If we truly want site-wide, we need an explicit cross-page model/query strategy. 5. Query handling + pagination edge cases - Strip/normalize , handle empty query explicitly (don’t run full search on blank/whitespace). - Preserve on pagination links. - Add a max query length guard (cheap protection against pathological requests). 6. Search + filters ordering - If we add category/date filters, apply queryset filters first, then call (Wagtail docs note this ordering). 7. StreamField body indexing quality - Indexing is useful, but this will likely include code block text too. That may produce noisy results (e.g., matching random code tokens). - We should decide now whether code blocks should be searchable; if not, mark code block text as non-indexed. 8. Reindex lifecycle - should be part of rollout/backfill, not just a one-off manual step, so existing content is guaranteed indexed after deploy. 9. Test impact in this repo - Replacing nav “Subscribe” with search will break existing unit/e2e assertions that currently expect that CTA; plan should explicitly include those test updates. If helpful, I can split this into a concrete implementation checklist (model/search_fields, URL/view, templates, and tests) to reduce risk during build.
Owner

Correcting formatting from my previous comment (the CLI ate inline code formatting). Reposting cleanly:

Thanks for writing this up — the overall direction (Wagtail DB backend on Postgres, no extra search infra) looks right for this project size. I’d support this with a few important adjustments to avoid subtle bugs.

  1. Required app for Postgres search
  • We should include django.contrib.postgres in INSTALLED_APPS when using the Postgres backend (per Wagtail docs), otherwise Postgres search features can be incomplete.
  1. search_fields shape for related data
  • index.SearchField("tags") is risky/not the right pattern for tag text matching.
  • Prefer index.RelatedFields("tags", [index.SearchField("name")]) so searches match tag names.
  • Same idea if we want category name/slug searchable.
  1. URL ordering
  • path("search/", ...) must be added before path("", include(wagtail_urls)), or Wagtail’s catch-all will swallow it.
  1. Scope mismatch: “site-wide” vs article-only
  • Current plan/query examples are article-only (ArticlePage.objects...). That’s fine, but then we should call it article search.
  • If we truly want site-wide, we need an explicit cross-page model/query strategy.
  1. Query handling + pagination edge cases
  • Strip/normalize q, handle empty query explicitly (don’t run full search on blank/whitespace).
  • Preserve q on pagination links.
  • Add a max query length guard (cheap protection against pathological requests).
  1. Search + filters ordering
  • If we add category/date filters, apply queryset filters first, then call .search() (Wagtail docs note this ordering).
  1. StreamField body indexing quality
  • Indexing body is useful, but this will likely include code block text too. That may produce noisy results (e.g., matching random code tokens).
  • We should decide now whether code blocks should be searchable; if not, mark code block text as non-indexed.
  1. Reindex lifecycle
  • update_index should be part of rollout/backfill, not just a one-off manual step, so existing content is guaranteed indexed after deploy.
  1. Test impact in this repo
  • Replacing nav “Subscribe” with search will break existing unit/e2e assertions that currently expect that CTA; plan should explicitly include those test updates.

If helpful, I can split this into a concrete implementation checklist (model/search_fields, URL/view, templates, and tests) to reduce risk during build.

Correcting formatting from my previous comment (the CLI ate inline code formatting). Reposting cleanly: Thanks for writing this up — the overall direction (Wagtail DB backend on Postgres, no extra search infra) looks right for this project size. I’d support this with a few important adjustments to avoid subtle bugs. 1. Required app for Postgres search - We should include `django.contrib.postgres` in `INSTALLED_APPS` when using the Postgres backend (per Wagtail docs), otherwise Postgres search features can be incomplete. 2. `search_fields` shape for related data - `index.SearchField("tags")` is risky/not the right pattern for tag text matching. - Prefer `index.RelatedFields("tags", [index.SearchField("name")])` so searches match tag names. - Same idea if we want category name/slug searchable. 3. URL ordering - `path("search/", ...)` must be added **before** `path("", include(wagtail_urls))`, or Wagtail’s catch-all will swallow it. 4. Scope mismatch: “site-wide” vs article-only - Current plan/query examples are article-only (`ArticlePage.objects...`). That’s fine, but then we should call it article search. - If we truly want site-wide, we need an explicit cross-page model/query strategy. 5. Query handling + pagination edge cases - Strip/normalize `q`, handle empty query explicitly (don’t run full search on blank/whitespace). - Preserve `q` on pagination links. - Add a max query length guard (cheap protection against pathological requests). 6. Search + filters ordering - If we add category/date filters, apply queryset filters first, then call `.search()` (Wagtail docs note this ordering). 7. StreamField body indexing quality - Indexing `body` is useful, but this will likely include code block text too. That may produce noisy results (e.g., matching random code tokens). - We should decide now whether code blocks should be searchable; if not, mark code block text as non-indexed. 8. Reindex lifecycle - `update_index` should be part of rollout/backfill, not just a one-off manual step, so existing content is guaranteed indexed after deploy. 9. Test impact in this repo - Replacing nav “Subscribe” with search will break existing unit/e2e assertions that currently expect that CTA; plan should explicitly include those test updates. If helpful, I can split this into a concrete implementation checklist (model/search_fields, URL/view, templates, and tests) to reduce risk during build.
mark changed title from Implement site-wide search with PostgreSQL full-text search to Implement article search with PostgreSQL full-text search 2026-03-03 21:09:39 +00:00
Author
Owner

All 9 points incorporated into the updated issue description. Here's what changed:

  1. django.contrib.postgres — added to INSTALLED_APPS step
  2. RelatedFields for tags — replaced SearchField("tags") with RelatedFields("tags", [SearchField("name")]) + added explicit note about why
  3. URL ordering — added warning that path("search/", ...) must come before wagtail_urls catch-all
  4. Scope clarified — renamed issue to "article search", added explicit scope section stating this is ArticlePage-only, cross-page-model is out of scope
  5. Query handling — added strip/normalize, empty/whitespace guard, max length guard (200 chars), preserve q on pagination links
  6. Filter-then-search ordering — documented that queryset filters must be applied before .search() per Wagtail docs
  7. Code block exclusion — added dedicated section (step 2) for StreamField body indexing that skips code blocks to avoid noisy results
  8. Reindex lifecycle — added step 8: update_index automated in deploy/entrypoint, not a manual one-off
  9. Test impact — added specific list of existing tests that need updating: test_newsletter_forms_render_in_nav_and_footer (asserts href="#newsletter") and test_nav_subscribe_cta_present (asserts Subscribe link). Confirmed newsletter E2E tests are sidebar-only so unaffected.
All 9 points incorporated into the updated issue description. Here's what changed: 1. **`django.contrib.postgres`** — added to INSTALLED_APPS step 2. **`RelatedFields` for tags** — replaced `SearchField("tags")` with `RelatedFields("tags", [SearchField("name")])` + added explicit note about why 3. **URL ordering** — added warning that `path("search/", ...)` must come before `wagtail_urls` catch-all 4. **Scope clarified** — renamed issue to "article search", added explicit scope section stating this is ArticlePage-only, cross-page-model is out of scope 5. **Query handling** — added strip/normalize, empty/whitespace guard, max length guard (200 chars), preserve `q` on pagination links 6. **Filter-then-search ordering** — documented that queryset filters must be applied before `.search()` per Wagtail docs 7. **Code block exclusion** — added dedicated section (step 2) for StreamField body indexing that skips code blocks to avoid noisy results 8. **Reindex lifecycle** — added step 8: `update_index` automated in deploy/entrypoint, not a manual one-off 9. **Test impact** — added specific list of existing tests that need updating: `test_newsletter_forms_render_in_nav_and_footer` (asserts `href="#newsletter"`) and `test_nav_subscribe_cta_present` (asserts Subscribe link). Confirmed newsletter E2E tests are sidebar-only so unaffected.
mark closed this issue 2026-03-03 21:58:08 +00:00
Sign in to join this conversation.
No Label
2 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: nohype/main-site#41