How to embed Keystone + SQLite in a Next.js app
In this tutorial, we're going to show you how to embed Keystone and an SQLite database into a Next.js app. By the end, your app will have a queryable GraphQL endpoint, based on your Keystone schema, running live on Vercel (for free!). Content remains editable via the Admin UI in your development environment, with changes published being with each deployment of the app.
We’ll take advantage of a new way of working with Keystone where you can run it from the same place you keep your frontend code and commit everything to Git.
If you’re happy to write content in a local-only environment, and don't need a writeable API in production, this use case may be handy for you.
Explaining “modes”
As a Headless CMS, by default Keystone works in Standalone mode. Where you host your Content API separately from your frontend(s). While this is great for scale, it complicates developing and deploying simple apps and websites.
Keystone 6 introduces a new Embedded mode, which brings a different approach to integrating Keystone. It lets you embed Keystone inside another app, which can streamline the development and deployment of simple projects.
Keystone 6 also introduces SQLite support – giving you the option to store your files and database content within your local Keystone repo instead of an external host.
Note: Modes is conceptual framework. It has no dedicated APIs, and there are no mentions of modes in the API docs.
Embedded mode comes with some limitations that we explore later on.
Lets get started!
Here's what we're going to do:
- Create a Next.js app
- Embed Keystone, and run an Admin UI you can read and write to locally
- Add a simple Keystone Schema with a
Post
List - Setup a secure read-only GraphQL API endpoint (and GraphQL Playground) that you can access in production
- Deploy the app to Vercel 🚀
Setup a Next.js app
Create a basic Next.js project with the --typescript
option in an empty directory.
yarn create next-app --typescript my-projectcd my-project
Keystone 6 has great TypeScript support. Including it in your project will make it easier to use Keystone’s APIs later.
Delete the /pages/api
directory. We’ll add a GraphQL API later in the tutorial. Your /pages
directory should now look like this:
.└── pages├── _app.tsx└── index.tsx
Start your local server
Run yarn dev
at the root of your project.
Next.js will start a local server for you at http://localhost:3000
Add Keystone to your project
Now that we have the Next.js starter with static files, let‘s embed Keystone into the app to blend file-based content with content you can edit using Keystone’s intuitive Admin UI.
Install dependencies
Add the following Keystone dependency to your project:
yarn add @keystone-next/keystone
Update .gitignore
Add the .keystone
directory to your .gitignore
file. The contents of .keystone
are generated at build time. You’ll never have to change them.
# In your .gitignore.keystone
Create your Keystone config
To create and edit blog records in Keystone’s Admin UI, add a keystone.ts
configuration file to your project root with a simple Post
list containing fields for a Title, Slug, and some Content.
Note: We're enabling experimental features to generate the APIs that make embedded mode work. These may change in future versions.
// keystone.tsimport { config, list } from '@keystone-next/keystone';import { text } from '@keystone-next/keystone/fields';const Post = list({fields: {title: text({ isRequired: true }),slug: text({ isIndexed: 'unique', isFilterable: true }),content: text(),},});export default config({db: { provider: 'sqlite', url: 'file:./app.db' },experimental: {generateNextGraphqlAPI: true,generateNodeAPI: true,},lists: { Post },});
For simplicity we set all Post fields as text
above. For a highly customisable rich text editor use the document
field type.
Add Keystone to Next.js config
Edit the next.config.js
file in your project root with the following:
// next.config.js/** @type {import('next').NextConfig} */- module.exports = {- reactStrictMode: true,- }+ const { withKeystone } = require("@keystone-next/keystone/next");+ module.exports = withKeystone({+ reactStrictMode: true,+ });
This is where the magic happens – the withKeystone
function lets Next.js encapsulate Keystone in its script runtime, while Keystone still operates independently of the Next.js frontend ✨
Update package scripts
Finally, make a small change to the scripts
object in package.json
to include Keystone‘s postinstall
script:
"scripts": {+ "postinstall": "keystone-next postinstall","dev": "next dev","build": "next build","start": "next start","lint": "next lint"},
Start all the things
Running yarn dev
again will do the following:
- Provision a GraphQL schema based on the configuration of
keystone.ts
- Build a Prisma.io schema (which Keystone uses to manage the database)
- Create the database and run a migration to set up your schema
- Serve Keystone’s Admin UI at http://localhost:8000
- Serve the Next.js frontend at http://localhost:3000
- Add a
postinstall
script that ensures everything works if we install other dependencies later on
Go ahead and add two post entries using your Admin UI, ensuring you only use hyphens-and-lowercase-chars
in the slug field for permalinks.
Query Keystone from Next.js
In order to query Keystone content we need to use the getStaticProps
and getStaticPaths
functions in Next.js. Let’s overwrite the contents of pages/index.tsx
with the following to query posts from Keystone:
// pages/index.tsximport { InferGetStaticPropsType } from 'next';import Link from 'next/link';// Import the generated Lists API from Keystoneimport { query } from '.keystone/api';// Home receives a `posts` prop from `getStaticProps` belowexport default function Home({posts,}: InferGetStaticPropsType<typeof getStaticProps>) {return (<div><main style={{margin: "3rem"}}><h1>Hello World! 👋🏻 </h1><ul>{/* Render each post with a link to the content page */}{posts.map(post => (<li key={post.id}><Link href={`/post/${post.slug}`}><a>{post.title}</a></Link></li>))}</ul></main></div>)}// Here we use the Lists API to load all the posts we want to display// The return of this function is provided to the `Home` componentexport async function getStaticProps() {const posts = await query.Post.findMany({ query: 'id title slug' });return { props: { posts } };}
Now add a /post
subdirectory in /pages
and include the code below in [slug].tsx
. This will generate a new webpage every time you publish a new post entry in Keystone’s Admin UI.
// pages/post/[slug].tsximport {GetStaticPathsResult,GetStaticPropsContext,InferGetStaticPropsType,} from 'next';import Link from 'next/link';import { query } from '.keystone/api';export default function PostPage({post,}: InferGetStaticPropsType<typeof getStaticProps>) {return (<div><main style={{margin: "3rem"}}><div><Link href="/"><a>← back home</a></Link></div><h1>{post.title}</h1><p>{post.content}</p></main></div>);}export async function getStaticPaths(): Promise<GetStaticPathsResult> {const posts = await query.Post.findMany({query: `slug`,});const paths = posts.map(post => post.slug).filter((slug): slug is string => !!slug).map(slug => `/post/${slug}`);return {paths,fallback: false,};}export async function getStaticProps({params,}: GetStaticPropsContext) {const post = await query.Post.findOne({where: { slug: params!.slug as string },query: 'id title content',});return { props: { post } };}
Run yarn dev
again.
Congratulations! 🙌 You now have:
- A Next.js frontend blending static pages from your frontend repo with dynamic content from your database
- Dynamic pages powered by Keystone content that‘s editable in an intuitive Admin UI.
Bonus: add the GraphQL API to the frontend
To get a read-only GraphQL API and GraphQL Playground in production, add /pages/api/graphql.tsx
with the following:
// pages/api/graphql.tsxexport { default, config } from '.keystone/next/graphql-api';
This takes the fully functional GraphQL API that Keystone is already generating and makes it available as an endpoint and GraphQL Playground within the Next.js frontend app at http://localhost:3000/api/graphql.
This gives you the ability to implement a search against the same content API you have running at build time, in run time.
Bonus: deploy to Vercel
To get your project on the internet via Vercel hosting complete the following steps:
- Commit your project to a repository in Github, Gitlab, or Bitbucket.
- Login to Vercel using an account from the services above.
- Create a new Vercel project and link it to your newly created repository.
Standalone VS Embedded modes
There are some limitations to be aware of running Keystone the way we've described in this tutorial:
- Because Admin UI access is restricted to a local development runtime, this mode is (currently) not suitable when you need more than one editor.
- SQLite doesn’t support scalability, concurrency, and centralisation as well as PostgreSQL. If your project has scaling needs, you’re probably better off with standalone mode and external PostgreSQL hosting.
It also has some advantages though:
- Your content and presentation are version controlled together
- It's an easy way to power a data-driven Next.js app
- The frontend can easily be deployed to hosts like Vercel
Embedded mode is a great way to operate a personal Next.js blog or portfolio with a CMS for content editing, instead of MDX.
Summary
Keystone’s Embedded mode and SQLite support gives you the option to run a self contained CMS from the same place you keep your frontend code. While this option restricts read-write access to people who can run the project in local development, it has advantages with ease of setup, security, and web deployment. This is also a great way to deploy a read-only API on the web for content you manage on your computer.