Single page apps have a tremendous amount of benefits, primarily surrounding performance, but also bring about some deficiencies that never really existed with multi page sites. Ndevr recently built a single page app called Pick Your Battles! to test out some new technology on our own time. The app could have been very easily been built with static generated pages on top of a typical PHP/MySQL backend, but we went with React/Node.js/Mongo with Nginx as a front-end to proxy requests between our static files and Node.js API. We identified early in our development cycle the poor experience when sharing what we call a Battle Results page, as the sharing crawlers aren’t quite “smart” enough to render the page and even if they were we don’t have any catchy photos on our results pages.
Shareable Images
Since we don’t have any photos on our Battle Results pages, and sharing the logo over and over isn’t very appealing we decided to find a solution for sharing a screenshot of the results page. We decided generating a screen shot of the results would be a perfect image to share. There are a few open source solutions available, we decided to go with a Node.js package called node-webshot on top of PhantomJS. A great feature of node-webshot is the ability to pass in separate markup to be rendered, allowing us to customize the screen shot or even generate different ones for new social networks.
Updating Meta Tags
We have the shareable images generating the way we want. Next we needed to determine how to get the meta tags to update properly. The Google pointed us to some guidance saying this was impossible with JavaScript and others saying “it should work.” The end result we found was that updating these tags simply with JavaScript wasn’t going to cut it, since as we mentioned earlier the two services we cared about Twitter and Facebook only use the static HTML response of the url and don’t actually render the page.
React Rendered Tags
<meta name="description" content="The app that helps you decide whether or not to argue your point!"/> <link rel="canonical" href="https://pickyourbattles.io" /> <meta property="og:locale" content="en_US"/> <meta property="og:type" content="website"/> <meta property="og:title" content="Pick Your Battles!"/> <meta property="og:description" content="The app that helps you decide whether or not to argue your point!"/> <meta property="og:url" content="https://pickyourbattles.io/"/> <meta property="og:site_name" content="Pick Your Battles"/> <meta property="og:image" content="https://pickyourbattles.io/assets/pickyourbattles-share-example.png"/> <meta name="twitter:card" content="summary"/> <meta name="twitter:description" content="The app that helps you decide whether or not to argue your point!"/> <meta name="twitter:title" content="Pick Your Battles!"/> <meta name="twitter:site" content="@ndevrinc"/> <meta name="twitter:domain" content="Ndevr"/> <meta name="twitter:image:src" content="https://pickyourbattles.io/assets/logo.png"/> <meta name="twitter:creator" content="@ndevrinc"/>
JavaScript Rendered Tags
<meta name="description" content="The app that helps you decide whether or not to argue your point!"> <link rel="canonical" href="https://pickyourbattles.io/#result/567054cfce13b307074be961#result/567054cfce13b307074be961"> <meta property="og:locale" content="en_US"> <meta property="og:type" content="website"> <meta property="og:title" content="Pick Your Battles!"> <meta property="og:description" content="The app that helps you decide whether or not to argue your point!"> <meta property="og:url" content="https://pickyourbattles.io/result/567054cfce13b307074be961#result/567054cfce13b307074be961"> <meta property="og:site_name" content="Pick Your Battles"> <meta property="og:image" content="https://pickyourbattles.io/share/567054cfce13b307074be961.png"> <meta name="twitter:card" content="summary"> <meta name="twitter:description" content="The app that helps you decide whether or not to argue your point!"> <meta name="twitter:title" content="Pick Your Battles!"> <meta name="twitter:site" content="@ndevrinc"> <meta name="twitter:domain" content="Ndevr"> <meta name="twitter:image:src" content="https://pickyourbattles.io/share/567054cfce13b307074be961.png"> <meta name="twitter:creator" content="@ndevrinc">
Generating Static HTML
Wait, I thought the point of a single page app was to no longer work with such an “antiquated” technology as HTML. Well, guess what the web still comes down to the basics and we needed some plain old dotHTML files to send to those crawlers. Finding this post about AngularJS SEO was a pretty good find, scroll down to the Taking Snapshots section to see how this can be done with AngularJS. To render and save static HTML files to our server we used another Node.js package called Zombie, then put a quick routing rule in our Nginx config and we have both a single page React app along with static generated HTML files all running happily together.
location / { index index.html; try_files $uri $uri/ /index.html; } location /result { index index.html; try_files $uri /snapshots/$uri.html 404.html; }
Putting it All Together
Once you have all the pieces mapped out putting them together is easy. Simply have your visitors share the urls of the static markup from your basic .html files you generated with Zombie which reference the screenshot images you took with node-webshot. We’re going to continue to evaluate how this is performing and iterate, when we have time, as we know it’s not perfect at the moment. Until then add some comments if you have questions or other tips you’ve found.