Converting a Ghost CMS Theme to Next.js with Cursor AI

Erik ChenErik Chen
12 min read

In this tutorial we’ll take a Ghost CMS theme (built with Handlebars templates) and convert it into a working Next.js site using the Cursor AI coding editor. We assume you know basic HTML/CSS/JavaScript but are new to Next.js. We’ll walk through setting up Cursor, creating a Next.js project, installing the Ghost Content API, and using Cursor’s AI features to transform the Ghost templates ( .hbs files) into React components. We’ll also fetch live content from Ghost via its Content API and rebuild pages (Home, Post, Tags) with dynamic routing. By the end you’ll have a deployed Next.js version of your Ghost blog, with as much original styling carried over as possible.

1. Install and Configure Cursor

  1. Download and install Cursor AI. Go to cursor.com and download the installer for your OS (Windows, Mac, or Linux). Run the installer and follow the prompts (it works like any standard app) (Cursor AI Code Editor: How to Set Up & Use (Step-by-Step)). Once installed, launch Cursor and sign in (you’ll automatically get a free trial of Cursor Pro).

  2. Import or set up your IDE preferences. On first launch, Cursor can import your VS Code settings so your themes, shortcuts, and extensions carry over (Cursor AI Code Editor: How to Set Up & Use (Step-by-Step)). You can also choose a keyboard shortcut scheme if you used another editor. Enable “Codebase Indexing” so Cursor can scan your project for smarter suggestions.

  3. Enable helpful AI features. Open the Cursor settings (click the gear icon) and under Features turn on Cursor Tab, Suggestions in Comments, and Auto Import (The Perfect Cursor AI setup for React and Next.js). This ensures you get the powerful tab completion and AI-generated comments/code suggestions. It’s also helpful to set the chat mode to “Agent” and enable auto-refresh/scroll in the Chat settings (The Perfect Cursor AI setup for React and Next.js). These tweaks let Cursor autocomplete code (Tab), fix imports automatically, and allow AI edits across files. Now Cursor is ready to help you build a React/Next project.

2. Create a Next.js Project and Add Dependencies

  1. Initialize a new Next.js app. In your terminal (Cursor has an integrated terminal, or use your OS terminal), create a new Next.js project. For example, run:

     npx create-next-app@latest ghost-to-next --typescript
    

    This sets up a project named ghost-to-next using TypeScript. You can omit --typescript if you prefer plain JavaScript. Answer the prompts (you can accept defaults). Once done, cd ghost-to-next into the project folder.

  2. Install the Ghost Content API client. Next.js will fetch content from Ghost’s Content API. Ghost provides an official JS client package for this. Install it by running:

     npm install @tryghost/content-api
    

    (Or yarn add @tryghost/content-api.) This matches the documentation example and ensures you have the GhostContentAPI class in your code (Content API JavaScript Client). If your Ghost theme uses helpers like string manipulation, you can also install @tryghost/helpers or @tryghost/string, but they are optional.

  3. Open the project in Cursor. In Cursor, use File > Open Folder (or the command palette) to open ghost-to-next. Cursor will index the codebase (if you enabled indexing) and you can now use its AI features on your new Next.js files.

3. Plan the Project Structure

Before coding, map the Ghost theme’s structure to a Next.js layout. A typical Ghost theme looks like this (Ghost Handlebars Theme Structure - Developer Documentation):

ghost-theme/
├── assets/
│   ├── css/
│   │   └── screen.css
│   ├── fonts/
│   ├── images/
│   └── js/
├── default.hbs
├── index.hbs         (required)
└── post.hbs          (required)

The required Ghost templates are index.hbs (list of posts) and post.hbs (single post), with optional partials and a default.hbs layout (Ghost Handlebars Theme Structure - Developer Documentation). We will mirror this in Next.js as follows:

  • Pages:

    • pages/index.jsx – Home page, will fetch and list recent posts.

    • pages/posts/[slug].jsx – Dynamic route for individual posts (slug from Ghost).

    • pages/tag/[slug].jsx – Dynamic tag archive page (optional, for listing posts by tag).

    • pages/_app.jsx – Global app file (for CSS imports).

  • Components:

    • We’ll create a components/ folder for reusable parts like header, footer, or post previews (converted from any Ghost partials).
  • Public assets:

    • Copy the Ghost theme’s assets (especially screen.css) into public/assets/css/. For example:

        ghost-to-next/
        ├── pages/
        │   ├── index.jsx
        │   ├── posts/[slug].jsx
        │   ├── tag/[slug].jsx
        │   └── _app.jsx
        ├── components/
        ├── public/
        │   └── assets/
        │       └── css/
        │           └── screen.css   # copied from Ghost theme
        ├── .env.local
        └── package.json
      

This way we can import screen.css in our Next app and reuse the original styling.

Planning ahead this structure will make converting templates and fetching data more straightforward. Notice how Ghost’s index.hbs and post.hbs will become React components/pages (index.jsx and [slug].jsx), and assets from /assets/css stay in a similar public location.

4. Convert Ghost Templates to React Components with Cursor AI

With the project set up, use Cursor to transform the Ghost Handlebars templates into JSX:

  • Open a Ghost template in Cursor. For example, open index.hbs from your Ghost theme. Select its content, or place your cursor at the top, and either use Cursor Tab (start typing a comment) or Chat (Ctrl+K) to assist. For instance, you might type:

      // Convert this Ghost Handlebars snippet to React JSX
    

    Then press Tab. Cursor will analyze the code and generate a React version. For example, a Ghost loop like:

      {{#foreach posts}}
        <article>
          <h2><a href="{{url}}">{{title}}</a></h2>
          <p>{{excerpt}}</p>
        </article>
      {{/foreach}}
    

    could be turned into JSX by Cursor as:

      {posts.map(post => (
        <article key={post.id}>
          <h2><Link href={`/posts/${post.slug}`}><a>{post.title}</a></Link></h2>
          <p>{post.excerpt}</p>
        </article>
      ))}
    

    (In practice, you’ll import Link from next/link and handle classes/CSS as needed.)

  • Split into components. If default.hbs is a layout, you can create a Layout.jsx component with the common header and footer. Cursor can help here too: copy the common HTML from default.hbs, select it, and use a comment like // Convert this HTML layout to a React component and press Tab. Cursor will scaffold a React component.

  • Repeat for other templates. Do the same for post.hbs (single post page) and any partials. Cursor’s AI Composer/Agent can intelligently refactor chunks of code, convert loops, conditionals, and inline helpers into React logic. For example, Ghost’s {{#post}} … {{/post}} helper can become a check on post data in React. If you’re stuck, you can also ask Cursor’s Chat (Ctrl+K) questions like “How to convert a Ghost helper to JSX?” or provide an HBS snippet and ask it to rewrite it in React style.

Cursor makes this conversion faster by handling the repetitive parts. As you work, it will auto-add imports (the Auto Import feature) and can even detect lint issues and suggest fixes (if enabled) (The Perfect Cursor AI setup for React and Next.js).

5. Set Up Ghost Environment Variables

To fetch live content from your Ghost site, you need its API URL and a Content API key. In Ghost Admin, go to Integrations and create a new “Custom Integration”. Ghost will show you a Content API Key (a long hex string) and remind you of your API URL (your site’s URL, without a trailing slash). According to Ghost docs:

  • url – API domain, must not end in a trailing slash.

  • key – hex string copied from the “Integrations” screen in Ghost Admin (Content API JavaScript Client).

In your Next.js project, create a file .env.local at the root and add:

NEXT_PUBLIC_GHOST_API_URL=https://your-ghost-site.com
NEXT_PUBLIC_GHOST_CONTENT_API_KEY=your_content_api_key_here

Using the NEXT_PUBLIC_ prefix ensures these variables are available in the browser (Next will embed them in the client bundle). These variables match the Ghost client’s options (url and key). We do not commit .env.local to Git, so add it to .gitignore. Now your code can read the Ghost URL and key via process.env.NEXT_PUBLIC_GHOST_API_URL and process.env.NEXT_PUBLIC_GHOST_CONTENT_API_KEY.

6. Fetch Ghost Content with the Content API

Next.js can fetch Ghost’s data at build time or on-demand. We’ll use the official client library (@tryghost/content-api) we installed. In any page or component, import it and initialize it with our env vars:

import GhostContentAPI from '@tryghost/content-api';

const api = new GhostContentAPI({
  url: process.env.NEXT_PUBLIC_GHOST_API_URL,
  key: process.env.NEXT_PUBLIC_GHOST_CONTENT_API_KEY,
  version: 'v5.0'
});

This matches the Ghost documentation example for creating the client (Content API JavaScript Client). With api ready, you can call its methods like api.posts.browse(), api.posts.read(), api.tags.browse(), etc. For example, to get the latest posts on the home page:

export async function getStaticProps() {
  const posts = await api.posts.browse({ include: 'tags,authors' });
  return { props: { posts } };
}

This uses api.posts.browse({limit: n}) to fetch many posts (Ghost uses pagination). The documentation shows a similar example of browsing 5 posts and iterating titles (Content API JavaScript Client). In our case, we return the data as props for React to render. We can also fetch pages (api.pages.browse() and api.pages.read({slug})) or tags (api.tags.browse()) the same way.

(Cursor’s AI can help write these functions too: you could type a comment like // fetch all posts from Ghost API and press Tab to get boilerplate code.)

7. Build the Home, Post, and Tag Pages

Now we create the actual Next.js pages and use the data:

  • Home page (pages/index.jsx): Use getStaticProps to fetch recent posts (as above) and render them in a list or grid. You can use the JSX we converted earlier. For example, map over posts and output titles, excerpts, and links to each post’s page (/posts/[slug]).

  • Post detail page (pages/posts/[slug].jsx): This is a dynamic route file. Write getStaticPaths() to pre-generate pages for each post. For example:

      export async function getStaticPaths() {
        const posts = await api.posts.browse({limit: 1000}); 
        const paths = posts.map(post => ({ params: { slug: post.slug } }));
        return { paths, fallback: false };
      }
    

    Then getStaticProps({ params }) can fetch the single post by slug:

      export async function getStaticProps({ params }) {
        const post = await api.posts.read({ slug: params.slug });
        return { props: { post } };
      }
    

    Inside the component, render the post’s title and content. Ghost post content is usually returned in HTML form, so you might render it with dangerouslySetInnerHTML inside a container (after sanitizing if needed). Otherwise, use the structured data (post.html, post.title, etc.).

  • Tag page (pages/tag/[slug].jsx): If your theme had tag archives, do the same: getStaticPaths() with api.tags.browse() to get all tag slugs, then in getStaticProps use api.posts.browse({filter: tag:${params.slug}}) or api.tags.read({slug: params.slug, include: 'count.posts'}) to find posts for that tag. Render the tag’s name and its posts.

Cursor can assist: For example, place a comment // Generate getStaticPaths for posts slugs and press Tab; it will often generate the loop and return statement automatically. Cursor’s AI is aware of Next’s conventions, so it can scaffold these functions if you hint at them.

In your JSX, use Next.js’s <Link> component for navigation. For example, on the home page use:

import Link from 'next/link';
// ...
<Link href={`/posts/${post.slug}`}><a>{post.title}</a></Link>

In dynamic pages, the filename [slug].jsx means Next.js will route /posts/my-post-slug to that component. Similarly, pages under pages/tag/[slug].jsx will handle /tag/technology, for example. Because we set fallback: false, any slug not returned by our API paths will give a 404. This matches the Ghost theme’s behavior (only existing posts/tags have pages).

9. Apply Ghost Theme Styling

We want to reuse the Ghost theme’s CSS. Earlier, we copied screen.css into public/assets/css/screen.css. Now import it in pages/_app.jsx or in individual pages:

import '../public/assets/css/screen.css';

This applies the original theme’s styles site-wide. You may need to adjust class names: Ghost themes often use semantic class names or none at all. In your converted JSX, check the HTML elements and classes (from default.hbs, post.hbs, etc.) and ensure they match the CSS rules in screen.css. Cursor AI can help here too: if you notice a missing class, comment something like // add className="site-title" to header element and use Tab to let Cursor fix it.

Because we placed CSS under public/assets/css, remember to update any paths to images or fonts inside screen.css if needed (paths may need adjusting to Next’s public/ structure).

By importing the CSS, much of the original look & feel carries over without rewriting styles. For any interactive components (menus, etc.), make sure any needed scripts are also added (for example, Ghost themes sometimes use JavaScript for menu toggles).

10. Test Locally and Deploy

  1. Test your app. Run npm run dev to start the Next.js development server. Open http://localhost:3000 and browse the pages. Check that the home page shows posts, links work, and individual post pages display correct content. Fix any errors (Cursor can help debug – for example, use the AI Console to ask why something isn’t working).

  2. Build for production. Run npm run build and npm run start to make sure a production build works. This compiles static pages using your getStaticProps logic.

  3. Deploy online. Commit your code to GitHub and connect to a hosting service like Vercel (which has first-class Next.js support) or Netlify. On Vercel, you can set the same environment variables (NEXT_PUBLIC_GHOST_API_URL and NEXT_PUBLIC_GHOST_CONTENT_API_KEY) in the project’s dashboard. When you deploy, Vercel will build the site and publish it.

  4. Final checks. Verify on the deployed URL that the site matches your Ghost theme’s style and that content loads from Ghost. Your Next.js site is now a headless front-end for Ghost CMS.


With these steps, you’ve successfully converted a Ghost Handlebars theme into a Next.js project using Cursor’s AI-powered assistance. You set up Cursor for React/Next development (The Perfect Cursor AI setup for React and Next.js) (Cursor AI Code Editor: How to Set Up & Use (Step-by-Step)), installed Next.js and the Ghost Content API client (Content API JavaScript Client), planned the file structure (mirroring index.hbspages/index.jsx, etc.) (Ghost Handlebars Theme Structure - Developer Documentation), and used Cursor to transform templates to JSX. You stored your Ghost API URL/key in environment variables (Content API JavaScript Client), fetched posts/tags via the Content API (Content API JavaScript Client), and built dynamic pages with Next.js routing. Finally, you applied the original CSS so the new site looks like your Ghost theme. Congratulations on modernizing your blog front-end with Next.js and Cursor!


0
Subscribe to my newsletter

Read articles from Erik Chen directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Erik Chen
Erik Chen