Skip to content

Commit 1fbdfbb

Browse files
authored
SEO Compatible Rendering (#186)
- SEO compatible rendering - Prerenders the initial component via the template tag using `vdom_to_html`, then loads the actual component in the background within a `hidden` div. When loaded, the prerender is replaced with the actual render. - `settings.py:REACTPY_PRERENDER` can be set to `True` to enable this behavior by default - Enable it on individual components via the template tag: `{% component "..." prerender="True" %}` - Docs styling, verbiage, and formatting tweaks - Rename undocumented utility function `reactpy_django.utils.ComponentPreloader` to `reactpy_django.utils.RootComponentFinder`. - Fix JavaScript being via `pip install -e .` on Windows. - Update PyPi package metadata - Update pull request template
1 parent 749e707 commit 1fbdfbb

39 files changed

+487
-126
lines changed

.github/pull_request_template.md

+6-5
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
*By submitting this pull request you agree that all contributions to this project are made under the MIT license.*
2-
31
## Description
42

53
A summary of the changes.
@@ -8,6 +6,9 @@ A summary of the changes.
86

97
Please update this checklist as you complete each item:
108

11-
- [ ] Tests have been included for all bug fixes or added functionality.
12-
- [ ] The changelog has been updated with any significant changes, if necessary.
13-
- [ ] GitHub Issues which may be closed by this PR have been linked.
9+
- [ ] Tests have been developed for bug fixes or new functionality.
10+
- [ ] The changelog has been updated, if necessary.
11+
- [ ] Documentation has been updated, if necessary.
12+
- [ ] GitHub Issues closed by this PR have been linked.
13+
14+
<sub>By submitting this pull request you agree that all contributions comply with this project's open source license(s).</sub>

CHANGELOG.md

+12
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,18 @@ Using the following categories, list your changes in this order:
3232

3333
<!--changelog-start-->
3434

35+
## [Unreleased]
36+
37+
### Added
38+
39+
- ReactPy components can now use SEO compatible rendering!
40+
- `settings.py:REACTPY_PRERENDER` can be set to `True` to enable this behavior by default
41+
- Or, you can enable it on individual components via the template tag: `{% component "..." prerender="True" %}`
42+
43+
### Changed
44+
45+
- Renamed undocumented utility function `reactpy_django.utils.ComponentPreloader` to `reactpy_django.utils.RootComponentFinder`.
46+
3547
## [3.5.1] - 2023-09-07
3648

3749
### Added

docs/python/template-tag-bad-view.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,5 @@
22

33

44
def example_view(request):
5-
context_vars = {"dont_do_this": "example_project.my_app.components.hello_world"}
5+
context_vars = {"my_variable": "example_project.my_app.components.hello_world"}
66
return render(request, "my-template.html", context_vars)

docs/src/about/code.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Then, by running the command below you can:
3535
- Download, build, and install Javascript dependencies
3636

3737
```bash linenums="0"
38-
pip install -e . -r requirements.txt
38+
pip install -e . -r requirements.txt --verbose --upgrade
3939
```
4040

4141
!!! warning "Pitfall"

docs/src/about/docs.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ Then, by running the command below you can:
2828
- Self-host a test server for the documentation
2929

3030
```bash linenums="0"
31-
pip install -e . -r requirements.txt --upgrade
31+
pip install -r requirements.txt --upgrade
3232
```
3333

3434
Finally, to verify that everything is working properly, you can manually run the docs preview web server.

docs/src/assets/css/admonition.css

+15-15
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,20 @@
11
[data-md-color-scheme="slate"] {
22
--admonition-border-color: transparent;
33
--admonition-expanded-border-color: rgba(255, 255, 255, 0.1);
4-
--note-bg-color: rgb(43 110 98/ 0.2);
4+
--note-bg-color: rgba(43, 110, 98, 0.2);
55
--terminal-bg-color: #0c0c0c;
66
--terminal-title-bg-color: #000;
7-
--deep-dive-bg-color: rgb(43 52 145 / 0.2);
7+
--deep-dive-bg-color: rgba(43, 52, 145, 0.2);
88
--you-will-learn-bg-color: #353a45;
9-
--pitfall-bg-color: rgb(182 87 0 / 0.2);
9+
--pitfall-bg-color: rgba(182, 87, 0, 0.2);
1010
}
1111
[data-md-color-scheme="default"] {
1212
--admonition-border-color: rgba(0, 0, 0, 0.08);
1313
--admonition-expanded-border-color: var(--admonition-border-color);
14-
--note-bg-color: rgb(244 251 249);
15-
--terminal-bg-color: rgb(64 71 86);
16-
--terminal-title-bg-color: rgb(35 39 47);
17-
--deep-dive-bg-color: rgb(243 244 253);
14+
--note-bg-color: rgb(244, 251, 249);
15+
--terminal-bg-color: rgb(64, 71, 86);
16+
--terminal-title-bg-color: rgb(35, 39, 47);
17+
--deep-dive-bg-color: rgb(243, 244, 253);
1818
--you-will-learn-bg-color: rgb(246, 247, 249);
1919
--pitfall-bg-color: rgb(254, 245, 231);
2020
}
@@ -81,12 +81,12 @@ React Name: "Note"
8181
font-size: 1rem;
8282
background: transparent;
8383
padding-bottom: 0;
84-
color: rgb(68 172 153);
84+
color: rgb(68, 172, 153);
8585
}
8686

8787
.md-typeset .note .admonition-title:before {
8888
font-size: 1.1rem;
89-
background: rgb(68 172 153);
89+
background: rgb(68, 172, 153);
9090
}
9191

9292
.md-typeset .note > .admonition-title:before,
@@ -109,12 +109,12 @@ React Name: "Pitfall"
109109
font-size: 1rem;
110110
background: transparent;
111111
padding-bottom: 0;
112-
color: rgb(219 125 39);
112+
color: rgb(219, 125, 39);
113113
}
114114

115115
.md-typeset .warning .admonition-title:before {
116116
font-size: 1.1rem;
117-
background: rgb(219 125 39);
117+
background: rgb(219, 125, 39);
118118
}
119119

120120
/*
@@ -131,12 +131,12 @@ React Name: "Deep Dive"
131131
font-size: 1rem;
132132
background: transparent;
133133
padding-bottom: 0;
134-
color: rgb(136 145 236);
134+
color: rgb(136, 145, 236);
135135
}
136136

137137
.md-typeset .info .admonition-title:before {
138138
font-size: 1.1rem;
139-
background: rgb(136 145 236);
139+
background: rgb(136, 145, 236);
140140
}
141141

142142
/*
@@ -152,11 +152,11 @@ React Name: "Terminal"
152152

153153
.md-typeset .example .admonition-title {
154154
background: var(--terminal-title-bg-color);
155-
color: rgb(246 247 249);
155+
color: rgb(246, 247, 249);
156156
}
157157

158158
.md-typeset .example .admonition-title:before {
159-
background: rgb(246 247 249);
159+
background: rgb(246, 247, 249);
160160
}
161161

162162
.md-typeset .admonition.example code {

docs/src/assets/css/code.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -9,7 +9,7 @@
99
--md-code-hl-color: #ffffcf1c;
1010
--md-code-bg-color: #16181d;
1111
--md-code-hl-comment-color: hsla(var(--md-hue), 75%, 90%, 0.43);
12-
--code-tab-color: rgb(52 58 70);
12+
--code-tab-color: rgb(52, 58, 70);
1313
--md-code-hl-name-color: #aadafc;
1414
--md-code-hl-string-color: hsl(21 49% 63% / 1);
1515
--md-code-hl-keyword-color: hsl(289.67deg 35% 60%);

docs/src/assets/css/main.css

+2-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
--reactpy-color: #58b962;
44
--reactpy-color-dark: #42914a;
55
--reactpy-color-darker: #34743b;
6-
--reactpy-color-opacity-10: rgb(88 185 98 / 10%);
6+
--reactpy-color-opacity-10: rgba(88, 185, 98, 0.1);
77
}
88

99
[data-md-color-accent="red"] {
@@ -12,7 +12,7 @@
1212
}
1313

1414
[data-md-color-scheme="slate"] {
15-
--md-default-bg-color: rgb(35 39 47);
15+
--md-default-bg-color: rgb(35, 39, 47);
1616
--md-default-bg-color--light: hsla(var(--md-hue), 15%, 16%, 0.54);
1717
--md-default-bg-color--lighter: hsla(var(--md-hue), 15%, 16%, 0.26);
1818
--md-default-bg-color--lightest: hsla(var(--md-hue), 15%, 16%, 0.07);

docs/src/assets/css/navbar.css

+20-4
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
[data-md-color-scheme="slate"] {
22
--md-header-border-color: rgb(255 255 255 / 5%);
3+
--md-version-bg-color: #ffffff0d;
34
}
45

56
[data-md-color-scheme="default"] {
67
--md-header-border-color: rgb(0 0 0 / 7%);
8+
--md-version-bg-color: #ae58ee2e;
79
}
810

911
.md-header {
@@ -28,12 +30,20 @@
2830
}
2931

3032
.md-version__list {
31-
margin: 0.2rem -0.8rem;
33+
margin: 0;
34+
left: 0;
35+
right: 0;
36+
top: 2.5rem;
3237
}
3338

34-
[dir="ltr"] .md-header__title.md-header__title--active {
35-
margin: 0;
36-
transition: margin 0.35s ease;
39+
.md-version {
40+
background: var(--md-version-bg-color);
41+
border-radius: 999px;
42+
padding: 0 0.8rem;
43+
margin: 0.3rem 0;
44+
height: 1.8rem;
45+
display: flex;
46+
font-size: 0.7rem;
3747
}
3848

3949
/* Mobile Styling */
@@ -97,6 +107,12 @@
97107
.md-header__topic {
98108
position: relative;
99109
}
110+
.md-header__title--active .md-header__topic {
111+
transform: none;
112+
opacity: 1;
113+
pointer-events: auto;
114+
z-index: 4;
115+
}
100116

101117
/* Search */
102118
.md-search {

docs/src/assets/css/sidebar.css

+1-1
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@
2828
}
2929

3030
.md-nav--lifted > .md-nav__list > .md-nav__item--active > .md-nav__link {
31-
color: rgb(133 142 159);
31+
color: rgb(133, 142, 159);
3232
margin: 0.5rem;
3333
}
3434

docs/src/learn/add-reactpy-to-a-django-project.md

+2
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,8 @@ Add `#!python "reactpy_django"` to [`INSTALLED_APPS`](https://docs.djangoproject
5353

5454
??? note "Configure ReactPy settings (Optional)"
5555

56+
{% include "../reference/settings.md" start="<!--intro-start-->" end="<!--intro-end-->" %}
57+
5658
{% include "../reference/settings.md" start="<!--config-table-start-->" end="<!--config-table-end-->" %}
5759

5860
## Step 3: Configure `urls.py`

docs/src/reference/settings.md

+26-13
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,11 @@
22

33
<p class="intro" markdown>
44

5-
Your **Django project's** `settings.py` can modify the behavior of ReactPy.
5+
<!--intro-start-->
6+
7+
These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy.
8+
9+
<!--intro-end-->
610

711
</p>
812

@@ -14,25 +18,34 @@ Your **Django project's** `settings.py` can modify the behavior of ReactPy.
1418

1519
---
1620

17-
## Primary Configuration
18-
1921
<!--config-table-start-->
2022

21-
These are ReactPy-Django's default settings values. You can modify these values in your **Django project's** `settings.py` to change the behavior of ReactPy.
23+
## General Settings
2224

2325
| Setting | Default Value | Example Value(s) | Description |
2426
| --- | --- | --- | --- |
25-
| `#!python REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used to store ReactPy web modules. ReactPy benefits from a fast, well indexed cache. We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). |
26-
| `#!python REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Database used to store ReactPy session data. ReactPy requires a multiprocessing-safe and thread-safe database. If configuring `#!python REACTPY_DATABASE`, it is mandatory to enable our database router like such:<br/>`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` |
27-
| `#!python REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 0`, `#!python 60`, `#!python 96000` | Maximum seconds to store ReactPy session data, such as `#!python args` and `#!python kwargs` passed into your component template tag. Use `#!python 0` to not store any session data. |
28-
| `#!python REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix to be used for all ReactPy WebSocket and HTTP URLs. |
29-
| `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python None`, `#!python "example_project.my_query_postprocessor"` | Dotted path to the default `#!python reactpy_django.hooks.use_query` postprocessor function. Postprocessor functions can be async or sync, and the parameters must contain the arg `#!python data`. Set `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to globally disable the default postprocessor. |
27+
| `#!python REACTPY_URL_PREFIX` | `#!python "reactpy/"` | `#!python "rp/"`, `#!python "render/reactpy/"` | The prefix used for all ReactPy WebSocket and HTTP URLs. |
28+
| `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` | `#!python "reactpy_django.utils.django_query_postprocessor"` | `#!python "example_project.postprocessor"`, `#!python None` | Dotted path to the default `#!python reactpy_django.hooks.use_query` postprocessor function. Postprocessor functions can be async or sync, and the function must contain a `#!python data` parameter. Set `#!python REACTPY_DEFAULT_QUERY_POSTPROCESSOR` to `#!python None` to globally disable the default postprocessor. |
3029
| `#!python REACTPY_AUTH_BACKEND` | `#!python "django.contrib.auth.backends.ModelBackend"` | `#!python "example_project.auth.MyModelBackend"` | Dotted path to the Django authentication backend to use for ReactPy components. This is only needed if:<br/> 1. You are using `#!python AuthMiddlewareStack` and...<br/> 2. You are using Django's `#!python AUTHENTICATION_BACKENDS` setting and...<br/> 3. Your Django user model does not define a `#!python backend` attribute. |
31-
| `#!python REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Whether to render ReactPy components in a dedicated thread. This allows the web server to process traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://pgjones.gitlab.io/hypercorn/) and [`uvicorn`](https://www.uvicorn.org/). |
32-
| `#!python REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir" ]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) as a manual override. |
33-
| `#!python REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. This value will gradually increase if `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` is greater than `#!python 1`. |
30+
31+
## Performance Settings
32+
33+
| Setting | Default Value | Example Value(s) | Description |
34+
| --- | --- | --- | --- |
35+
| `#!python REACTPY_DATABASE` | `#!python "default"` | `#!python "my-reactpy-database"` | Multiprocessing-safe database used to store ReactPy session data. If configuring `#!python REACTPY_DATABASE`, it is mandatory to enable our database router like such:<br/>`#!python DATABASE_ROUTERS = ["reactpy_django.database.Router", ...]` |
36+
| `#!python REACTPY_CACHE` | `#!python "default"` | `#!python "my-reactpy-cache"` | Cache used for ReactPy JavaScript modules. We recommend installing [`redis`](https://redis.io/) or [`python-diskcache`](https://grantjenks.com/docs/diskcache/tutorial.html#djangocache). |
37+
| `#!python REACTPY_BACKHAUL_THREAD` | `#!python False` | `#!python True` | Configures whether ReactPy components are rendered in a dedicated thread. This allows the web server to process traffic during ReactPy rendering. Vastly improves throughput with web servers such as [`hypercorn`](https://pgjones.gitlab.io/hypercorn/) and [`uvicorn`](https://www.uvicorn.org/). |
38+
| `#!python REACTPY_DEFAULT_HOSTS` | `#!python None` | `#!python ["localhost:8000", "localhost:8001", "localhost:8002/subdir"]` | The default host(s) that can render your ReactPy components. ReactPy will use these hosts in a round-robin fashion, allowing for easy distributed computing. You can use the `#!python host` argument in your [template tag](../reference/template-tag.md#component) as a manual override. |
39+
| `#!python REACTPY_PRERENDER` | `#!python False` | `#!python True` | Configures whether to pre-render your components, which enables SEO compatibility and increases perceived responsiveness. You can use the `#!python prerender` argument in your [template tag](../reference/template-tag.md#component) as a manual override. During pre-rendering, there are some key differences in behavior:<br/> 1. Only the component's first render is pre-rendered.<br/> 2. All `#!python connection` related hooks use HTTP.<br/> 3. `#!python html.script` is executed during both pre-render and render.<br/> 4. Component is non-interactive until a WebSocket connection is formed. |
40+
41+
## Stability Settings
42+
43+
| Setting | Default Value | Example Value(s) | Description |
44+
| --- | --- | --- | --- |
45+
| `#!python REACTPY_RECONNECT_INTERVAL` | `#!python 750` | `#!python 100`, `#!python 2500`, `#!python 6000` | Milliseconds between client reconnection attempts. |
46+
| `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` | `#!python 1.25` | `#!python 1`, `#!python 1.5`, `#!python 3` | On each reconnection attempt, the `#!python REACTPY_RECONNECT_INTERVAL` will be multiplied by this value to increase the time between attempts. You can keep time between each reconnection the same by setting this to `#!python 1`. |
3447
| `#!python REACTPY_RECONNECT_MAX_INTERVAL` | `#!python 60000` | `#!python 10000`, `#!python 25000`, `#!python 900000` | Maximum milliseconds between client reconnection attempts. This allows setting an upper bound on how high `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` can increase the time between reconnection attempts. |
3548
| `#!python REACTPY_RECONNECT_MAX_RETRIES` | `#!python 150` | `#!python 0`, `#!python 5`, `#!python 300` | Maximum number of reconnection attempts before the client gives up. |
36-
| `#!python REACTPY_RECONNECT_BACKOFF_MULTIPLIER` | `#!python 1.25` | `#!python 1`, `#!python 1.5`, `#!python 3` | Multiplier for the time between client reconnection attempts. On each reconnection attempt, the `#!python REACTPY_RECONNECT_INTERVAL` will be multiplied by this to increase the time between attempts. You can keep time between each reconnection the same by setting this to `#!python 1`. |
49+
| `#!python REACTPY_SESSION_MAX_AGE` | `#!python 259200` | `#!python 0`, `#!python 60`, `#!python 96000` | Maximum seconds to store ReactPy component sessions. This includes data such as `#!python *args` and `#!python **kwargs` passed into your component template tag. Use `#!python 0` to not store any session data. |
3750

3851
<!--config-table-end-->

docs/src/reference/template-tag.md

+8-7
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,9 @@ This template tag can be used to insert any number of ReactPy components onto yo
2525
| `#!python dotted_path` | `#!python str` | The dotted path to the component to render. | N/A |
2626
| `#!python *args` | `#!python Any` | The positional arguments to provide to the component. | N/A |
2727
| `#!python class` | `#!python str | None` | The HTML class to apply to the top-level component div. | `#!python None` |
28-
| `#!python key` | `#!python str | None` | Force the component's root node to use a [specific key value](https://reactpy.dev/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `#!python key` within a template tag is effectively useless. | `#!python None` |
29-
| `#!python host` | `#!python str | None` | The host to use for the ReactPy connections. If set to `#!python None`, the host will be automatically configured.<br/>Example values include: `localhost:8000`, `example.com`, `example.com/subdir` | `#!python None` |
28+
| `#!python key` | `#!python Any` | Force the component's root node to use a [specific key value](https://reactpy.dev/docs/guides/creating-interfaces/rendering-data/index.html#organizing-items-with-keys). Using `#!python key` within a template tag is effectively useless. | `#!python None` |
29+
| `#!python host` | `#!python str | None` | The host to use for the ReactPy connections. If unset, the host will be automatically configured.<br/>Example values include: `localhost:8000`, `example.com`, `example.com/subdir` | `#!python None` |
30+
| `#!python prerender` | `#!python str` | If `#!python "True"`, the component will pre-rendered, which enables SEO compatibility and increases perceived responsiveness. | `#!python "False"` |
3031
| `#!python **kwargs` | `#!python Any` | The keyword arguments to provide to the component. | N/A |
3132

3233
<font size="4">**Returns**</font>
@@ -37,11 +38,11 @@ This template tag can be used to insert any number of ReactPy components onto yo
3738

3839
<!--context-start-->
3940

40-
??? warning "Do not use context variables for the ReactPy component name"
41+
??? warning "Do not use context variables for the component path"
4142

42-
Our preprocessor relies on the template tag containing a string.
43+
The ReactPy component finder (`#!python reactpy_django.utils.RootComponentFinder`) requires that your component path is a string.
4344

44-
**Do not** use Django template/context variables for the component path. Failure to follow this warning can result in unexpected behavior.
45+
**Do not** use Django template/context variables for the component path. Failure to follow this warning can result in unexpected behavior, such as components that will not render.
4546

4647
For example, **do not** do the following:
4748

@@ -52,7 +53,7 @@ This template tag can be used to insert any number of ReactPy components onto yo
5253
{% component "example_project.my_app.components.hello_world" recipient="World" %}
5354

5455
<!-- This is bad -->
55-
{% component dont_do_this recipient="World" %}
56+
{% component my_variable recipient="World" %}
5657
```
5758

5859
=== "views.py"
@@ -81,7 +82,7 @@ This template tag can be used to insert any number of ReactPy components onto yo
8182

8283
1. If your host address are completely separate ( `origin1.com != origin2.com` ) you will need to [configure CORS headers](https://pypi.org/project/django-cors-headers/) on your main application during deployment.
8384
2. You will not need to register ReactPy HTTP or WebSocket paths on any applications that do not perform any component rendering.
84-
3. Your component will only be able to access `#!python *args`/`#!python **kwargs` you provide to the template tag if your applications share a common database.
85+
3. Your component will only be able to access your template tag's `#!python *args`/`#!python **kwargs` if your applications share a common database.
8586

8687
<!--multiple-components-start-->
8788

0 commit comments

Comments
 (0)