Skip to content

Commit 8ca31a5

Browse files
committed
Improve Lighthouse scores and add landing page benchmark
- Wrap blank template in rx.el.main landmark with page title/description - Add aria_label/title to ColorModeIconButton and StickyBadge - Add landing page prod benchmark alongside blank template test - Document page structure and metadata best practices
1 parent 2ec0b7a commit 8ca31a5

12 files changed

Lines changed: 800 additions & 73 deletions

File tree

.github/workflows/performance.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ jobs:
6767
env:
6868
REFLEX_RUN_LIGHTHOUSE: "1"
6969
run: |
70+
mkdir -p .pytest-tmp/lighthouse
7071
uv run pytest tests/integration/test_lighthouse.py -q -s --tb=no --basetemp=.pytest-tmp/lighthouse
7172
7273
- name: Upload Lighthouse artifacts

packages/reflex-base/src/reflex_base/.templates/apps/blank/code/blank.py

Lines changed: 28 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -11,26 +11,37 @@ class State(rx.State):
1111

1212
def index() -> rx.Component:
1313
# Welcome Page (Index)
14-
return rx.container(
15-
rx.color_mode.button(position="top-right"),
16-
rx.vstack(
17-
rx.heading("Welcome to Reflex!", size="9"),
18-
rx.text(
19-
"Get started by editing ",
20-
rx.code(f"{config.app_name}/{config.app_name}.py"),
21-
size="5",
14+
return rx.el.main(
15+
rx.container(
16+
rx.color_mode.button(position="top-right"),
17+
rx.vstack(
18+
rx.heading("Welcome to Reflex!", size="9"),
19+
rx.text(
20+
"Get started by editing ",
21+
rx.code(f"{config.app_name}/{config.app_name}.py"),
22+
size="5",
23+
),
24+
rx.button(
25+
rx.link(
26+
"Check out our docs!",
27+
href="https://reflex.dev/docs/getting-started/introduction/",
28+
is_external=True,
29+
underline="none",
30+
),
31+
as_child=True,
32+
high_contrast=True,
33+
),
34+
spacing="5",
35+
justify="center",
36+
min_height="85vh",
2237
),
23-
rx.link(
24-
rx.button("Check out our docs!"),
25-
href="https://reflex.dev/docs/getting-started/introduction/",
26-
is_external=True,
27-
),
28-
spacing="5",
29-
justify="center",
30-
min_height="85vh",
3138
),
3239
)
3340

3441

3542
app = rx.App()
36-
app.add_page(index)
43+
app.add_page(
44+
index,
45+
title="Welcome to Reflex",
46+
description="A starter Reflex app.",
47+
)

packages/reflex-components-core/src/reflex_components_core/core/sticky.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def create(cls):
8484
desktop_only(StickyLabel.create()),
8585
href="https://reflex.dev",
8686
target="_blank",
87+
aria_label="Built with Reflex",
88+
title="Built with Reflex",
8789
width="auto",
8890
padding="0.375rem",
8991
align="center",

packages/reflex-components-radix/src/reflex_components_radix/themes/color_mode.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ def create(
147147
props.setdefault("background", "transparent")
148148
props.setdefault("color", "inherit")
149149
props.setdefault("z_index", "20")
150+
props.setdefault("aria_label", "Toggle color mode")
151+
props.setdefault("title", "Toggle color mode")
150152
props.setdefault(":hover", {"cursor": "pointer"})
151153

152154
if allow_system:

pyi_hashes.json

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
{
2+
"packages/reflex-components-core/src/reflex_components_core/core/sticky.pyi": "4b34eca0e7338ec80ac5985345717bc9",
3+
"packages/reflex-components-radix/src/reflex_components_radix/themes/color_mode.pyi": "3a419f78071b0dd6be55dc55e7334a1b",
24
"reflex/__init__.pyi": "5de3d4af8ea86e9755f622510b868196",
35
"reflex/components/__init__.pyi": "f39a2af77f438fa243c58c965f19d42e",
46
"reflex/experimental/memo.pyi": "c10cbc554fe2ffdb3a008b59bc503936"

reflex/docs/getting_started/project-structure.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,8 @@ Initializing your project creates a directory with the same name as your app. Th
6464

6565
Reflex generates a default app within the `{app_name}/{app_name}.py` file. You can modify this file to customize your app.
6666

67+
The starter page also includes explicit page metadata. As you customize the app, update the page `title` and `description` in `app.add_page(...)` or `@rx.page(...)` so your production pages describe your project clearly.
68+
6769
## Python Project Files
6870

6971
`pyproject.toml` defines your Python project metadata and dependencies. `uv add reflex` records the Reflex dependency there before you initialize the app.

reflex/docs/pages/overview.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,26 @@ In this example we create three pages:
4545
# Video: Pages and URL Routes
4646
```
4747

48+
## Page Structure and Accessibility
49+
50+
For better accessibility and Lighthouse scores, wrap your page content in an `rx.el.main` element. This provides the `<main>` HTML landmark that screen readers and search engines use to identify the primary content of the page.
51+
52+
```python
53+
def index():
54+
return rx.el.main(
55+
navbar(),
56+
rx.container(
57+
rx.heading("Welcome"),
58+
rx.text("Page content here."),
59+
),
60+
footer(),
61+
)
62+
```
63+
64+
```md alert
65+
# Every page should have exactly one `<main>` landmark. Without it, accessibility tools like Lighthouse will flag the "Document does not have a main landmark" audit.
66+
```
67+
4868
## Page Decorator
4969

5070
You can also use the `@rx.page` decorator to add a page.
@@ -207,6 +227,8 @@ You can add page metadata such as:
207227
{meta_data}
208228
```
209229

230+
For production apps, set `title` and `description` explicitly on each public page with `@rx.page(...)` or `app.add_page(...)`. Reflex will use what you provide there, so it is best to treat page metadata as part of the page definition rather than something to fill in later.
231+
210232
## Getting the Current Page
211233

212234
You can access the current page from the `router` attribute in any state. See the [router docs](/docs/utility_methods/router_attributes) for all available attributes.

scripts/run_lighthouse.py

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,72 @@
1-
"""Run the local Lighthouse benchmark with persistent caching."""
1+
"""Run the local Lighthouse benchmark with a fresh app build."""
22

33
from __future__ import annotations
44

55
import contextlib
66
import io
7+
import shutil
8+
from collections.abc import Callable
79
from pathlib import Path
810

911
from tests.integration.lighthouse_utils import (
10-
get_local_cached_app_root,
12+
LIGHTHOUSE_APP_NAME,
13+
LIGHTHOUSE_LANDING_APP_NAME,
14+
LighthouseBenchmarkResult,
1115
run_blank_prod_lighthouse_benchmark,
16+
run_landing_prod_lighthouse_benchmark,
1217
)
1318

1419

15-
def main() -> int:
16-
"""Run the Lighthouse benchmark and print a compact summary.
20+
def _run_benchmark(
21+
run_fn: Callable[..., LighthouseBenchmarkResult],
22+
app_root: Path,
23+
report_path: Path,
24+
) -> LighthouseBenchmarkResult:
25+
"""Run a single benchmark, suppressing internal output.
1726
1827
Returns:
19-
The process exit code.
28+
The benchmark result.
2029
"""
21-
app_root = get_local_cached_app_root()
22-
report_path = Path(".states") / "lighthouse" / "blank-prod-lighthouse.json"
23-
30+
shutil.rmtree(app_root, ignore_errors=True)
2431
stdout_buffer = io.StringIO()
2532
stderr_buffer = io.StringIO()
2633
with (
2734
contextlib.redirect_stdout(stdout_buffer),
2835
contextlib.redirect_stderr(stderr_buffer),
2936
):
30-
result = run_blank_prod_lighthouse_benchmark(
31-
app_root=app_root,
32-
report_path=report_path,
33-
)
34-
35-
print(result.summary) # noqa: T201
36-
if result.failures:
37-
return 1
38-
return 0
37+
return run_fn(app_root=app_root, report_path=report_path)
38+
39+
40+
def main() -> int:
41+
"""Run the Lighthouse benchmarks and print compact summaries.
42+
43+
Returns:
44+
The process exit code.
45+
"""
46+
report_dir = Path(".states") / "lighthouse"
47+
all_failures = []
48+
49+
benchmarks = [
50+
(
51+
LIGHTHOUSE_APP_NAME,
52+
run_blank_prod_lighthouse_benchmark,
53+
report_dir / "blank-prod-lighthouse.json",
54+
),
55+
(
56+
LIGHTHOUSE_LANDING_APP_NAME,
57+
run_landing_prod_lighthouse_benchmark,
58+
report_dir / "landing-prod-lighthouse.json",
59+
),
60+
]
61+
62+
for name, run_fn, report_path in benchmarks:
63+
app_root = Path(".states") / name
64+
result = _run_benchmark(run_fn, app_root, report_path)
65+
print(result.summary) # noqa: T201
66+
print() # noqa: T201
67+
all_failures.extend(result.failures)
68+
69+
return 1 if all_failures else 0
3970

4071

4172
if __name__ == "__main__":

0 commit comments

Comments
 (0)