Skip to content

Build A Static Site With Dynamic Flair

Building a new marketing site, a totally new greenfield project within an existing organization, is fun and exciting, but offers a unique set of challenges. Let's outline some of the most common scenarios and see how to tackle them head on.

The idea: we have a new site that has static content and maybe some forms. Some of the requirements from our collective bosses: everyone needs to be able contribute, but only the director or those bestowed with the power can publish content. The other big need is to be able to see changes, updates, new material on the site, pass around an internal url to gather feedback, before it ever goes live. There are a few more catches that will come up as we scaffold out this exciting new project.

Like all good sized projects, we've made some choices up front and want to make sure they will meet our needs before writing up the work and getting started. Based on a few of the requirements outlined earlier, we'll be using Sanity content platform. Everything will hopefully go there from importing existing content, rich media, and copy for the landing page. For our development choice and deployment, we've chosen Next.js paired with Vercel for deploy. The idea is to make the developer experience vastly superior to our current system: to make deployment and preview of the project as seamless and transparent as possible. Let's dive in.

Sanity Content Platform

Headless CMS offers several advantages over the traditional method of content. For a platform like Wordpress, the user-generated content, structure, persistence, layout, and style are all combined into a single application. This has benefits such as ease of use, a rich ecosystem of plugins and tools, and many more. It also tightly couples our process and curation of content with the site itself. What Sanity offers is a rich content creation experience, called Studio, separate from our end result, meaning we can distribute it quickly and easily across a global CDN network. That's where Vercel comes in, but we'll get to that.

With the Studio comes dedicated SDKs and a generated GraphQL endpoint to get that structured content in just the right way, at just the right time. While GROQ is similar to GraphQL queries, it offers a bit more specificity when required. We can give specific access to everyone in the company using roles that allow them to create, edit, or update curtain content. We'll reserve a role 'owner' for those with final sign off to hit publish and send changes out into the world. The real win with Sanity and other headless CMS's is we can have the development team build out a structured scheme for blog posts, marketing updates, product releases, landing pages, or anything else using Sanity's document based objects and then (and here's the key) hand it over the the rest of the team to build it using their expertise. They can then add titles, descriptions, the right copy, images with specific crops, highlights and metadata for a new project rollout. All that stuff developers are not involved in, the rest of the team can take the scheme and the studio and build just what they need, on their own terms.

Next.js

Now, to get all that hard work from the content backend and out in to world, we're goning to use a React framework, Next.js. The biggest selling point for this project is two forms of pre-rendering: Static Generation and Server-side Rendering that Next.js offers. This gives a couple features right away. We can statically generate all that content on build time once, send it out to the edges and users will have a super fast, high quality experience. Once that snazzy page has loaded, we can selectively hydrate any dynamic content we might want to keep users engages, maybe load inventory of our latest project from Shopify. That's the other big win with Next.js: it's data hungry. We can load, fetch, and hydrate data from any source in a multitude of ways.

First and foremost, we can get content from Sanity on build, and then add sources as the project grows: maybe it's Shopify, or some legacy inventory service over a REST endpoint. The powerful idea that makes Next.js a good fit for this project is that it has opinions on when to fetch data, but the how and where are completely open to mix and match. As a bonus, we also get Server-side Rendering for a great preview experience that we'll look into shortly.

How to Query

We've chosen our respective platform, deployment method, framework, and content management system. As we begin to wire up the data on the backend to the client, there's a decision to make. Sanity offers a unique query language called groq and also a generated GraphQL endpoint based on the scheme that we create in our studio. So, which do we use?

Here's the great part: Next.js is unopinionated about the way we fetch data, only really where it's fetched, be that server side, at build, or client side. GROQ isn't totally foreign as a query language:

*[_type == 'movie' && releaseYear >= 1979] | order(releaseYear) { _id, title, releaseYear }

Using the provided Sanity SDK to build and fetch queries or mutations is similar enough to GraphQL, so it won't be a huge lift in learning. That being said, the team is already comfortable with GraphQL, so that makes for a logically choice to start with. However, if or when the need arises for multiple queries or some gnarly nested query, GROQ is in our tool belt and can be additive to the project without backtracking on the work already accomplished. That being said, we also have the flexibility to add additional datasources: maybe there's a DAM in the company that years of assets we need to use or an aggregate sales service that the backend team built for us with using SalesForce. We can selectively fetch this data when the user expects it.

By just relying on the components of Next.js, we even get prefetching on <Link> pages without having to make any configurations. These are a couple of the small wins we get when choosing a framework with some opinions baked in for us.

Datasets, Preview, and Production

Things are going great. We've got the studio up with structured schemes for the other teams in the company to add content. An early version of the site deployed on Vercel and tied to the main branch in our git repository. Teams are moving quickly and the site is automatically built and deployed on PR merges. So how do teams starting sharing content, work, ideas, before it goes out for the whole world to see? Preview Mode.

When we enable preview mode on our Next.js site, it's just a boolean flag in the cookie, but what it does when a request comes in with preview=true is rather than serve static data or props that we've fetched on build, we can have it fetch draft or unpublished data from our studio.

export async function getStaticProps(context) {
  // If context.preview is true, append "draft" to the API query
  // to request draft data instead of published data from Sanity
  const res = await fetch(`https://.../${context.preview ? 'preview' : ''}`)
  // ...
}

In practice, anyone with access to the studio can go in, update the layout and copy of a landing page, maybe give it a bit more flair and click the 'Live Preview' tab that we put right there in the studio! It's going to make an api request to our site at /api/preview passing the draft id and the super secret token that we generate and keep in our ENV list on Vercel.

Our endpoint will verify the token, maybe check that the draft id exists to be safe, sets that preview cookie to true, and finally redirects to the site page where getServerSideProps picks up on preview mode. Now we're looking at that super rad landing page rendered in the site with all the CSS styles, headers, footers, side nav, exactly what it will look like when it's published.

//api/preivew 
...
// Enable Preview Mode by setting the cookies
  res.setPreviewData({})

  // Redirect to the path from the fetched post
  // We don't redirect to req.query.slug as that might lead to open redirect vulnerabilities
  res.redirect(post.slug)

Wrap up

"Service, [and] amplify, and give new skills to non technical people and users."
Guillermo Rauch

"The power, that in the past, only developers had." ~Kapehe

When it comes down to it, what we want in a successful project is to empower users and creators to make without limits. What we've gone over together and outlined above is to that service. As developers, we want to enable those around us, in our companies and projects, to bring their skillset and expertise to the table and contribute in a meaningful way. Do that with as little friction as possible, and you're off to a great start.

I want to thank my guests on Build IT Better, Kapehe, Devrel with Sanity CMS, and Guillermo Rauch, CEO of Vercel, for a fantastic conversation and sharing their knowledge with me. This article would not be possible without their time and insight. Thank you. đź‘‹

This Dot Labs is a development consultancy that is trusted by top industry companies, including Stripe, Xero, Wikimedia, Docusign, and Twilio. This Dot takes a hands-on approach by providing tailored development strategies to help you approach your most pressing challenges with clarity and confidence. Whether it's bridging the gap between business and technology or modernizing legacy systems, you’ll find a breadth of experience and knowledge you need. Check out how This Dot Labs can empower your tech journey.

You might also like

Next.js 13 Server Actions cover image

Next.js 13 Server Actions

Introduction May 2023, Vercel announced that the App Router is recommended for production in the new version of Next.js v13.4, and it came with a lot of good features. In this article, we will focus on one of the best new features in the App Router: the Server Actions. Server Actions vs Server Components At first, you may think both Server Components and Server Actions are the same, but they are not. Server Components are featured in React 18 to render some components on the server side. On the other hand, Server Actions is a Next.js 13 feature that allows you to execute functions on the backend from the frontend. Back to Web 1.0 Before Single Page Applications frameworks, we were using HTML forms for most things. All you had to do was add a directory to their action attribute, and it would push the data to the server with each input name. No state was required, no async and await, etc. When Remix became open source last year, it introduced this idea again to the market. Its form actions would work even without JavaScript enabled in the browsers. It was mind-blowing. Next.js Server Actions are similar, with a wider range of use cases. They added a lot to it so you don’t only use them on HTML forms, but also on buttons and client components. How useful are Server Actions As I said in the section above, the Server Actions are like Form Actions in Remix and also they work on both Server and Client in a way similar to tRPC or Telefunc. Think about it. Instead of creating an API endpoint and making a fetch request to it from the client side, you execute a function that’s already on the server from the client like any other JavaScript function. Server Actions on the Server Side To make this post more effective, I’ll build a simple counter component with Server Actions. It runs even if JavaScript is turned off. First, create a new Next.js 13 App router project: ` npx create-next-app@latest ` Then, in the app/page.tsx, add a variable outside of the page component. `js let count = 0; export default function Home() { // ... } ` Then, add the Server Action, which is an async function with the “use server”; tag. `js let count = 0; export default function Home() { async function increment() { "use server"; count++; } return ( Count {count} Increment ) } ` After implementing the above, when you click on the increment button, you shouldn’t see the change until you refresh the page. You need to use revalidatePath to get a reactive state. `js import { revalidatePath } from "next/cache"; let count = 0; export default function Home() { async function increment() { "use server"; count++; revalidatePath("/"); } return ( Count {count} Increment ) } ` Now you have a Counter that can run with zero JavaScript. Server Actions on the Client Side You can also use the Server Action on the client component (it will be more like tRPC and Telefunc) to trigger a function on the server from the client and get data. But it will not work in the exact same way because in Next.js, each file has to be run on the server or the client. You can’t run the same file on both. So we need to move the function to its own new file, and add the ”use server”; tag on the top of the file. `js "use server"; export async function increment() { "use server"; count++; revalidatePath("/"); } ` We can import it from the client component, and use this server action there! Conclusion Server Actions are a great addition to Next.js, and it will make it easier to build full-stack applications that require a lot of work to be done in traditional frontend/backend ways like creating the API route, and then fetching it. I hope you enjoyed this article, and if you have any questions or feedback, feel free to reach out to us....

Utilizing API Environment Variables on Next.js Apps Deployed to AWS Amplify cover image

Utilizing API Environment Variables on Next.js Apps Deployed to AWS Amplify

Although Next.js is a Vercel product, you may choose not to deploy to Vercel due to their pricing model or concerns with vendor lock-in. Fortunately, several other platforms fully support deployment of Next.js including AWS Amplify. Whether you’re using the Next.js app directory or not, you still have API routes that get deployed as serverless functions to whatever cloud provider you choose. This is no different on AWS Amplify. However, Amplify may require an extra step for the serverless functions if you’re using environment variables. Let’s explore how AWS Amplify is deploying your API routes, and how you can properly utilize environment variables in this context. How AWS Amplify manages Next.js API Routes When you deploy Next.js apps via Amplify, it takes the standard build outputs, stores them in S3, and serves them from behind a Cloudfront distribution. However, when you start introducing server side rendering, Amplify utilizes Lambda Edge functions. These edge functions execute the functionality required to properly render the server rendered page. This same flow works for API routes in a Next.js app. They’re deployed to individual lambdas. In Next.js apps, you have two (2) types of environment variables. There are the variables prefixed with NEXTPUBLIC_ that indicate to Next.js that the variable is available on the frontend of your application and can be exposed to the general public. At build time, Amplify injects these variables, and values that are stored in the Amplify Console UI, into your frontend application. You also have other environment variables that represent secrets that should not be exposed to users. These will not be included in your build. However, neither set of these variables will be injected into your API routes. If you need any environment variable in your API routes, you will need to explicitly inject these values into your application at build time so they can be referenced by the Next.js systems, and stored alongside your lambdas. Injecting Environment Variables into the Amplify Build By default, Amplify generates the following amplify.yml file that controls your application’s continuous delivery (CD). The following is that default file for Next.js applications: ` version: 1 frontend: phases: preBuild: commands: - npm ci build: commands: - npm run build artifacts: baseDirectory: .next files: - '/*' cache: paths: - nodemodules/**/* - .next/cache//* ` To inject variables into our build, we need to write them to a .env.production file before the application build runs in the build phase. We can do that using the following bash command: ` env | grep -e >> .env.production ` env pulls all environment variables accessible. We use the pipe operator (|) to pass the result of that command to the grep -e which searches the output for the matching pattern. In this case, that’s our environment variable which will output the line that it is on. We then use the >> operator to append to the .env.production file, or create it if it does not exist. Be careful not to use a single > operator as that will overwrite your file’s full content. Our amplify.yml should now look like this: ` version: 1 frontend: phases: preBuild: commands: - npm ci build: commands: - env | grep -e >> .env.production - npm run build artifacts: baseDirectory: .next files: - '/*' cache: paths: - nodemodules/**/* - .next/cache//* ` It is important to note that you have to do this for all environment variables you wish to use in an API route whether they have the NEXTPUBLIC_ prefix or not. Now, you can use process.env.VARIABLE NAME] in your API routes to access your functions without any problems. If you want to learn more about environment variables in Next.js, [check out their docs. Conclusion In short, AWS Amplify deploys your Next.js API routes as Lambda Edge functions that can’t access your console set environment variables by default. As a result, you’ll need to use the method described above to get environment variables in your function as needed. If you want to get started with Next.js on Amplify today, check out our starter.dev kit to get started, and deploy it to your AWS Amplify account. It’ll auto-connect to your git repository and auto-deploy on push, and collaborating with others won’t cost you extra per seat....

Build It Better Headless CMS With Prismic & Storyblok cover image

Build It Better Headless CMS With Prismic & Storyblok

The Headless CMS architecture is red hot in the JamStack community right now. I feel like new products and services are popping up daily. I had the pleasure of sitting down with Lucie Haberer, DevRel at Prismic, and Samuel Snopko, DevRel at Storyblok to get the details on this emerging product category, its origin story, features, tradeoffs, user experience, futures, and more. Below is a summary of the information that I learned from our conversation. The Pain of Getting Data out of the CMS The growing use and improvement of frontend tools and frameworks puts increased pressure on existing monolithic content management systems to make the data more accessible outside of itself. Getting content in a monolith, like WordPress, was relatively quick and easy. Getting that data from there to anywhere else that wasn't a hosted webpage, within that single-engine, was far more difficult. Coupled with the skyrocketing use of mobile devices, apps, and increased web access around the world, the monolith became a point of frustration for developers, and a bottleneck for businesses needing to update and adapt quickly. Who's Leading Who? Is the JAMStack empowering Headless CMS, or is it the other way around? From static site generators like Jekyll, to full-blown web applications backed with content available via APIs from Headless CMS, this relationship isn't one-sided, but rather reciprocal. As UIs needed more options and flexibility over the content, the APIs that Headless companies like Prismic and Storyblok offer were built-out to meet these needs. As the ecosystem of content APIs expanded, so did the features offered by visual studios, as well as their use in web apps, mobile, video, and smart devices. It is this mutual benefit that has led to explosive growth in both Headless CMS and JAMStack use across the industry. Not just creating content, but changing content shape I think the real superpower of Headless CMS lies in its ability to let anyone not just create content, but modify the shape of the content itself. Imagine the scenario where a company wants a quick landing page for an upcoming event. Now, most of us would roll our eyes at the use of "quick', but bear with me. It's a "simple" page with a hero image, some event details, an image, and a video. It's a specific use case, and they don't want to add it as a blog post along with the other content. Maybe they want it at '/coming-soon' on the domain. Well, that URL route doesn't even exist currently. Could this be done without ever contacting the development team? Since the content is decoupled, and just pulled via APIs when the site is built, surely you'd need some tweaking in the code for the HTTP fetch calls? With Headless CMS, nobody from the development, ops, or admin teams needs to be involved. The setup would be something like this: There is a defined set of routes within the content. A route has a slug property and Page property. Page is another data type. A page is an array of basic types like image, text, date, and yes even video. So in our scenario, the team- without you, the developer- can go in add their route, name whatever they'd like, and start building out their custom page. Arranging the image on top, with the right copy, and the video above the fold is a matter of simply ordering the item in the content interface. Once it's all set, they can retrieve a preview link, send it to the director to view and approve, and then hit publish. Whenever content is added or updated, our site fetches the data, executes npm run build`, and then pushes that build-out across a network of CDNs. In this case, our fetch routes function has been happily iterating over a list of type routes on every build- this time it just so happens to have a '/coming-soon' element to it. So it creates the actual route, and then fetches the Page data from the CMS. It is then formatted, and styled as just as data from a Page type would be. Then it is bundled, and deployed with the rest of the site. VoilĂ . Now, it goes without saying that a lot of time and consideration goes into setting this all up, testing it, and getting it to work together. But, it illustrates the level to which we can empower others to create and build on this model of decoupling content, provided by Headless CMS. Just The Beginning It is clear that the paradigm has shifted to Headless content, and that consumption is an aggregate of many sources into multiple formats. From mobile apps, web, to voice assistants, and curbside pickup, people everywhere are adapting and changing their habits. As developers, we have the tools to build an awesome and empowering experience for others to create their vision on top of this. I am excited to see the next idea, product, or format that is published without direct help from the developers....

Testing a Fastify app with the NodeJS test runner cover image

Testing a Fastify app with the NodeJS test runner

Introduction Node.js has shipped a built-in test runner for a couple of major versions. Since its release I haven’t heard much about it so I decided to try it out on a simple Fastify API server application that I was working on. It turns out, it’s pretty good! It’s also really nice to start testing a node application without dealing with the hassle of installing some additional dependencies and managing more configurations. Since it’s got my stamp of approval, why not write a post about it? In this post, we will hit the highlights of the testing API and write some basic but real-life tests for an API server. This server will be built with Fastify, a plugin-centric API framework. They have some good documentation on testing that should make this pretty easy. We’ll also add a SQL driver for the plugin we will test. Setup Let's set up our simple API server by creating a new project, adding our dependencies, and creating some files. Ensure you’re running node v20 or greater (Test runner is a stable API as of the 20 major releases) Overview `index.js` - node entry that initializes our Fastify app and listens for incoming http requests on port 3001 `app.js` - this file exports a function that creates and returns our Fastify application instance `sql-plugin.js` - a Fastify plugin that sets up and connects to a SQL driver and makes it available on our app instance Application Code A simple first test For our first test we will just test our servers index route. If you recall from the app.js` code above, our index route returns a 501 response for “not implemented”. In this test, we're using the createApp` function to create a new instance of our Fastify app, and then using the `inject` method from the Fastify API to make a request to the `/` route. We import our test utilities directly from the node. Notice we can pass async functions to our test to use async/await. Node’s assert API has been around for a long time, this is what we are using to make our test assertions. To run this test, we can use the following command: By default the Node.js test runner uses the TAP reporter. You can configure it using other reporters or even create your own custom reporters for it to use. Testing our SQL plugin Next, let's take a look at how to test our Fastify Postgres plugin. This one is a bit more involved and gives us an opportunity to use more of the test runner features. In this example, we are using a feature called Subtests. This simply means when nested tests inside of a top-level test. In our top-level test call, we get a test parameter t` that we call methods on in our nested test structure. In this example, we use `t.beforeEach` to create a new Fastify app instance for each test, and call the `test` method to register our nested tests. Along with `beforeEach` the other methods you might expect are also available: `afterEach`, `before`, `after`. Since we don’t want to connect to our Postgres database in our tests, we are using the available Mocking API to mock out the client. This was the API that I was most excited to see included in the Node Test Runner. After the basics, you almost always need to mock some functions, methods, or libraries in your tests. After trying this feature, it works easily and as expected, I was confident that I could get pretty far testing with the new Node.js core API’s. Since my plugin only uses the end method of the Postgres driver, it’s the only method I provide a mock function for. Our second test confirms that it gets called when our Fastify server is shutting down. Additional features A lot of other features that are common in other popular testing frameworks are also available. Test styles and methods Along with our basic test` based tests we used for our Fastify plugins - `test` also includes `skip`, `todo`, and `only` methods. They are for what you would expect based on the names, skipping or only running certain tests, and work-in-progress tests. If you prefer, you also have the option of using the describe` → `it` test syntax. They both come with the same methods as `test` and I think it really comes down to a matter of personal preference. Test coverage This might be the deal breaker for some since this feature is still experimental. As popular as test coverage reporting is, I expect this API to be finalized and become stable in an upcoming version. Since this isn’t something that’s being shipped for the end user though, I say go for it. What’s the worst that could happen really? Other CLI flags —watch` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--watch —test-name-pattern` - https://nodejs.org/dist/latest-v20.x/docs/api/cli.html#--test-name-pattern TypeScript support You can use a loader like you would for a regular node application to execute TypeScript files. Some popular examples are tsx` and `ts-node`. In practice, I found that this currently doesn’t work well since the test runner only looks for JS file types. After digging in I found that they added support to locate your test files via a glob string but it won’t be available until the next major version release. Conclusion The built-in test runner is a lot more comprehensive than I expected it to be. I was able to easily write some real-world tests for my application. If you don’t mind some of the features like coverage reporting being experimental, you can get pretty far without installing any additional dependencies. The biggest deal breaker on many projects at this point, in my opinion, is the lack of straightforward TypeScript support. This is the test command that I ended up with in my application: I’ll be honest, I stole this from a GitHub issue thread and I don’t know exactly how it works (but it does). If TypeScript is a requirement, maybe stick with Jest or Vitest for now 🙂...