Home Tutorials Render Storyblok Stories Dynamically in Remix In this short tutorial, we will see how to start making a real website with Remix and Storyblok. We will add a layout with a navigation bar and a footer to our website to make it look better. We will also see how we can add new pages and render them dynamically according to the added components inside them.
Requirements This is a part of the Ultimate Tutorial Guide for Remix . You can find the previous part of the series here , which shows you how to integrate Storyblok and Remix in 5 minutes. We recommend you take a look at that tutorial before starting this one.
Adding a Layout Let’s add a layout to our website by creating a static navigation bar and a footer. We will look at how to create dynamic navigation bars in an upcoming tutorial of this series.
Create two files in your components
folder, Navigation.jsx
and Footer.jsx
, and add the following code to Navigation.jsx
file:
components/Navigation.jsx Copy to clipboard
import { useState } from "react";
import { Link, NavLink } from "@remix-run/react";
const Navigation = () => {
const [openMenu, setOpenMenu] = useState(false);
return (
<div className="relative bg-white border-b-2 border-gray-100">
<div className="max-w-7xl mx-auto px-4 sm:px-6">
<div className="flex justify-between items-center py-6 md:justify-start md:space-x-10">
<div className="flex justify-start lg:w-0 lg:flex-1">
<Link prefetch="intent" to="/">
<span className="sr-only">Storyblok</span>
<img
className="h-20 w-auto sm:h-10 hidden sm:block"
src="https://a.storyblok.com/f/88751/251x53/0d3909fe96/storyblok-primary.png"
alt="Storyblok"
/>
<img
className="h-20 w-auto sm:h-10 sm:hidden"
src="https://a.storyblok.com/f/88751/92x106/835caf912a/storyblok-logo.png"
alt="Storyblok"
/>
</Link>
</div>
<div className="-mr-2 -my-2 md:hidden">
<button
type="button"
onClick={() => setOpenMenu(true)}
className="bg-white rounded-md p-2 inline-flex items-center justify-center text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
aria-expanded="false"
>
<span className="sr-only">Open menu</span>
{/* <!-- Heroicon name: outline/menu --> */}
<svg
className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4 6h16M4 12h16M4 18h16"
/>
</svg>
</button>
</div>
<div className="hidden md:flex items-center justify-end md:flex-1 lg:w-0 space-x-10">
<NavLink
prefetch="intent"
to="/about"
className="text-base font-medium text-gray-500 hover:text-gray-900"
>
About
</NavLink>
<NavLink
prefetch="intent"
to="/blog"
className="text-base font-medium text-gray-500 hover:text-gray-900"
>
Blog
</NavLink>
<NavLink
prefetch="intent"
to="/services"
className="text-base font-medium text-gray-500 hover:text-gray-900"
>
Services
</NavLink>
</div>
</div>
</div>
{/* <!--
Mobile menu, show/hide based on mobile menu state.
--> */}
{openMenu && (
<div className="absolute top-0 inset-x-0 p-2 transition transform origin-top-right md:hidden">
<div className="rounded-lg shadow-lg ring-1 ring-black ring-opacity-5 bg-white divide-y-2 divide-gray-50">
<div className="pt-5 pb-6 px-5">
<div className="flex items-center justify-between">
<div>
<img
className="h-8 w-auto"
src="https://a.storyblok.com/f/88751/92x106/835caf912a/storyblok-logo.png"
alt="Storyblok"
/>
</div>
<div className="-mr-2">
<button
type="button"
onClick={() => setOpenMenu(false)}
className="bg-white rounded-md p-2 inline-flex items-center justify-center text-gray-400 hover:text-gray-500 hover:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-inset focus:ring-indigo-500"
>
<span className="sr-only">Close menu</span>
{/* <!-- Heroicon name: outline/x --> */}
<svg
className="h-6 w-6"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M6 18L18 6M6 6l12 12"
/>
</svg>
</button>
</div>
</div>
<div className="mt-6">
<nav className="grid gap-y-8">
<Link
to="/about"
className="-m-3 p-3 flex items-center rounded-md hover:bg-gray-50"
>
{/* <!-- Heroicon name: outline/chart-bar --> */}
<span className="ml-3 text-base font-medium text-gray-900">
About
</span>
</Link>
<Link
to="/blog"
className="-m-3 p-3 flex items-center rounded-md hover:bg-gray-50"
>
{/* <!-- Heroicon name: outline/cursor-click --> */}
<span className="ml-3 text-base font-medium text-gray-900">
Blog
</span>
</Link>
<Link
to="/services"
className="-m-3 p-3 flex items-center rounded-md hover:bg-gray-50"
>
<span className="ml-3 text-base font-medium text-gray-900">
Services
</span>
</Link>
</nav>
</div>
</div>
</div>
</div>
)}
</div>
);
};
export default Navigation;
Hint: Make sure to replace the logos on lines 17 and 22 for desktop and mobile, respectively. You can choose any images you like. You must also add an image for the mobile navigation bar in line 97.
Similarly, add the following code to your Footer.jsx
file:
components/Footer.jsx Copy to clipboard
const Footer = () => (
<footer className="bg-white block w-full" aria-labelledby="footer-heading">
<h2 id="footer-heading" className="sr-only">Footer</h2>
<div className=" py-12 mx-auto max-w-7xl lg:py-16 px-12 lg:px-20">
<div className="grid md:grid-cols-4 md:gap-8 grid-cols-2">
<div className="mt-12 md:mt-0">
<h3 className="text-xs font-semibold tracking-wider text-blue-600 uppercase">Support</h3>
<ul role="list" className="mt-4 space-y-4">
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Pricing </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Documentation </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Guides </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> API Status </a>
</li>
</ul>
</div>
<div className="mt-12 md:mt-0">
<h3 className="text-xs font-semibold tracking-wider text-blue-600 uppercase">Support</h3>
<ul role="list" className="mt-4 space-y-4">
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Pricing </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Documentation </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Guides </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> API Status </a>
</li>
</ul>
</div>
<div className="mt-12 md:mt-0">
<h3 className="text-xs font-semibold tracking-wider text-blue-600 uppercase">Support</h3>
<ul role="list" className="mt-4 space-y-4">
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Pricing </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Documentation </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Guides </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> API Status </a>
</li>
</ul>
</div>
<div className="mt-12 md:mt-0">
<h3 className="text-xs font-semibold tracking-wider text-blue-600 uppercase">Support</h3>
<ul role="list" className="mt-4 space-y-4">
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Pricing </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Documentation </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> Guides </a>
</li>
<li>
<a href="#" className="text-sm font-normal text-gray-500 hover:text-gray-900"> API Status </a>
</li>
</ul>
</div>
</div>
</div>
<div className="px-5 py-12 mx-auto bg-gray-50 sm:px-6 md:flex md:items-center md:justify-between lg:px-20">
<div className="flex justify-center mb-8 space-x-6 md:order-last md:mb-0">
<a href="#" className="text-gray-400 hover:text-gray-500">
<span className="sr-only">Facebook</span>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fillRule="evenodd" d="M22 12c0-5.523-4.477-10-10-10S2 6.477 2 12c0 4.991 3.657 9.128 8.438 9.878v-6.987h-2.54V12h2.54V9.797c0-2.506 1.492-3.89 3.777-3.89 1.094 0 2.238.195 2.238.195v2.46h-1.26c-1.243 0-1.63.771-1.63 1.562V12h2.773l-.443 2.89h-2.33v6.988C18.343 21.128 22 16.991 22 12z" clipRule="evenodd"></path>
</svg>
</a>
<a href="#" className="text-gray-400 hover:text-gray-500">
<span className="sr-only">Instagram</span>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fillRule="evenodd" d="M12.315 2c2.43 0 2.784.013 3.808.06 1.064.049 1.791.218 2.427.465a4.902 4.902 0 011.772 1.153 4.902 4.902 0 011.153 1.772c.247.636.416 1.363.465 2.427.048 1.067.06 1.407.06 4.123v.08c0 2.643-.012 2.987-.06 4.043-.049 1.064-.218 1.791-.465 2.427a4.902 4.902 0 01-1.153 1.772 4.902 4.902 0 01-1.772 1.153c-.636.247-1.363.416-2.427.465-1.067.048-1.407.06-4.123.06h-.08c-2.643 0-2.987-.012-4.043-.06-1.064-.049-1.791-.218-2.427-.465a4.902 4.902 0 01-1.772-1.153 4.902 4.902 0 01-1.153-1.772c-.247-.636-.416-1.363-.465-2.427-.047-1.024-.06-1.379-.06-3.808v-.63c0-2.43.013-2.784.06-3.808.049-1.064.218-1.791.465-2.427a4.902 4.902 0 011.153-1.772A4.902 4.902 0 015.45 2.525c.636-.247 1.363-.416 2.427-.465C8.901 2.013 9.256 2 11.685 2h.63zm-.081 1.802h-.468c-2.456 0-2.784.011-3.807.058-.975.045-1.504.207-1.857.344-.467.182-.8.398-1.15.748-.35.35-.566.683-.748 1.15-.137.353-.3.882-.344 1.857-.047 1.023-.058 1.351-.058 3.807v.468c0 2.456.011 2.784.058 3.807.045.975.207 1.504.344 1.857.182.466.399.8.748 1.15.35.35.683.566 1.15.748.353.137.882.3 1.857.344 1.054.048 1.37.058 4.041.058h.08c2.597 0 2.917-.01 3.96-.058.976-.045 1.505-.207 1.858-.344.466-.182.8-.398 1.15-.748.35-.35.566-.683.748-1.15.137-.353.3-.882.344-1.857.048-1.055.058-1.37.058-4.041v-.08c0-2.597-.01-2.917-.058-3.96-.045-.976-.207-1.505-.344-1.858a3.097 3.097 0 00-.748-1.15 3.098 3.098 0 00-1.15-.748c-.353-.137-.882-.3-1.857-.344-1.023-.047-1.351-.058-3.807-.058zM12 6.865a5.135 5.135 0 110 10.27 5.135 5.135 0 010-10.27zm0 1.802a3.333 3.333 0 100 6.666 3.333 3.333 0 000-6.666zm5.338-3.205a1.2 1.2 0 110 2.4 1.2 1.2 0 010-2.4z" clipRule="evenodd"></path>
</svg>
</a>
<a href="#" className="text-gray-400 hover:text-gray-500">
<span className="sr-only">Twitter</span>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path d="M8.29 20.251c7.547 0 11.675-6.253 11.675-11.675 0-.178 0-.355-.012-.53A8.348 8.348 0 0022 5.92a8.19 8.19 0 01-2.357.646 4.118 4.118 0 001.804-2.27 8.224 8.224 0 01-2.605.996 4.107 4.107 0 00-6.993 3.743 11.65 11.65 0 01-8.457-4.287 4.106 4.106 0 001.27 5.477A4.072 4.072 0 012.8 9.713v.052a4.105 4.105 0 003.292 4.022 4.095 4.095 0 01-1.853.07 4.108 4.108 0 003.834 2.85A8.233 8.233 0 012 18.407a11.616 11.616 0 006.29 1.84"></path>
</svg>
</a>
<a href="#" className="text-gray-400 hover:text-gray-500">
<span className="sr-only">GitHub</span>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fillRule="evenodd" d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z" clipRule="evenodd"></path>
</svg>
</a>
<a href="#" className="text-gray-400 hover:text-gray-500">
<span className="sr-only">Dribbble</span>
<svg className="w-6 h-6" fill="currentColor" viewBox="0 0 24 24" aria-hidden="true">
<path fillRule="evenodd" d="M12 2C6.48 2 2 6.48 2 12s4.48 10 10 10c5.51 0 10-4.48 10-10S17.51 2 12 2zm6.605 4.61a8.502 8.502 0 011.93 5.314c-.281-.054-3.101-.629-5.943-.271-.065-.141-.12-.293-.184-.445a25.416 25.416 0 00-.564-1.236c3.145-1.28 4.577-3.124 4.761-3.362zM12 3.475c2.17 0 4.154.813 5.662 2.148-.152.216-1.443 1.941-4.48 3.08-1.399-2.57-2.95-4.675-3.189-5A8.687 8.687 0 0112 3.475zm-3.633.803a53.896 53.896 0 013.167 4.935c-3.992 1.063-7.517 1.04-7.896 1.04a8.581 8.581 0 014.729-5.975zM3.453 12.01v-.26c.37.01 4.512.065 8.775-1.215.25.477.477.965.694 1.453-.109.033-.228.065-.336.098-4.404 1.42-6.747 5.303-6.942 5.629a8.522 8.522 0 01-2.19-5.705zM12 20.547a8.482 8.482 0 01-5.239-1.8c.152-.315 1.888-3.656 6.703-5.337.022-.01.033-.01.054-.022a35.318 35.318 0 011.823 6.475 8.4 8.4 0 01-3.341.684zm4.761-1.465c-.086-.52-.542-3.015-1.659-6.084 2.679-.423 5.022.271 5.314.369a8.468 8.468 0 01-3.655 5.715z" clipRule="evenodd"></path>
</svg>
</a>
</div>
<div className="mt-8 md:mt-0 md:order-1">
<span className="mt-2 text-sm font-light text-gray-500">
Copyright © 2017-2022
<a href="#" className="mx-2 text-wickedblue hover:text-gray-500" rel="noopener noreferrer">Storyblok GmbH</a>
</span>
</div>
</div>
</footer>
)
export default Footer
Now, we need to add a Layout.jsx
file in the components folder, which will use the two newly created components. Add the following code:
components/Layout.jsx Copy to clipboard
import Footer from "./Footer";
import Navigation from "./Navigation";
const Layout = ({ children }) => (
<div className="text-center">
<Navigation />
{children}
<Footer />
</div>
);
export default Layout;
Having created these files, we need to add this layout to our root.jsx:
root.jsx Copy to clipboard
import {
Links,
LiveReload,
Meta,
Outlet,
Scripts,
ScrollRestoration,
} from "@remix-run/react";
import stylesheet from "~/tailwind.css";
import { storyblokInit, apiPlugin } from "@storyblok/react";
import Feature from "./components/Feature";
import Grid from "./components/Grid";
import Page from "./components/Page";
import Teaser from "./components/Teaser";
import Layout from "./components/Layout";
const components = {
feature: Feature,
grid: Grid,
teaser: Teaser,
page: Page,
};
storyblokInit({
accessToken: "your-preview-token",
use: [apiPlugin],
components,
});
export const links = () => [{ rel: "stylesheet", href: stylesheet }];
export default function App() {
return (
<html lang="en">
<head>
<meta charSet="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<Meta />
<Links />
</head>
<body>
<Layout>
<Outlet />
<ScrollRestoration />
<Scripts />
<LiveReload />
</Layout>
</body>
</html>
);
}
This code integrates Storyblok with a Remix application. It imports storyblokInit and apiPlugin from @storyblok/react for Storyblok functionality and custom React components like Feature , Grid , Page , and Teaser. These components correspond to Storyblok's components.
The components object maps Storyblok components to React components. storyblokInit is called with an accessToken for authentication, apiPlugin for API integration, and the components mapping. This setup enables dynamic content rendering in the application using Storyblok data. After saving your code, our home story should look something like this:
Home Story with a Layout
Adding Pages in Storyblok With our logic being complete, we can now add our Blog and About pages in our Storyblok space! To do that, simply go to Content {1} , Create new {2} , and then Choose Story {3} .
Adding a story
Now you can provide a name {1} – the slug will be filled out automatically for you. Let’s start with the About page.
Configuring a new story
Now, you can enter any headline you want for your newly created about page:
New block rendering correctly on the About page
We can similarly create the Blog
and the Services
pages as well. These stories are getting generated by the routes/$s.jsx
file, which is catching all the routes. Now, let’s also see how we can add the existing components to any of the pages.
On the right-hand side of the about story, we can see the empty body with a plus button that will allow us to add existing components to the page. If we click on it, we will see the following.
Add Components
We have a list of existing components and can choose any of them. Let’s add the Teaser component, and fill in the Name field for that. As soon as we start filling the content in, we see the changes on the visual editor as well.
Once we hover on the right-hand side, we should see a similar plus button below the Teaser . Let’s add a Grid component along with three Features as columns, similar to what we have on the home story. We should see something like this now.
Grid Addition
In a similar way, we can add any other components, and they will be rendered automatically. We could add another Grid
, and it should look something like this.
Dynamic Rendering
We can even add more components anywhere we like in the About story if we have those created. Now, we could also add the components of choice to the two other stories, Blog and Services , as well.
Wrapping Up Congratulations! You have created your first pages in Storyblok and they are rendered in your Remix project dynamically – how cool!?
Author Alexandra Spalato Alexandra Spalato is a Developer Relations Engineer at Storyblok, specializing in JAMstack & headless architecture. With a background in freelance dev & entrepreneurship, she brings a unique perspective to successful web projects. Alexandra drives progress in tech industry through speaking at conferences & empowering developers