Backend: Achieving SEO, Accessibility and separate Frontend deployment - Into Foundation Spa React series, Part 3
In the third installment of the "Into Foundation Spa React Series", I'm adding SEO, Accessibility, and a Separated Frontend Workflow. The code for this can be found in the Foundation.SpaViewEngine project, which is part of the Foundation Spa React project on GitHub. This blog post aims to give you a good starting point when investigating that code.
Full disclosure: I'm aware that there're controversial decisions in this section and, yes, feel free to suggest improvements. However, the ability to deploy the SPA without any additional services beyond the Optimizely Digital Experience Platform should not be affected.
Solution Outline
Assuming that the frontend will be delivered as a set of JavaScript, CSS, and HTML files, we need to achieve two goals:
- Static asset storage: Allow the storage of these assets within the .Net project, in a way that does not require a .Net deployment to update the assets and serve them to the public when requested.
- Server-side template execution: Allow server-side (pre-)rendering of static pages that can be hydrated on the client.
These are then also the two parts that create are part of the SpaViewEngine Project, though relatively separated within the project.
Static Asset Storage
The first challenge to overcome is the storage location, where the decision needed to be made at which level to interact with Content Cloud. Two criteria's needed to be satisfied by the solution:
- No duplication of logic already in Content Cloud, but reuse existing logic
- Works both on DXP as well as in self-hosted scenario's
- Simple and straight-forward solution
The sole solution that was identified to meet all these criteria was to use a Media IContent type, so version control, access rights, and storage of the binary data were already covered. With this solution, the risk is introduced to use a lot of storage space (each version) as well as generating potentially hundreds of IContent items to just store the assets. The storage space can not be overcome but can be mitigated by means of file compression and the number of IContent items risk is mitigated by archiving the assets into one archive file prior to uploading into Content Cloud. With support needed for both Windows and non-Windows development environments, the Zip compression & archiving format was selected. Last, but not least, in order to prevent a collision on the file extension with existing Media types, the file extension has been defined to be "spa".
So, let us dive into the actual implementation.
The frontend IContent items are kept out-of-view of most editors by generating a single IContent entry of type SpaFolder under the root, outside of the websites. This IContent entry exists to provide the tree structure that will hold the static assets and can be used to manage access rights.
The creation and update of SpaMedia items are done by the SpaMediaDeploymentApiController
, which is a standard .Net API Controller, which takes an uploaded file and stores as binary data with an IContent item. To ensure that this is only accessible by authorized frontend developers, there are two access rights that must be satisfied:
- The current user must have the function permission: "DeploySpa", for the service to start processing the request.
- The current user must have the right to Edit & Publish the new SpaMedia item/version under the SpaFolder node.
This allows for a setup where a functional/technical administrative user might be allowed to roll back a newly deployed version, without allowing that user to deploy new versions using the API.
With the assets being able to be deployed into Content Cloud using this setup, the last piece of the puzzle is to let these assets being requested by the browser. This is relatively easily achieved by leveraging the partial routing support within .Net Framework, all routes matching spaview/{container}/{*path}
will be given to our SpaMediaAssetController
. The job of this controller is pretty straightforward, it will try loading the container (SpaMedia, by its name) and asset, based on the path inside the container. The controller itself modifies the Response so that it will add the appropriate headers to enable caching, and removes the cookies from the Response so that a CDN will recognize the assets as being static.
Server-Side Template Execution
Having static assets is great, however, for server-side rendering, we need to make sure that all content entered and (re-)organized by editors is represented correctly when the site is accessed by a browser that does not execute JavaScript. Typical use-cases are search engines (SEO) and assistive technologies (WCAG). With the front end delivered as a JavaScript application, this JavaScript needs to be executed to generate the HTML.
To support this within the current state of the DXP, the SpaViewEngine adds a new ViewEngine to .Net, which takes priority over Razor and leverages the JavaScriptEngineSwitcher project to offer Server-Side JavaScript execution. The current implementation defaults to (and has only be tested with) V8 as an engine, but has been designed to have the engine configured through the standard dependency injection of Content Cloud.
To bridge the gap between the .Net and JavaScript, it adds a number of poly-fills (core-js) and the epi
global variable. Furthermore, it exposes the current content through __INITIAL__DATA__
and some advanced features through __EpiserverAPI__
and offers wrappers that make it easier to work with the .Net objects from JavaScript.
The most important part here is that it takes a specific server-side rendering built of the application, allowing to use of different bundling strategies between the browser and server-side rendering capabilities. The server-side rendering is not bound to react, but its signature is highly designed after the capabilities of react-helmet. The ViewEngine assumes that the server-side bundle implements the global, parameter-less function render(), with a return type that can be understood to be of type SSRResponse. Within the render() method, the server-side logic can use the data and APIs in __INITIAL__DATA__
and __EpiserverAPI__
to render the current content.
Comments