I've been wanting to write this code for a while. As you may know, I love static sites and this blog is powered by ghost. I've also been part of a presentation about using GatsbyJS with Sitecore JSS. Something kind of hit me while I was working on the Svelte integration for Sitecore JSS though. Sitecore JSS has the functionality already to render the entire page so that page can be editable in Experience Editor. When you load a JSS page in Experience Editor it will server side render the page using the renderView function defined in your application. It's just a Javascript function though and we have total control over it and we can call it just like any other function.

So the basic idea here is that can we just crawl all our app routes and call renderView for each one of them and write the output to disk. I thought I might be missing something so I asked Kam about this approach and he pointed me towards this proof of concept example from the JSS GitHub repo that would allow you to prerender routes. I learned a lot from just reading this file and I need to further explore createDisconnectedLayoutService to see if I can replace my layout service fetch request approach with it.

I had to prove out this idea, so I just started hacking it together. I just hardcoded a few routes in a script and tried to get it working... and it did. My ultimate goal was to just create a little script that could be placed in the scripts directory of any Sitecore JSS application, so there are no external dependencies besides the ones the sample apps already ship with.

One advantage this method has over doing a GatsbyJS integration with Sitecore JSS is that you can retain Experience Editor functionality and build out your pages just like you always have.

So, how does it work?

First, the script gathers your routes. It supports both disconnected and connected modes. In disconnected mode, it will just read your data\routes directory and discover any files with route definitions in them. In connected mode, it'll crawl the Sitecore tree for the JSS app with GraphQL queries and look for any App Route template (you can configure which it considers to be a route.)

After that, it will iterate each route it discovers and call the layout service with the route path and use the layout service JSON to call renderView for this route. A view bag is also built containing the dictionary for the item language to avoid a web request during rendering.

Once renderView returns the server side rendered HTML and that data is written to disk at the path where it would appear in the Sitecore tree. I chose to just use index.html files inside folders to keep things consistent and not have to worry about handling the HTML extensions for when these files are hosted.

So without further ado, here's the sample React app hosted in an S3 bucket.

JSS sample app with a doge twist

Getting Started

Interested in using this? Awesome! Here are the steps for getting started with this project:

  • Clone or download a ZIP of this repository.
  • Copy bootstrap-static.js and build-static.js to your scripts directory of your JSS app.
  • Add the following commands to the scripts section of your package.json
    "bootstrap:static:disconnected": "node scripts/bootstrap-static.js --disconnected",
    "bootstrap:static:connected": "node scripts/bootstrap-static.js",
    "build:static": "npm-run-all --serial bootstrap:static:disconnected build:client build:server --parallel generate:static",
    "build:static:connected": "npm-run-all --serial bootstrap:static:connected build:client build:server --parallel generate:static",
    "run:static": "npm-run-all --parallel generate:static",
    "generate:static": "node ./scripts/build-static.js"
  • After that you can run jss build:static or jss build:static:connected. These commands will build the server and client and execute generate:static. run:static can be useful if you're changing the actual code of the generator and know you've already built disconnected or connected last, which will save time when running test builds.

Future Improvements

There's certainly room for improvement on this script. Here are some things that I'd like to address in the future:

  • I'd love to consider using the Content Search API on the GraphQL endpoints if it's an option for discovering routes in connected mode. If you had a lot of content items or a lot of levels, this script could result in a decent amount of queries. I'd just make this an option that you can take advantage of if you wanted to since results may vary, depending on how fresh your index is. The search field also isn't enabled on the default Sitecore JSS GraphQL endpoint either, which would have broken my unofficial rule of not requiring changes to be made to use this.
  • General code cleanup. There are some areas where I commented where I'm just not happy with how it came out. It got a little confusing dealing with recursive promises and waiting for them all to finish so I could flatten all the results and return them all at once. Hacked around this some by passing a single array that I would push matches in but it's not what I wanted. PR's welcome. ;)
  • Currently copying media just happens by default, this could be a lengthy procedure if you have a lot of content. I'd definitely consider rewriting images in the generated HTML to point to the Sitecore server to serve images instead of copying them over. Also would love to check if a copied file has changed if it already exists and avoid copying it over if it's unnecessary.
  • I had to duplicate the entire generate-config.js file to override the GraphQL endpoint address. I'd love it if Sitecore JSS allowed this to be passed in the same way we can already pass in overrides for other properties, it would eliminate most of what is in this file.
  • You'll notice some dictionary request errors client side. I was going to write the dictionary to disk and will in the future but I need to give it a file extension so the mime-type is correct when it's served, which will require people to change their code to account for this, which I was hoping to avoid. I'll add this in the near future though.
  • Convert the code to Typescript.
  • Only one language for now, currently set to English but you can just change the variable to point at the language you are targeting.
  • Resized images – might be some issues here.

What about personalization?

Not supported but I'm open to ideas on how to make this possible. I assume it would require some call backs to the Sitecore servers to be able to do this, which was out of scope for this proof of concept.

Conclusion

This code is really a starting place and may not even work for your needs. It should definitely be considered proof of concept. I've only tested this with React and Sitecore JSS 11 and 12, but will test it with Vue and will make any adjustments needed to allow it to work there as well.