> ## Documentation Index
> Fetch the complete documentation index at: https://ghost-renovate-mint-4-x.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Working With Eleventy

> Build a completely custom front-end for your Ghost site with the flexibility of Static Site Generator [Eleventy](http://11ty.io).

***

<Frame>
  <img src="https://mintcdn.com/ghost-renovate-mint-4-x/inRwn_VgcH9hJgfM/images/0ed9faae-admin-api-eleventy-diagram_hu5ba97386724b594b90daeca2cbf04049_20855_1000x0_resize_q100_h2_box_3.webp?fit=max&auto=format&n=inRwn_VgcH9hJgfM&q=85&s=87e4a5bbadf40b3b3a2f7daf20ddac18" width="1000" height="523" data-path="images/0ed9faae-admin-api-eleventy-diagram_hu5ba97386724b594b90daeca2cbf04049_20855_1000x0_resize_q100_h2_box_3.webp" />
</Frame>

## Eleventy and Ghost

Eleventy is a good fit for a lightweight Ghost front-end. Ghost remains the CMS: editors write posts, manage pages, upload images, and handle publishing in Ghost Admin. Eleventy reads that content from the [Content API](/content-api/) when the site builds.

For most Eleventy sites, use the official [JavaScript Content API client](/content-api/javascript/). It is maintained as part of the [TryGhost SDK](https://github.com/TryGhost/SDK) and works in Node.js during Eleventy builds.

Use the [Admin API](/admin-api/) only for private server-side scripts, such as imports or custom publishing tools. Do not put an Admin API key in client-side JavaScript.

## Prerequisites

This configuration requires basic knowledge of JavaScript and HTML templates. You will also need:

* A running Ghost site, either self-hosted or on [Ghost(Pro)](https://ghost.org/pricing/)
* A custom integration in Ghost Admin so you can copy the Content API URL and key
* An Eleventy project

Create the Content API key from **Settings -> Integrations** in Ghost Admin. For more detail, see [Content API authentication](/content-api/javascript/#authentication).

## Start from a new Eleventy site

If you already have an Eleventy site, skip to the Content API setup. Otherwise, create a small project and install Eleventy:

```bash theme={"dark"}
mkdir my-eleventy-site
cd my-eleventy-site
npm init -y
npm install @11ty/eleventy @tryghost/content-api dotenv
```

Eleventy documents the same project setup in the [official Eleventy docs](https://www.11ty.dev/docs/).

## Install the Ghost Content API client

If you started with an existing Eleventy site, install the Ghost Content API client:

```bash theme={"dark"}
npm install @tryghost/content-api dotenv
```

Add your Ghost credentials to environment variables. A local `.env` file is fine if your deployment platform loads it and you keep it out of source control.

```bash theme={"dark"}
GHOST_API_URL=https://demo.ghost.io
GHOST_CONTENT_API_KEY=22444f78447824223cefc48062
GHOST_API_VERSION=v6.0
```

For Ghost(Pro), the URL is usually your `.ghost.io` URL. For self-hosted Ghost, use the public URL for your Ghost install.

## Fetch Ghost content with a global data file

Eleventy global data files make API data available to every template. Create `_data/ghost.js`:

```js theme={"dark"}
// _data/ghost.js
require("dotenv").config();
const GhostContentAPI = require("@tryghost/content-api");

const api = new GhostContentAPI({
  url: process.env.GHOST_API_URL,
  key: process.env.GHOST_CONTENT_API_KEY,
  version: process.env.GHOST_API_VERSION || "v6.0",
});

async function browseAll(resource, options = {}) {
  const items = [];
  let page = 1;

  while (page) {
    const response = await resource.browse({
      ...options,
      limit: 100,
      page,
    });

    items.push(...response);
    page = response.meta.pagination.next || null;
  }

  return items;
}

module.exports = async function() {
  const [posts, pages, tags, authors, settings] = await Promise.all([
    browseAll(api.posts, { include: "tags,authors", order: "published_at DESC" }),
    browseAll(api.pages, { include: "tags,authors" }),
    browseAll(api.tags, { include: "count.posts" }),
    browseAll(api.authors, { include: "count.posts" }),
    api.settings.browse(),
  ]);

  return {
    posts,
    pages,
    tags,
    authors,
    settings,
  };
};
```

Ghost 6.0 and later limit each browse request to 100 records, so static builds need pagination. The helper above follows the `meta.pagination.next` value until there are no more pages. See [pagination for building static sites](/jamstack/#pagination-for-building-static-sites) and [Content API pagination](/content-api/pagination) for the underlying API behavior.

## List posts on the home page

Create `index.njk` and read from the `ghost` data object:

```njk theme={"dark"}
---
title: Latest posts
---

<!doctype html>
<html lang="{{ ghost.settings.lang or 'en' }}">
  <head>
    <meta charset="utf-8">
    <title>{{ ghost.settings.title }}</title>
  </head>
  <body>
    <h1>{{ ghost.settings.title }}</h1>

    <ul>
      {% for post in ghost.posts %}
        <li>
          <a href="/posts/{{ post.slug }}/">{{ post.title }}</a>
          {% if post.primary_author %}
            by {{ post.primary_author.name }}
          {% endif %}
        </li>
      {% endfor %}
    </ul>
  </body>
</html>
```

## Create one page per post

Use Eleventy pagination with a page size of `1` to create a static page for every Ghost post.

```njk theme={"dark"}
---
pagination:
  data: ghost.posts
  size: 1
  alias: post
permalink: "posts/{{ post.slug }}/index.html"
---

<!doctype html>
<html lang="{{ ghost.settings.lang or 'en' }}">
  <head>
    <meta charset="utf-8">
    <title>{{ post.title }} - {{ ghost.settings.title }}</title>
  </head>
  <body>
    <article>
      <h1>{{ post.title }}</h1>
      {% if post.primary_author %}
        <p>{{ post.primary_author.name }}</p>
      {% endif %}
      {{ post.html | safe }}
    </article>
  </body>
</html>
```

Ghost returns post HTML from content written in Ghost Admin. Rendering it with the Nunjucks `safe` filter is normal for a trusted CMS source, but do not use it for untrusted user-submitted HTML.

## Create tag and author pages

The same `ghost` data object includes tags and authors. For example, this creates one archive page per tag:

```njk theme={"dark"}
---
pagination:
  data: ghost.tags
  size: 1
  alias: tag
permalink: "tag/{{ tag.slug }}/index.html"
---

<!doctype html>
<html lang="{{ ghost.settings.lang or 'en' }}">
  <head>
    <meta charset="utf-8">
    <title>{{ tag.name }} - {{ ghost.settings.title }}</title>
  </head>
  <body>
    <h1>{{ tag.name }}</h1>

    <ul>
      {% for post in ghost.posts %}
        {% if post.primary_tag and post.primary_tag.slug == tag.slug %}
          <li><a href="/posts/{{ post.slug }}/">{{ post.title }}</a></li>
        {% endif %}
      {% endfor %}
    </ul>
  </body>
</html>
```

For larger sites, pre-group posts by tag in `_data/ghost.js` so templates do less filtering work.

## Run Eleventy locally

Once the data file and templates are in place, start Eleventy's local development server:

```bash theme={"dark"}
npx @11ty/eleventy --serve
```

Eleventy serves the site at `http://localhost:8080/` by default.

## Use the Admin API from private scripts

The Admin API can create, update, and publish content. It is useful for migrations or custom editorial tooling, but it is not needed to render a public Eleventy front-end.

Install the Admin API client only where the key stays private:

```bash theme={"dark"}
npm install @tryghost/admin-api
```

```js theme={"dark"}
// scripts/create-draft.js
const GhostAdminAPI = require("@tryghost/admin-api");

const admin = new GhostAdminAPI({
  url: process.env.GHOST_API_URL,
  key: process.env.GHOST_ADMIN_API_KEY,
  version: "v6.0",
});

admin.posts.add(
  {
    title: "Draft from a private script",
    html: "<p>This draft was created through the Admin API.</p>",
    status: "draft",
  },
  { source: "html" }
);
```

Run Admin API scripts on your own machine, in CI, or on a server. Never ship `GHOST_ADMIN_API_KEY` to the browser.

## Next steps

Read the [Content API JavaScript client](/content-api/javascript/) docs for available endpoints and options. Eleventy's [global data files](https://www.11ty.dev/docs/data-global/) and [pagination](https://www.11ty.dev/docs/pagination/) docs explain how API data becomes static pages.
