How we built a publisher platform with Next.js, GraphQL and Vercel
The backstory
Back in 2020, we got a mission from Alright, one of our key partners: Build an alternative CMS plus an optimized website focused on small and medium publishers.
Alright is an Ad tech company that connects publishers and brands through proprietary technologies.
They have more than 300 publishers all over Brazil, which means the solution should be built with scalability in mind – and not only that – we should have a more generic approach for the technological stack.
Why not WordPress, you might be asking. Well, some publishers are already using WordPress and some bottlenecks could be found when integrating Alright’s technology with these websites, especially because the WP instances are not running on Alright’s infrastructure.
Well, so we did it. Our team suggested using a generic GraphQL API to deal not just with the data from this CMS but with all generic data Alright has in other applications and that would eventually be needed in other apps.
We love PostgreSQL, so here the decision was easy. If we need a relational database with sane structured data, why not going with the “The World’s Most Advanced Open Source Relational Database”? In the beginning, we wanted a quick solution for GraphQL, so instead of building all schemes, resolvers, mutations, etc, by hand, we choose PostGraphile.
There are a lot of benefits with a solution that takes a database and generates an API, but as you can imagine, there are some issues as well (we’ll cover that in a future post).
Was that enough? Nope. What about the consuming interface? Enters Next.js into the picture.
The CMS itself was not built with Next.js – considering it is an application that doesn’t need performance, SEO, or a strategy for dealing with multiple users; therefore, we built a plain React SPA.
Since our main goal was to build a performance-first website for publishers, we bet on Next.js and its promises. Back then, Next.js hadn’t yet achieved what they have today, which is quite impressive. We adopted it for the project we called Alright Publisher Viewer, which was the “view” part of a growing ecosystem of big and small pieces of software.
After the deploy
Technically speaking and from the developer point of view, Next.js is great, but we had some difficulties integrating all the stuff previously built.
Besides the Alright Publisher Viewer application, we now had Alright Publisher Editor (the React SPA CMS), Alright GraphQL API, Alright Sqitch (PostgreSQL related), and some other helper applications.
The technology behind the product wasn’t trivial, but in the end, we made it work pretty well.
There was an additional and complex step here in this phase: We were creating and managing all the necessary infrastructure to support the apps. Which means our team was dealing with Amazon daily. EC2, Code Deploy, S3, Cloud Front, RDS and so on.
It worked well, but it could have been so much better, especially when we look at the developer experience, at the processes of deploying to different environments (staging, production, etc).
Right after our first release we noticed something odd. The Alright Publisher Viewer, the app we built with Next.js, wasn’t even close to the result we expected.
After all the struggle adapting the Next.js application to run well on Amazon, all days spent on the architecture and, when everything was well limed, our production application was just… OK.
There was certainly something wrong here, our team thought. And indeed there was.
A shift in our strategy
The main issue was related to how Next.js works and how we implemented the logic in the view.
If you have an application that needs to fetch data, what do you do?
That was easy. As we need performance and SEO, Next.js’ approach of server-side rendering seemed to be the solution.
We looked at the documentation and… boom! Server-side rendering (SSR) was working. Our view layer was communicating just fine with the GraphQL API.
export async function getServerSideProps(context) {
return {
props: {}, // will be passed to the page component as props
}
}
So what could be the issue? The problem is that this is not as fast, as performant, as we needed. We implemented the application in a way that, every time someone asks for a page, there would be a request for the API and the page would be rendered on the server.
Sure, a cache layer is missing, you’re probably thinking.
We knew that, but we also knew there was something even better: the Next.js’s approach of static pages.
export async function getStaticProps(context) {
return {
props: {}, // will be passed to the page component as props
}
}
When you use getStaticProps
, Next.js will pre-render the page at build time using the props returned by getStaticProps
.
That’s awesome! We’re almost there.
To get all things working we have to combine getStaticProps
and getStaticPaths.
export async function getStaticPaths() {
return {
paths: [
{ params: { ... } } // See the "paths" section below
],
fallback: true or false // See the "fallback" section below
};
}
The application has dynamic routes. Remember, Alright is delivering a tool for publishers, so it’s basically a news website with a lot of pages and articles. In Next.js we need to define a list of paths that have to be rendered as HTML at build time.
If you export an async function called getStaticPaths
from a page which uses dynamic routes, Next.js will statically pre-render all the paths specified by getStaticPaths
.
There is a detail that must be taken into consideration: publishers may have thousands of articles (pages), which was the case for our first publisher using the platform.
We can’t generate all pages at build time. Imagine millions of pages being generated for each build, that’s not feasible.
Next.js has this covered. With getStaticPaths
and getStaticProps
we can limit how many pages we want to generate at build time.
But there’s more! What happens with pages that haven’t been generated yet when someone tries to access it? If fallback is true, then the behavior of getStaticProps
will change. The paths that have not been generated at build time will not result in a 404 page. Instead, Next.js will serve a “fallback” version of the page on the first request to such a path.
In the background, Next.js will statically generate the requested path HTML and JSON. This includes running getStaticProps
.
In short, with this approach. we can have as many pages as we want pre-generated and in cache and when a new page is requested, Next.js will load all data necessary and cache it.
Moving to Vercel
Now that we have the perfect combination of features on the Next.js side there is just one piece missing: Vercel.
Vercel combines the best developer experience with an obsessive focus on end-user performance.
Actually, we already had the old application (before the static refactoring) running on Vercel.
We started with small steps and one thing we wanted was to be able to use all the developer experience deploying and building applications Vercel offers. We love the automated deploys via Git (including different branches) and the powerful interface and CLI.
As we had already had the new application running on Vercel with multiple branches, moving everything to production wasn’t that difficult. With a few changes on the DNS, some environment variables and some small optimization on the application, we achieved our goal.
Another killer feature from Vercel is its Edge Network.
The Vercel Edge Network sits in-between the internet and our application’s deployments. It has become our de-facto CDN.
This layer is responsible for routing requests to the correct Serverless Function or Static File output of our builds. Vercel caches our content at the edge in order to serve data as fast as possible.
Conclusion and next steps
Of course, there were some extra steps and a lot of research, but in the end, we got a nice and complex application up and running. And, more important, running in a scalable way.
All infrastructure is automated, the developer experience is great and new features can be developed, merged and deployed smoothly.
Vercel and Next.js are great for this kind of application, but first, you need to know what do you want and what are the possible approaches to achieve that. Then, you can move on to the advantages of combining the platform and the framework.
We still need to migrate some sub-projects, but the bottleneck has been successfully removed.
Our plan is to have all applications running on Vercel and let just the database on Amazon RDS (for now).
This work has already been done and is currently working on staging environment.