Rebuilding Our Signup Flow With Gatsby

Kirby Cool
Thanx
Published in
7 min readAug 1, 2019

--

The problem

Thanx is a customer engagement platform for real-world merchants. It connects users and merchants so users can earn rewards at merchants they love and merchants can gain more insights into who their customers are. This connection happens for the first time on the signup page, so the user’s experience there needs to be seamless and leave a good impression. Our existing signup flow, part of a server rendered rails app, was starting to show its age and didn’t look like the experience we wanted.

With that in mind, we decided to rebuild our signup experience, making sure to focus on:

  • Performance Most users sign up on a phone, so the signup app needs to load quickly and feel snappy on a mobile connection.
  • SEO Having good SEO helps our merchants improve their signup numbers and drive their success on Thanx.

The app

Our app has a main landing page for each merchant on our platform. Each merchant has different programs that a user can sign up with.

A distilled version of the actual app looks like this:

  • /:merchantId/:programId - Landing page for signing up for a merchant and program. It has a field for email and some text about the program
  • /:merchantId/:programId/info - Page where the user enters full name, phone number etc.
  • /:merchantId/:programId/success - Success page after the user signs up

There’s a couple of special cases

  • /:merchantId/join - The main landing page for a merchant. Every merchant has a join program
  • /:merchantId/ - Redirects to /:merchantId/join

One thing to notice is that users will always start on the landing page. This will have implications for how we design our app.

Choosing Gatsby

Before choosing a technology, we evaluated a few common options:

  • Next.js: Next.js is a popular framework for server side rendering React apps. However, the additional complexity of running a node server seemed like overkill for our app, which only has one main landing page that would benefit from server rendering.
  • Ad-hoc prerendering: We put together a prototype script to prerender our application, but we thought this would become cumbersome to maintain as our app grew.

We eventually settled on using a Gatsby.

Gatsby is a static site generator built around React, primarily for building things like blogs or brochure sites. But it can also be use to build apps that prerender some initial pages and then behave like a client react app from there, which Gatsby calls a hybrid app.

This is perfect for our use case! Having a prerendered landing page makes the initial load fast and gives search engine crawlers something to index. Also, Gatsby’s programmatic page creation api reduces the boilerplate required to prerender those landing pages for each merchant in our database. This seemed like a good fit.

Building the app

The client side

We have one Gatsby page component which serves as the main entry point to our app. During Once the app loads, routing responsibility gets handed off to the client side router (we used @reach/router).

The GraphQL query here is a Gatsby page query. Once we hook up our GraphQL api, Gatsby will handle making the query at build time and supplying data to our page component in data.

I’ve intentionally left out the state management part, which could be its own post. We ended up using react context, but there are a variety of tools (Redux, Mobx, Apollo etc.) to choose from.

Prerendering /:merchantId/join

Gatsby has a build step where it takes your app code and generates static html and js files to deploy to a web server. During this step, we use Gatsby’s createPages api to programmatically create our landing pages, which Gatsby then uses to generate html. We start with the /:merchantId/join page for each merchant, since the "join" program is by far the most common signup avenue for users. First, we load the merchant ids from our server and then create a signup page with that merchant handle. It looks something like this:

Including the ensures that all nested routes use the same Gatsby page component. If we didn’t include this, Gatsby would mount a different page component instance for a route like /${merchantId}/join/info. Routes that match the matchPath will use the same merchant and program data as /${merchantId/join}, so we don't have to do any refetching.

Now we can run gatsby build and populate our public/ folder with a bunch of prerendered html pages!

Handling Non Prerendered Routes

Our app works great for the most common case of /${merchantId}/join, but merchants can have any number of additional programs that a user can sign up for. How can we make sure those programs are also supported?

One promising option is to query for every program of every merchant and prerender a landing page for each one. This is a possible path forward but comes with a couple of drawbacks:

  • Our build step would make n merchants * m programs queries, which makes our build step pretty slow. We could do a better job of batching queries to relieve this, but we'll still load a lot of data
  • If a merchant adds a new program, we have to rebuild the app to serve this new program

The last point is a deal breaker. We need to allow users to sign up for all programs at all times, so we need a mechanism to handle program’s that haven’t had pages built yet.

To do this, we need to leverage how Gatsby handles 404s. If the user requests a page that we haven’t prerendered, Gatsby responds with a blank 404 page, loads the js bundle and let’s the client code render an appropriate error page. Instead of immediately rendering an error page, our app can try our client side routes first and load the correct merchant and program.

We configure Gatsby like this:

Using a matchPath of /* tells Gatsby to use our index.js page for all requests that don't match another page (like the /join pages we created earlier). Our client side routing can then take over and render the appropriate components, or a client side 404 page if the route isn't supported in our app.

Improving the experience for other programs

Great! Now our app can handle signing up for any merchant or program. But the experience falls back to a client side rendering for a lot of cases. Especially on slow mobile connections, users stare at a blank screen or a spinner while the react app loads. Since we’ll always have the merchant data, we’d like to show as much of the landing page as possible while the react app and program data load.

We accomplish this by prerendering a version of the landing page for an unloaded program, and then using matchPath to match every /:merchantId/:programId to it. (/join has higher precedence due to the way Gatsby's internal routing works)

Server config

Now we have skeleton pages for unloaded programs, but that’s not enough to get our server to serve them. We need to configure our static web server to redirect all “/:handle/*/” requests to our prerendered html at “/:handle/program/index.html”. This will vary depending on the web server in use, but we generate a bunch of location entries for nginx that look like this:

location ~ ^/a-merchant-id/[^/]*/$ { 
try_files $uri /a-merchant-id/program/index.html @404;
}

Learnings

This is close to how we have our signup flow set up in production and it’s been going well so far, although I’m sure we’ll have to tweak our prerendering in the future. Here’s what we learned.

What went well

This approach allowed us to build a fast and SEO friendly app with minimal boilerplate thanks to Gatsby’s static rendering.

From a development perspective, transitioning from create-react-app was easy once we cleared the initial hurdle of setting up the Gatsby build config. The app is just a normal react app once it loads. Deploying is also easy since we only need a small build step before serving static files.

What was challenging

We did run into some challenges while building this app.

  • Prerendering requires more complex setup: This isn’t unique to our approach, but server rendering an app requires extra setup for a lot of tools used in the app (CSS-in-JS, Apollo etc.). Our inexperience in dealing with this made transitioning to server rendering more challenging. Gatsby’s large plugin ecosystem helped ease our learning curve.
  • Build step is hard to test: Because our build setup is more complicated that a typical Gatsby static site build and involves some tricks in the web server config, it’s difficult to fully test. We’ve done our best to add end to end tests to give us confidence here.

Conclusion

Overall, Gatsby worked well for us for this project. It accomplished our performance and seo goals through it’s static rendering. While using Gatsby for a hybrid app required more time and learning to set up than we had hoped, once we cleared this hurdle, our team was very productive in building the app. We’re satisfied by our choice of technology and will look at using Gatsby for future projects where it makes sense.

Thanx!

Thanks for reading! I hope getting a look at our approach for this kind of app is useful for your projects.

--

--