Step-by-Step: Deploy a Headless CMS on Apache Sling with a JavaScript Front-End

Read this article in clean Markdown format for LLMs and AI context.

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

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

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 /contentCreate → 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 enCreate → 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:

PropertyTypeValue
jcr:titleString“Welcome to Sling”
jcr:descriptionString“A quick intro to headless CMS on Sling.”
publishedDate2024-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

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:

<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:

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

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

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

4.2 Write a Fetch Helper

Create src/api.js:

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:

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:

<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

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.

Reactions
Do you have any feedback or ideas on how we can improve this page?