Skip to content
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

SEO Compatible Rendering #578

Open
Archmonger opened this issue Jan 10, 2022 · 8 comments
Open

SEO Compatible Rendering #578

Archmonger opened this issue Jan 10, 2022 · 8 comments
Labels
priority-3-low May be resolved one any timeline. type-feature About new capabilities

Comments

@Archmonger
Copy link
Contributor

Archmonger commented Jan 10, 2022

Current Situation

Currently, sites built in IDOM are not SEO compatible. This is a fairly common issue with JavaScript frameworks such as ReactJS.

Prior discussion: #486
Prior Issue: reactive-python/reactpy-django#93

Proposed Actions

To resolve this, there needs to be an initial HTTP render, followed by a JavaScript re-render.

This will likely be accomplished through Preact's hydrate API.

The best way of doing this requires some form of persistent storage of all hook states, due to the fact that ASGI websockets are a completely different stack than HTTP rendering. Hook states will need to be stored server side in order to prevent spoofing. We can either use a database or determine if multiprocessing.shared_memory can be used here.

  1. Use the template tag to render the initial component as raw HTML
  2. Serialize all the component's hook values (probably through dill.pickle) and
    • This might need exposing some new hook APIs in core that provides returns hook values for a given component instance.
  3. Store serialized hook values within the database
    • Use the component's UUID as the database ID.
  4. When the JavaScript client requests it, rehydrate the components hook values
  5. Execute the ReactJS rendering
  6. Delete the component hook values from the database, as they are no longer needed.

The database model might look like this:

class IdomHookState(Model):
    uuid = models.UUIDField(
        primary_key=True, default=uuid.uuid4, editable=False, unique=True
    )
    hook_attributes = models.TextField()

This design brings up a challenge of determining when to evict old hook states (for example, if a user did the initial render but for some reason never performed a websocket connection). We don't want to store everything forever, so some configurable age-based eviction strategy might be needed. Expired entries should be culled before each fetch of IdomHookState.hook_attributes. Expiration should be based on last access time, which is fairly simple to do in Django like such: last_accessed = models.DateTimeField(auto_now=True).

@Archmonger Archmonger added the flag-triage Not prioritized. label Jan 10, 2022
@Archmonger
Copy link
Contributor Author

This is an "experimental feature" and belongs in v2 -> v3

@rmorshea rmorshea added type-feature About new capabilities and removed flag-triage Not prioritized. labels Jan 10, 2022
@rmorshea rmorshea changed the title Server side rendering Server Side Rendering Jan 10, 2022
@rmorshea rmorshea added this to the 3.0 milestone Jan 11, 2022
@rmorshea rmorshea added the priority-3-low May be resolved one any timeline. label Jan 13, 2022
@Archmonger
Copy link
Contributor Author

Archmonger commented Mar 9, 2022

In order to take advantage of why users want SSR (namely SEO), IDOM might need to be modified to use HTTP instead of websockets.

@rmorshea
Copy link
Collaborator

So my thinking on this is that only the initial page needs the be rendered in HTML. Subsequent updates could be communicated via websockets. Then again, React has a way to do this, so if we can do server-side rendering with a traditional React solution and communicate via sockets on localhost then that could be an option too. The latter is a bit more complicated with respect to the tooling, but less work with respect to actual implementation, so I'd probably lean towards the latter assuming it's possible.

@Archmonger
Copy link
Contributor Author

Archmonger commented Mar 10, 2022

I did some investigation and here's what I came up with.

The Ugly

  • The currently accepted "best methods" for SSR within React seems to rely frameworks such as NextJS, Razzle, or NodeJS.
    • These frameworks are used to render only the initial page server-side.
    • As you mentioned, that's the preferred behavior.
  • If we want native SSR that relies on existing JS frameworks, we might end up relying on transpiling.
    • I can't think of a good way to propagate renders from IDOM Server Side -> Any JS Framework Server Side (transpiled?) -> React Client Side
    • Unless we can figure out a cross-platform way to make that work, I don't recommend going down this path.

Suggested Approach

  • My suggested solution is to do this within Python.
  • In theory, it's currently possible to immediately develop SSR for Django IDOM due to template tags.
  • There are a few things to consider with this design...
    • To avoid wasteful "double rendering" (SSR render + WS render), we need a way to signal mountViewToElement that the initial render has already been server-side rendered.
      • Likely just a ssr boolean parameter within mountViewToElement.
    • Rehydrating hook values from the SSR render into the WS render will be difficult and come with big limitations/hurdles.
      • We can't really store these values client side due to having to protect against value spoofing.
      • Will likely need to serialize and store the component's hook attributes within a database.
      • This limits SSR capabilities to web frameworks with integrated database capabilities.
      • This limits hooks' initial values to those serializable by dill.pickle.
      • On web frameworks without databases, we can fallback to "double rendering" the component.
      • Alternatively to database storage, we can use redis. But I'm personally not a huge fan of IDOM forcing redis usage.
  • This template tag system should be replicated to all other frameworks we support.

Resources

@Archmonger Archmonger changed the title Server Side Rendering Render initial HTML via HTTP (SSR style) Nov 6, 2022
@Archmonger Archmonger changed the title Render initial HTML via HTTP (SSR style) SEO Compatible Rendering Feb 2, 2023
@Archmonger Archmonger modified the milestones: Speculative, Luxury Feb 2, 2023
@Archmonger
Copy link
Contributor Author

@rmorshea I've discovered react actually has an API for this.

https://beta.reactjs.org/reference/react-dom/client/hydrateRoot

@rmorshea
Copy link
Collaborator

rmorshea commented Feb 10, 2023

We're technically using Preact right now, so we'd probably what we'd want to pay attention to its specific hydration details.

It would be interesting to see if there's anything special about pre-rendering. If not, then we could potentially just use vdom_to_html. If so, we might need a build step where we're require the user to have NodeJS installed so we can use preact build.

@Archmonger
Copy link
Contributor Author

Archmonger commented Feb 10, 2023

I don't think we should use that pre-rendering API.

If we do, then

  1. We are making the assumption that IDOM knows where HTML static pages should be stored.
    • This would mandate users to define a HTML output directory via Options
  2. Have users perform a build step
  3. Would seems like like we'd be encroaching features that our backends do.

We may want to stare at the pre-rendering source and see if anything unique is being done, which we could implement as mutations. I doubt it though, so vdom_to_html seems like the right choice.

@rmorshea rmorshea removed this from the Luxury milestone Feb 21, 2023
@Archmonger
Copy link
Contributor Author

Archmonger commented Sep 14, 2023

I've been doing some thinking on this. I think we might need to take a "zero rehydration" policy and always double render (HTTP followed by WS render).

Since the HTTP stack and websocket stack aren't always on the same process/thread, we can't store any data in-memory. Additionally, not all backends have have a built-in database framework... and we really don't want to build any database features directly into ReactPy.

So, to keep things consistent it's probably best to do the following:

  • Template tag performs the Initial HTTP render using html_to_vdom
    • Exist solely to pre-populate the page.
    • This initial render is completely non-interactive, and the page will remain non-interactive until the re-rendering step below.
  • ASAP re-render using websockets.
    • Developers will need to be mindful of a few details
      • There may be differences between the two renders, since hook values aren't stored + rehydrated.
      • use_scope/use_connection hooks will utilize a HTTP Carrier during this initial render.

If we really want to, we can enable rehydration for batteries-included frameworks such as Django. But in my opinion, having a the functional difference between the rehydration/non-rehydration between frameworks isn't a good idea. Even more so when considering that it would introduce more maintenance burdens.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority-3-low May be resolved one any timeline. type-feature About new capabilities
Projects
None yet
Development

No branches or pull requests

2 participants