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.jsonjust works. - Built‑in content negotiation – ask for
.json,.html, or.xmland 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 /content → Create → Folder → name it
site. - Inside
site, create another folder calleden. 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
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.
- → The Ultimate JavaScript Debugging Cheat Sheet: 25 Shortcuts Every Developer Needs @codecheatsheet
- → How to Build Your First Interactive To-Do List with Vanilla JavaScript @jsbeginnerhub
- → Seamless Keyboard‑Friendly Anchor Navigation with CSS and a Little JavaScript @toggleanchors
- → The Ultimate One-Page Cheat Sheet for Debugging JavaScript Errors Fast @codecheatsheet
- → How to Choose Between React, Vue, and Svelte for Your Next Project: A Performance-First Guide @jsframeworkshowdown