forked from zauberzeug/nicegui
-
Notifications
You must be signed in to change notification settings - Fork 0
Re-implement SEO support for website (ported to website/components/ structure) #137
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
evnchn
wants to merge
4
commits into
main
Choose a base branch
from
seo-pr-clean-reimpl-2026-04-15
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
Show all changes
4 commits
Select commit
Hold shift + click to select a range
a053826
Add comprehensive SEO support for documentation website
evnchn 328747e
Move test_seo.py into tests/ to pick up conftest.py
evnchn cf69b7d
Avoid eager website imports leaking Outbox coroutines during test col…
evnchn 547d136
Prevent SEO tag accumulation across ui.sub_pages navigation
evnchn File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,178 @@ | ||
| from website.seo import breadcrumb_jsonld, extract_description, page_seo_html | ||
|
|
||
|
|
||
| def test_extract_description_plain_text(): | ||
| text = 'This is a simple description that is long enough to pass the minimum length threshold for extraction.' | ||
| result = extract_description(text) | ||
| assert result == text | ||
|
|
||
|
|
||
| def test_extract_description_returns_none_for_short_text(): | ||
| assert extract_description('Too short') is None | ||
| assert extract_description('') is None | ||
|
|
||
|
|
||
| def test_extract_description_strips_markdown_bold(): | ||
| text = 'This is **bold text** that should be cleaned up properly by the extraction function.' | ||
| result = extract_description(text) | ||
| assert '**' not in result | ||
| assert 'bold text' in result | ||
|
|
||
|
|
||
| def test_extract_description_strips_markdown_italic(): | ||
| text = 'This is _italic text_ that should be cleaned up properly by the extraction function.' | ||
| result = extract_description(text) | ||
| assert result is not None | ||
| assert '_italic' not in result | ||
| assert 'italic text' in result | ||
|
|
||
|
|
||
| def test_extract_description_strips_markdown_links(): | ||
| text = 'Click [this link](https://example.com) to visit the site and learn about the features.' | ||
| result = extract_description(text) | ||
| assert result is not None | ||
| assert 'this link' in result | ||
| assert 'https://example.com' not in result | ||
| assert '[' not in result | ||
| assert '](' not in result | ||
|
|
||
|
|
||
| def test_extract_description_strips_rst_links(): | ||
| text = 'See `NiceGUI documentation <https://nicegui.io>`_ for more information about all features.' | ||
| result = extract_description(text) | ||
| assert result is not None | ||
| assert 'NiceGUI documentation' in result | ||
| assert 'https://nicegui.io' not in result | ||
| assert '`' not in result | ||
|
|
||
|
|
||
| def test_extract_description_strips_backtick_code(): | ||
| text = 'Use the `ui.button` element to create interactive buttons in your Python application.' | ||
| result = extract_description(text) | ||
| assert result is not None | ||
| assert 'ui.button' in result | ||
| assert '`' not in result | ||
|
|
||
|
|
||
| def test_extract_description_strips_html_tags(): | ||
| text = 'This has <b>bold HTML</b> and <a href="url">a link</a> that need to be properly cleaned.' | ||
| result = extract_description(text) | ||
| assert result is not None | ||
| assert '<b>' not in result | ||
| assert '<a ' not in result | ||
| assert 'bold HTML' in result | ||
|
|
||
|
|
||
| def test_extract_description_truncates_long_text(): | ||
| text = 'A ' * 200 # very long text | ||
| result = extract_description(text) | ||
| assert result is not None | ||
| assert len(result) <= 160 | ||
| assert result.endswith('...') | ||
|
|
||
|
|
||
| def test_extract_description_truncates_at_word_boundary(): | ||
| text = 'word ' * 50 # 250 chars | ||
| result = extract_description(text) | ||
| assert result is not None | ||
| assert len(result) <= 160 | ||
| assert result.endswith('...') | ||
| assert not result.endswith(' ...') # should not have trailing space before ellipsis | ||
|
|
||
|
|
||
| def test_extract_description_strips_param_directives(): | ||
| text = ('This function does something useful and important for the application.\n' | ||
| ':param name: the name of the element\n' | ||
| ':type name: str') | ||
| result = extract_description(text) | ||
| assert result is not None | ||
| assert ':param' not in result | ||
| assert 'something useful' in result | ||
|
|
||
|
|
||
| def test_extract_description_strips_return_directives(): | ||
| text = ('This function returns a value that is useful for the calling application.\n' | ||
| ':return the computed value') | ||
| result = extract_description(text) | ||
| assert result is not None | ||
| assert ':return' not in result | ||
|
|
||
|
|
||
| def test_extract_description_collapses_whitespace(): | ||
| text = 'This has lots\n\nof whitespace scattered throughout the entire text string.' | ||
| result = extract_description(text) | ||
| assert result is not None | ||
| assert ' ' not in result | ||
|
|
||
|
|
||
| def test_page_seo_html_contains_meta_description(): | ||
| result = page_seo_html(title='Test', description='A test page', path='/test') | ||
| assert 'name="description"' in result | ||
| assert 'A test page' in result | ||
|
|
||
|
|
||
| def test_page_seo_html_tags_carry_seo_marker(): | ||
| # SPA navigation removes previously injected SEO tags via this marker; | ||
| # every emitted tag must carry it so the cleanup query catches them. | ||
| result = page_seo_html(title='Test', description='A test page', path='/test') | ||
| for line in result.splitlines(): | ||
| assert 'data-nicegui-seo' in line, f'missing SEO marker on: {line}' | ||
|
|
||
|
|
||
| def test_page_seo_html_contains_canonical(): | ||
| result = page_seo_html(title='Test', description='A test page', path='/test') | ||
| assert 'rel="canonical"' in result | ||
| assert 'https://nicegui.io/test' in result | ||
|
|
||
|
|
||
| def test_page_seo_html_contains_open_graph(): | ||
| result = page_seo_html(title='Test', description='A test page', path='/test') | ||
| assert 'og:title' in result | ||
| assert 'og:description' in result | ||
| assert 'og:url' in result | ||
| assert 'og:type' in result | ||
| assert 'og:site_name' in result | ||
|
|
||
|
|
||
| def test_page_seo_html_contains_twitter_card(): | ||
| result = page_seo_html(title='Test', description='A test page', path='/test') | ||
| assert 'twitter:card' in result | ||
| assert 'twitter:title' in result | ||
|
|
||
|
|
||
| def test_page_seo_html_escapes_special_characters(): | ||
| result = page_seo_html(title='Test & "Quotes"', description='A <b>bold</b> description', path='/test') | ||
| assert 'Test & "Quotes"' in result | ||
| assert '<b>bold</b>' in result | ||
|
|
||
|
|
||
| def test_page_seo_html_og_type_default(): | ||
| result = page_seo_html(title='Test', description='Desc', path='/') | ||
| assert 'content="website"' in result | ||
|
|
||
|
|
||
| def test_page_seo_html_og_type_article(): | ||
| result = page_seo_html(title='Test', description='Desc', path='/', og_type='article') | ||
| assert 'content="article"' in result | ||
|
|
||
|
|
||
| def test_breadcrumb_jsonld_structure(): | ||
| result = breadcrumb_jsonld([('Home', '/'), ('Docs', '/docs')]) | ||
| assert 'type="application/ld+json"' in result | ||
| assert '</script>' in result | ||
| assert 'BreadcrumbList' in result | ||
| assert '"position":1' in result | ||
| assert '"position":2' in result | ||
| assert '"name":"Home"' in result | ||
| assert '"name":"Docs"' in result | ||
|
|
||
|
|
||
| def test_breadcrumb_jsonld_includes_full_urls(): | ||
| result = breadcrumb_jsonld([('Home', '/'), ('Docs', '/docs')]) | ||
| assert 'https://nicegui.io/' in result | ||
| assert 'https://nicegui.io/docs' in result | ||
|
|
||
|
|
||
| def test_breadcrumb_jsonld_escapes_closing_script(): | ||
| result = breadcrumb_jsonld([('Test</script>', '/test')]) | ||
| assert '</script><' not in result.replace('</script>', '', 1) # only the closing tag itself | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,9 +1,14 @@ | ||
| from . import documentation, examples_page, fly, main_page, svg | ||
| """NiceGUI documentation website package. | ||
|
|
||
| __all__ = [ | ||
| 'documentation', | ||
| 'examples_page', | ||
| 'fly', | ||
| 'main_page', | ||
| 'svg', | ||
| ] | ||
| This ``__init__`` intentionally does not eagerly import submodules. | ||
| ``main.py`` imports the submodules it needs explicitly, and eagerly | ||
| importing them here pulls in documentation modules that instantiate | ||
| ``Client`` objects at module load time (to pre-render UI snippets). | ||
| Those ``Client`` objects schedule an ``Outbox.loop`` coroutine via | ||
| ``background_tasks.create_or_defer``; when there is no running event | ||
| loop (e.g. during pytest collection of ``tests/test_seo.py``), the | ||
| coroutines are deferred to ``app.on_startup``, never awaited, and | ||
| eventually garbage-collected, producing ``RuntimeWarning`` entries | ||
| that break the ``unraisableexception`` plugin's setup hook for the | ||
| first test. | ||
| """ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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 nowebsite/tests/directory). Please update the PR description (or move the test file) so reviewers/users can find the correct location.There was a problem hiding this comment.
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 stalewebsite/tests/reference).