Jedwal.co is a headless CMS I built on top of Google Drive; it turns Google Docs into markdown (MDX) formatted posts (and Google Sheets into JSON REST API endpoints).
I used it to power this portfolio. Here’s how.
To set expectations, this post will be lightly technical and assumes you have general familiarity with NextJS.
Choosing a project template
Since Jedwal is headless, we’ll need a project template to work with.
I’ve chosen the Portfolio Starter Kit from Vercel. It uses NextJS static site generation with MDX, so it’s a good fit for our content type. I’m using Vercel hosting for simplicity.
By default, this template reads MDX files from inside the repository. We’ll refactor it later to pull from our CMS instead.
Creating our first CMS Post
Now that we have our template repository set up, let’s create our first Jedwal CMS post.
We’ll start by creating a new Google Doc for the post (here’s the Google Doc for this one)! Then, in the Jedwal dashboard, we’ll,
- Create a new post from the Google Doc.
- Add a category to our post. Eventually, we’ll have two types of posts (
funandwork). Let’s addfunto this post.
Here’s a walkthrough:
This creates a content URL for our post. We can open it directly and check out the content:
https://api.jedwal.co/doc/117187395759203962885/this-site-uses-jedwal
Inside, the response will look something like this. Note the content field contains our MDX post data.
{
"content":"---\n\ntitle: 'This portfolio uses the CMS I made'\n\nstartDate: 'Sep. 2025'\n\nendDate: Feb. 2023\n\nrank: 1\n\nsummary: 'Sean McClure | Fun | This portfolio uses the CMS I made.\n\nimage: '/jedwal-homepage.png'\n\n---\n\n<Image sr=\"/jedwal-blog-splash.png\" al=\"Jedwal Splash Page\" widt={1200} heigh={630} />\n\n[Jedwal.co](http://Jedwal.co) is a headless CMS I built on top of Google Drive; it turns Google Docs into…",
"title":"blog - jedwal",
"published_at":"2025-09-25T01:49:18.127614",
"creator":"Sean McClure”
}
Getting a list of all our posts
Once we create more posts (I’ll skip that for now), we’ll need a way to keep track of all of them.
Jedwal exposes an endpoint that lists all posts for a user,
https://api.jedwal.co/docs/117187395759203962885
We can also filter for posts of a specific category with the categories query param like,
https://api.jedwal.co/docs/117187395759203962885?categories=fun
Wiring up our CMS posts into our template
Okay, we have our portfolio template and our CMS posts. Let’s wire them together.
This requires replacing the template’s logic that reads posts from the filesystem with new logic that fetches posts from Jedwal.
The template uses a function getBlogPosts() (and other helpers) that we’ll replace with our own. Here’s our implementation:
// app/utils.ts
// Define our Jedwal URLs/configs
const jedwalAccountId = "117187395759203962885" // This is my Jedwal Account ID
const jedwalPostBaseUrl = `https://api.jedwal.co/doc/${jedwalAccountId}`
const jedwalCollectionUrl = `https://api.jedwal.co/docs/${jedwalAccountId}`
async function fetchMdxFromJedwal(slug: string) {
const res = await fetch(`${jedwalPostBaseUrl}/${slug}`)
if (!res.ok) {
throw new Error(`${res.statusText}: url ${jedwalPostBaseUrl}/${slug}`)
}
const data = await res.json()
return parseFrontmatter(data.content)
}
export async function getPosts(postType: "work" | "fun") {
const res = await fetch(`${jedwalCollectionUrl}?categories=${postType}`)
const { apis } = await res.json() // fetch a list of all our posts
// now fetch the content for each post
return Promise.all(
apis.map(async ({ doc_api_name, slug }) => {
const { metadata, content } = await fetchMdxFromJedwal(doc_api_name)
return { metadata, slug, content }
})
)
}
We can then use this to create a /fun/ route that generates pages for all posts in the fun category.
// app/fun/[slug]/page.tsx
import { notFound } from 'next/navigation'
import { CustomMDX } from 'app/components/mdx'
import { getPosts } from 'app/utils'
export async function generateStaticParams() {
let posts = await getPosts("fun") // only generate pages for “fun” posts
return posts.map((post) => ({
slug: post.slug,
}))
}
export default async function WorkPost({ params }) {
let posts = await getPosts("fun")
let post = posts.find((post) => post.slug === params.slug)
if (!post) {
notFound()
}
return (
<section>
<article className="prose">
<CustomMDX source={post.content} />
</article>
</section>
)
That’s about it!
When we deploy our app, it will fetch all fun posts and create a page for them. I skipped a few small steps here, so to see the full implementation, you can explore the repository for this portfolio (coming soon, sorry).
Before we go, let’s walk through a few more details and features…
Optimizing Images
Jedwal supports images out-of-the-box. You can add images to your Google Docs and they will be automatically turned into markdown image format (like this cat!).

But, NextJS also has great, optimized image support. Using MDX, we can use NextJS’s <Image/> component directly. We’ll,
- Upload an image to the
public/directory of our portfolio repository. - In our source Google Doc, define an image component using MDX component syntax pointing to that image
For example,
<Image src="/scary-cat-shark.jpg" alt="cat shark" width={300} height={150} />
Will create:
Automatically updating the site on content changes
Since we’re using NextJS’s static site generation, our portfolio won’t automatically update when we republish our posts through Jedwal; it will only update after we re-deploy the site.
We could manually kick off a deploy every time we update content, but that’s tedious.
To help with this, Jedwal has webhook integrations. We can configure posts to send webhook events on content publish.
We can integrate this with NextJS Deploy Hooks to trigger a build every time we update our post’s content.
To do this we’ll,
- Create a deploy hook for your repository through our Vercel dashboard
- In Jedwal, navigate to the post, scroll to Webhook Integrations, Paste deploy hook URL and click Add Webhook
Here’s a walkthrough of step 2:
Now, our portfolio will automatically update when our content does.
*To note: It would probably be better to trigger NextJS’s ISR on-demand page revalidation to reduce the number of rebuilds, but that would take more setup so I’m opting for a deploy trigger for simplicity. *
TL:DR
Basically,
- Jedwal is a headless CMS that allows you to edit content from Google Docs,
- we can integrate it with NextJS (or any framework that can use MDX!),
- and all the content in this portfolio is driven using it.
Cheers :)