---
title: Step-by-Step: Deploy a Headless CMS on Apache Sling with a JavaScript Front-End
siteUrl: https://logzly.com/slingtheweb
author: slingtheweb (Slinging the Web)
date: 2026-06-20T17:04:01.805636
tags: [sling, headlesscms, javascript]
url: https://logzly.com/slingtheweb/step-by-step-deploy-a-headless-cms-on-apache-sling-with-a-javascript-front-end
---


You’ve probably heard the buzz about headless CMSes and wondered why anyone would bother with Apache Sling when there are so many “plug‑and‑play” platforms out there. The truth is, Sling gives you a clean, resource‑oriented way to serve content that plays nicely with any front‑end framework you like. If you’re a JavaScript fan who wants full control over the API layer, this guide is for you.

## Why a Headless CMS on Sling?

When I first tried to stitch together a React site with a traditional CMS, I spent more time fighting the CMS’s page‑centric mindset than writing actual UI code. Sling flips the script: every piece of content is a resource, and every resource can be accessed via a simple URL. No extra plugins, no hidden magic. You get:

* **Predictable URLs** – `/content/site/en/articles/123.json` just works.  
* **Built‑in content negotiation** – ask for `.json`, `.html`, or `.xml` and Sling serves the right format.  
* **Open‑source freedom** – you can tweak the OSGi bundles, add your own servlets, or replace the storage backend.

All of that makes Sling a solid foundation for a headless CMS, especially when you pair it with a lightweight JavaScript front‑end that talks to the API over fetch.

## Prerequisites

Before we dive in, make sure you have the following on your machine:

* JDK 11 or newer  
* Apache Maven 3.6+  
* Sling Quickstart (the runnable JAR) – you can grab it from the Apache site.  
* Node.js 18+ (I use 20, but any recent version works)  
* A code editor – VS Code is my daily driver.

If any of these sound unfamiliar, take a quick look at the official docs. They’re short and to the point.

## 1. Set Up a Sling Instance

### 1.1 Download the Quickstart

```bash
wget https://repo1.maven.org/maven2/org/apache/sling/org.apache.sling.launchpad/11.0.0/org.apache.sling.launchpad-11.0.0.jar -O sling.jar
```

### 1.2 Run It

```bash
java -jar sling.jar -p 8080
```

Sling will start on port 8080 and create a tiny admin console at `http://localhost:8080/system/console`. The first time you hit it, you’ll be prompted to set an admin password. Pick something you can remember – you’ll need it later.

### 1.3 Verify the Installation

Open `http://localhost:8080` in your browser. You should see the default Sling welcome page. If you do, congratulations – the server is alive.

## 2. Create a Simple Content Model

For a headless CMS we need a place to store articles. In Sling, content lives under the `/content` tree.

### 2.1 Add a Folder

Navigate to the JCR (Java Content Repository) browser at `http://localhost:8080/crx/de`. Log in with the admin credentials you just set.

* Right‑click **/content** → **Create → Folder** → name it `site`.  
* Inside `site`, create another folder called `en`. This will be our language node.

### 2.2 Define an Article Node

Right‑click `en` → **Create → Node** → name it `articles`. Set the primary type to `sling:Folder`.

Now create a child node under `articles` called `welcome`. Set its primary type to `nt:unstructured` and add the following properties:

| Property | Type   | Value |
|----------|--------|-------|
| `jcr:title` | String | “Welcome to Sling” |
| `jcr:description` | String | “A quick intro to headless CMS on Sling.” |
| `published` | Date | `2024-01-01T00:00:00.000Z` |

Save the node. Sling automatically makes this content available as JSON at `http://localhost:8080/content/site/en/articles/welcome.json`.

## 3. Expose a Clean API with Sling Servlets

While the raw JSON works, we often want a tidy endpoint like `/api/articles`. A small servlet can do that.

### 3.1 Create a Maven Project

```bash
mvn archetype:generate \
  -DgroupId=com.example.slingcms \
  -DartifactId=sling-cms-api \
  -DarchetypeArtifactId=maven-archetype-quickstart \
  -DinteractiveMode=false
```

### 3.2 Add Sling Dependencies

Edit `pom.xml` and add:

```xml
<dependencies>
  <dependency>
    <groupId>org.apache.sling</groupId>
    <artifactId>org.apache.sling.api</artifactId>
    <version>2.24.0</version>
  </dependency>
  <dependency>
    <groupId>org.apache.sling</groupId>
    <artifactId>org.apache.sling.servlets.annotations</artifactId>
    <version>1.2.6</version>
  </dependency>
</dependencies>
```

### 3.3 Write the Servlet

Create `src/main/java/com/example/slingcms/ArticleServlet.java`:

```java
package com.example.slingcms;

import org.apache.sling.api.SlingHttpServletRequest;
import org.apache.sling.api.SlingHttpServletResponse;
import org.apache.sling.api.resource.Resource;
import org.apache.sling.api.servlets.SlingAllMethodsServlet;
import org.osgi.service.component.annotations.Component;
import org.osgi.service.component.annotations.Reference;
import org.apache.sling.api.resource.ResourceResolver;
import org.apache.sling.api.resource.ResourceResolverFactory;
import org.apache.sling.api.servlets.HttpConstants;
import org.apache.sling.api.servlets.ServletResolverConstants;

import javax.servlet.Servlet;
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.HashMap;
import java.util.Map;

@Component(
    service = Servlet.class,
    property = {
        ServletResolverConstants.SLING_SERVLET_METHODS + "=" + HttpConstants.METHOD_GET,
        ServletResolverConstants.SLING_SERVLET_PATHS + "=" + "/api/articles"
    }
)
public class ArticleServlet extends SlingAllMethodsServlet {

    @Reference
    private ResourceResolverFactory resolverFactory;

    @Override
    protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
        ResourceResolver resolver = null;
        try {
            Map<String, Object> auth = new HashMap<>();
            auth.put(ResourceResolverFactory.SUBSERVICE, "cms-reader");
            resolver = resolverFactory.getServiceResourceResolver(auth);
            Resource root = resolver.getResource("/content/site/en/articles");
            List<Map<String, Object>> articles = new ArrayList<>();

            if (root != null) {
                for (Resource child : root.getChildren()) {
                    Map<String, Object> article = new HashMap<>();
                    article.put("path", child.getPath());
                    article.put("title", child.getValueMap().get("jcr:title", String.class));
                    article.put("description", child.getValueMap().get("jcr:description", String.class));
                    article.put("published", child.getValueMap().get("published", String.class));
                    articles.add(article);
                }
            }

            response.setContentType("application/json");
            response.getWriter().write(JsonUtil.toJson(articles));
        } catch (Exception e) {
            response.sendError(500, "Failed to fetch articles");
        } finally {
            if (resolver != null) {
                resolver.close();
            }
        }
    }
}
```

A quick note: `JsonUtil.toJson` is a tiny helper that uses Jackson. Add Jackson as a dependency if you don’t have it already.

### 3.4 Build and Deploy

```bash
mvn clean package
```

Copy the generated bundle (`target/sling-cms-api-1.0-SNAPSHOT.jar`) into the Sling `install` folder (`http://localhost:8080/system/console/configMgr`). Sling will hot‑deploy the bundle. Verify the servlet works by visiting `http://localhost:8080/api/articles`. You should see a JSON array of your article objects.

## 4. Build the JavaScript Front‑End

I like to keep the front‑end separate, so let’s spin up a tiny Vite project that fetches the API.

### 4.1 Scaffold Vite

```bash
npm create vite@latest sling-frontend -- --template vanilla
cd sling-frontend
npm install
```

### 4.2 Write a Fetch Helper

Create `src/api.js`:

```javascript
export async function getArticles() {
  const resp = await fetch('http://localhost:8080/api/articles');
  if (!resp.ok) {
    throw new Error('Network response was not ok');
  }
  return await resp.json();
}
```

### 4.3 Render Articles

Edit `src/main.js`:

```javascript
import { getArticles } from './api.js';

async function render() {
  const container = document.getElementById('articles');
  try {
    const articles = await getArticles();
    articles.forEach(a => {
      const div = document.createElement('div');
      div.className = 'article';
      div.innerHTML = `
        <h2>${a.title}</h2>
        <p>${a.description}</p>
        <small>Published: ${new Date(a.published).toLocaleDateString()}</small>
      `;
      container.appendChild(div);
    });
  } catch (err) {
    container.textContent = 'Failed to load articles';
    console.error(err);
  }
}

render();
```

Add a placeholder in `index.html`:

```html
<body>
  <h1>My Sling‑Powered Blog</h1>
  <div id="articles">Loading…</div>
  <script type="module" src="/src/main.js"></script>
</body>
```

### 4.4 Run the Front‑End

```bash
npm run dev
```

Open `http://localhost:5173` (or whatever port Vite reports). You should see the article list pulled from Sling. No page reloads, just a clean API call.

## 5. Wrap Up and Next Steps

You now have a minimal headless CMS stack:

* **Sling** serves content as resources and exposes a custom JSON API via a servlet.  
* **Node/JavaScript** fetches that API and renders it in the browser.

From here you can:

* Add authentication (Sling supports OAuth, JWT, or simple basic auth).  
* Introduce a richer content model – images, tags, author profiles.  
* Swap the front‑end framework – React, Svelte, or even a static site generator that pulls data at build time.

What I love about this setup is the clear separation of concerns. Sling handles storage, versioning, and URL management. The JavaScript side stays focused on UI. And because everything is open source, you can peek under the hood whenever you feel like it.

If you run into a snag, the Sling community mailing list is surprisingly friendly, and the Apache docs are a gold mine of examples. Keep experimenting, and soon you’ll have a production‑ready headless CMS that you built from scratch.