Skip to content

Re-implement SEO support for website (ported to website/components/ structure) - #137

Open
evnchn wants to merge 4 commits into
mainfrom
seo-pr-clean-reimpl-2026-04-15
Open

Re-implement SEO support for website (ported to website/components/ structure)#137
evnchn wants to merge 4 commits into
mainfrom
seo-pr-clean-reimpl-2026-04-15

Conversation

@evnchn

@evnchn evnchn commented Apr 15, 2026

Copy link
Copy Markdown
Owner

Re-implementation of ex-upstream zauberzeug#5813 against new website/ structure from zauberzeug#5910.

What stays: website/seo.py, website/static/header.html, tests/test_seo.py.

What changed: SEO wiring distributed across new website/components/*.py.

Status: Fork PR for user review before upstream re-submission.

evnchn and others added 3 commits April 15, 2026 15:44
Re-implementation of ex-upstream PR zauberzeug#5813 against the new
website/components/ structure introduced by upstream zauberzeug#5910.

- New website/seo.py: meta/OG/Twitter/canonical tag builders,
  breadcrumb JSON-LD, markdown/rst description extractor
- Per-page wiring in main_page, examples_page, imprint_privacy,
  and documentation.rendering (dispatcher-level integration)
- Organization + SoftwareApplication JSON-LD injected from
  header.add_head_html so every page carries it
- /robots.txt and /sitemap.xml routes in main.py, sitemap
  generated from documentation.registry
- website/tests/test_seo.py added to pytest testpaths
- website/static/header.html: remove static meta description
  (now emitted per-page by seo.page_seo_html)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
website/tests/ lacked the tests/conftest.py fixtures, leaving unraisable
coroutines dangling from earlier tests. Dropping the extra testpath fixes
test_module_import_isolation_first_test.
…lection

Empty out website/__init__.py so importing a leaf module such as
`website.seo` no longer drags in `website.documentation` and friends.
Those packages call `with Client(...)` at import time to pre-render
documentation UI, which schedules `Outbox.loop` via
`background_tasks.create_or_defer`. When no event loop is running
(pytest collection) the coroutine is deferred to `app.on_startup`,
never awaited, and finalized by the GC - producing ~25
`RuntimeWarning: coroutine 'Outbox.loop' was never awaited` entries
that the pytest `unraisableexception` plugin re-raises in the setup
hook of the first test (`test_module_import_isolation_first_test`).

`main.py` already imports the specific submodules it needs
(`from website import documentation, examples_page, ...`), so an
empty `__init__.py` is sufficient for the production website.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Re-implements SEO support for the NiceGUI documentation website under the newer website/components-based structure by generating per-page meta tags/structured data and adding robots.txt + sitemap.xml endpoints.

Changes:

  • Add centralized SEO helpers (page_seo_html, breadcrumb_jsonld, extract_description) and accompanying unit tests.
  • Wire per-page SEO metadata (description/canonical/OpenGraph/Twitter + breadcrumbs) into main, examples, imprint/privacy, and documentation rendering flows.
  • Add /robots.txt and /sitemap.xml, and adjust website/__init__.py to avoid eager imports that trigger pytest warnings.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
website/static/header.html Removes hardcoded meta description so descriptions can be page-specific.
website/seo.py Introduces helpers for meta tags, JSON-LD breadcrumbs, and description extraction.
website/main_page.py Adds page title + SEO head injection for the homepage.
website/imprint_privacy.py Adds SEO head injection + breadcrumbs for imprint/privacy page.
website/header.py Adds sitewide JSON-LD (Organization + SoftwareApplication) to the shared header head HTML.
website/examples_page.py Adds SEO head injection + breadcrumbs for examples page.
website/documentation/rendering.py Derives per-doc-page descriptions/titles and injects SEO head + breadcrumbs.
website/init.py Removes eager imports to prevent side effects during test collection.
tests/test_seo.py Adds tests for description extraction, SEO HTML generation, and JSON-LD escaping.
main.py Adds robots.txt and sitemap.xml endpoints and imports website.seo constants.

Comment thread website/examples_page.py Outdated
Comment on lines +10 to +15
title = 'NiceGUI Examples - Python UI Code Samples and Demos'
description = ('Browse in-depth NiceGUI examples including authentication, chat apps, todo lists, and more. '
'See real Python GUI code with live demos.')
ui.page_title(title)
ui.add_head_html(page_seo_html(title=title, description=description, path='/examples'))
ui.add_head_html(breadcrumb_jsonld([('Home', '/'), ('Examples', '/examples')]))

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ui.add_head_html(...) is executed on every sub-page render. Because NiceGUI's add_head_html appends to document.head, client-side navigation will accumulate duplicate meta tags/canonical links and breadcrumb JSON-LD. Consider removing/overwriting previously injected SEO elements instead of always inserting new ones.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. Fixed in 547d136: introduced apply_page_seo(...) in website/seo.py. Each emitted tag carries a data-nicegui-seo attribute; the first call per client uses ui.add_head_html (so SSR HTML has them for crawlers), subsequent SPA-nav calls remove tagged elements via ui.run_javascript before re-inserting, preventing accumulation.

Comment thread website/imprint_privacy.py Outdated
def create() -> None:
title = 'Imprint & Privacy Policy - NiceGUI'
description = 'Legal information, imprint, and privacy policy for NiceGUI by Zauberzeug GmbH.'
ui.page_title('Imprint & Privacy | NiceGUI')

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The HTML title set via ui.page_title(...) does not match the SEO title passed to page_seo_html(...) (Imprint & Privacy | NiceGUI vs Imprint & Privacy Policy - NiceGUI). This creates inconsistent <title>/OpenGraph/Twitter titles for the same page. Align these values (or derive one from the other) so a single canonical title is used across tags.

Suggested change
ui.page_title('Imprint & Privacy | NiceGUI')
ui.page_title(title)

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. Fixed in 547d136: ui.page_title(title) now uses the same title variable as the SEO title, so <title>/OpenGraph/Twitter are consistent.

Comment thread website/imprint_privacy.py Outdated
Comment on lines +12 to +13
ui.add_head_html(page_seo_html(title=title, description=description, path='/imprint_privacy'))
ui.add_head_html(breadcrumb_jsonld([('Home', '/'), ('Imprint & Privacy', '/imprint_privacy')]))

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These ui.add_head_html(...) calls run inside a ui.sub_pages route handler. Since add_head_html appends/inserts into document.head, navigating within the SPA will add duplicate canonical/OG/Twitter tags and breadcrumb JSON-LD blocks over time. Consider cleaning up previously injected SEO tags (e.g., remove elements with a dedicated marker attribute) before adding new ones.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. Fixed in 547d136: introduced apply_page_seo(...) in website/seo.py. Each emitted tag carries a data-nicegui-seo attribute; the first call per client uses ui.add_head_html (so SSR HTML has them for crawlers), subsequent SPA-nav calls remove tagged elements via ui.run_javascript before re-inserting, preventing accumulation.

Comment thread website/documentation/rendering.py Outdated
Comment on lines +62 to +75
description = _build_page_description(documentation)
path = f'/documentation/{documentation.name}' if documentation.name else '/documentation'
ui.add_head_html(page_seo_html(title=seo_title, description=description, path=path, og_type='article'))

breadcrumbs = [('Home', '/'), ('Documentation', '/documentation')]
if documentation.name:
if documentation.back_link is not None:
from .content import registry
parent = registry.get(documentation.back_link)
if parent and parent.title:
parent_title = parent.title.replace('*', '')
breadcrumbs.append((parent_title, f'/documentation/{documentation.back_link}'))
breadcrumbs.append((title, path))
ui.add_head_html(breadcrumb_jsonld(breadcrumbs))

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ui.add_head_html(...) is called as part of sub-page rendering. With ui.sub_pages SPA navigation, add_head_html keeps appending into document.head, so documentation navigation can accumulate many duplicate canonical/OG/Twitter tags and breadcrumb JSON-LD scripts. Consider switching to an update-in-place approach (remove/replace previously injected SEO elements) to avoid head bloat and ambiguous metadata.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. Fixed in 547d136: introduced apply_page_seo(...) in website/seo.py. Each emitted tag carries a data-nicegui-seo attribute; the first call per client uses ui.add_head_html (so SSR HTML has them for crawlers), subsequent SPA-nav calls remove tagged elements via ui.run_javascript before re-inserting, preventing accumulation.

Comment thread tests/test_seo.py
Comment on lines +1 to +2
from website.seo import breadcrumb_jsonld, extract_description, page_seo_html

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR description says "What stays: ... website/tests/test_seo.py", but the tests added/modified here are under tests/test_seo.py (and there is no website/tests/ directory). Please update the PR description (or move the test file) so reviewers/users can find the correct location.

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. PR description updated: path now correctly reads tests/test_seo.py (no stale website/tests/ reference).

Comment thread website/main_page.py Outdated
Comment on lines +19 to +22
title = 'NiceGUI - Easy-to-Use Python-Based UI Framework'
ui.page_title(title)
ui.add_head_html(page_seo_html(title=title, description=DEFAULT_DESCRIPTION, path='/'))
ui.add_head_html(breadcrumb_jsonld([('Home', '/')]))

Copilot AI Apr 15, 2026

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ui.add_head_html(...) is called inside a ui.sub_pages route builder. In SPA navigation, add_head_html appends/inserts into document.head without removing previous tags, so navigating between sub-pages will accumulate duplicate canonical/OG/Twitter tags and multiple breadcrumb JSON-LD blocks. Consider replacing/removing previously injected SEO tags (e.g., mark them with a data-... attribute and remove via ui.run_javascript before inserting, or implement an update-in-place helper instead of repeated add_head_html).

Copilot uses AI. Check for mistakes.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Valid. Fixed in 547d136: introduced apply_page_seo(...) in website/seo.py. Each emitted tag carries a data-nicegui-seo attribute; the first call per client uses ui.add_head_html (so SSR HTML has them for crawlers), subsequent SPA-nav calls remove tagged elements via ui.run_javascript before re-inserting, preventing accumulation.

Per-page SEO was injected via `ui.add_head_html(...)` inside sub_pages
route handlers. Because SPA navigation keeps appending to document.head
(and to the server-side client._head_html buffer), repeated navigation
between pages duplicated every meta/canonical/og/twitter tag and every
breadcrumb JSON-LD block in the live DOM.

Introduce `apply_page_seo(...)` that tags every emitted element with a
`data-nicegui-seo` attribute. The first call per client uses
`add_head_html` so the tags land in the SSR HTML that crawlers see;
subsequent calls patch the live DOM via `ui.run_javascript`, removing
previously injected tags before appending the new set. Callers in
main_page, examples_page, imprint_privacy, and documentation/rendering
switch to the helper.

Also align `ui.page_title` on the imprint/privacy page with the SEO
title (previously "Imprint & Privacy | NiceGUI" vs. "Imprint & Privacy
Policy - NiceGUI") so there is a single canonical title across
<title>/OpenGraph/Twitter.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants