Ghost provides a powerful Content API that allows you to retrieve published content programmatically. This guide covers Java integration for fetching posts, pages, authors, and tags from Ghost.
1. Ghost Content API Overview
Key Endpoints
- Posts: /ghost/api/content/posts/ - Pages: /ghost/api/content/pages/ - Authors: /ghost/api/content/authors/ - Tags: /ghost/api/content/tags/ - Settings: /ghost/api/content/settings/
Authentication
- Content API uses API Keys (no admin access)
- Key is passed via
keyquery parameter - Read-only access to published content
2. Project Setup and Dependencies
Maven Dependencies (pom.xml)
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0">
<modelVersion>4.0.0</modelVersion>
<groupId>com.example</groupId>
<artifactId>ghost-content-api</artifactId>
<version>1.0.0</version>
<properties>
<maven.compiler.source>11</maven.compiler.source>
<maven.compiler.target>11</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<jackson.version>2.15.2</jackson.version>
<okhttp.version>4.11.0</okhttp.version>
<spring.boot.version>2.7.14</spring.boot.version>
</properties>
<dependencies>
<!-- HTTP Client -->
<dependency>
<groupId>com.squareup.okhttp3</groupId>
<artifactId>okhttp</artifactId>
<version>${okhttp.version}</version>
</dependency>
<!-- JSON Processing -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>${jackson.version}</version>
</dependency>
<!-- Spring Boot (Optional) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
<version>${spring.boot.version}</version>
</dependency>
<!-- Cache -->
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
<version>3.1.6</version>
</dependency>
<!-- Logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-api</artifactId>
<version>2.0.7</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>${spring.boot.version}</version>
</plugin>
</plugins>
</build>
</project>
3. Configuration Classes
Application Properties (application.yml)
ghost:
api:
url: ${GHOST_API_URL:https://your-ghost-site.com}
content-key: ${GHOST_CONTENT_KEY:}
version: v4
timeout: 30000
cache:
enabled: true
ttl: 300 # 5 minutes
retry:
max-attempts: 3
backoff-delay: 1000
logging:
level:
com.example.ghost: DEBUG
Configuration Properties Class
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Component
@ConfigurationProperties(prefix = "ghost.api")
public class GhostConfig {
private String url;
private String contentKey;
private String version = "v4";
private int timeout = 30000;
private CacheConfig cache = new CacheConfig();
private RetryConfig retry = new RetryConfig();
// Getters and Setters
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public String getContentKey() { return contentKey; }
public void setContentKey(String contentKey) { this.contentKey = contentKey; }
public String getVersion() { return version; }
public void setVersion(String version) { this.version = version; }
public int getTimeout() { return timeout; }
public void setTimeout(int timeout) { this.timeout = timeout; }
public CacheConfig getCache() { return cache; }
public void setCache(CacheConfig cache) { this.cache = cache; }
public RetryConfig getRetry() { return retry; }
public void setRetry(RetryConfig retry) { this.retry = retry; }
public static class CacheConfig {
private boolean enabled = true;
private long ttl = 300; // 5 minutes
public boolean isEnabled() { return enabled; }
public void setEnabled(boolean enabled) { this.enabled = enabled; }
public long getTtl() { return ttl; }
public void setTtl(long ttl) { this.ttl = ttl; }
}
public static class RetryConfig {
private int maxAttempts = 3;
private long backoffDelay = 1000;
public int getMaxAttempts() { return maxAttempts; }
public void setMaxAttempts(int maxAttempts) { this.maxAttempts = maxAttempts; }
public long getBackoffDelay() { return backoffDelay; }
public void setBackoffDelay(long backoffDelay) { this.backoffDelay = backoffDelay; }
}
}
4. Data Transfer Objects (DTOs)
Base Response Wrapper
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
@JsonIgnoreProperties(ignoreUnknown = true)
public class GhostResponse<T> {
private List<T> data;
private Meta meta;
// Getters and Setters
public List<T> getData() { return data; }
public void setData(List<T> data) { this.data = data; }
public Meta getMeta() { return meta; }
public void setMeta(Meta meta) { this.meta = meta; }
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Meta {
private Pagination pagination;
public Pagination getPagination() { return pagination; }
public void setPagination(Pagination pagination) { this.pagination = pagination; }
@JsonIgnoreProperties(ignoreUnknown = true)
public static class Pagination {
private int page;
private int limit;
private int pages;
private int total;
@JsonProperty("next") private Integer nextPage;
@JsonProperty("prev") private Integer prevPage;
public int getPage() { return page; }
public void setPage(int page) { this.page = page; }
public int getLimit() { return limit; }
public void setLimit(int limit) { this.limit = limit; }
public int getPages() { return pages; }
public void setPages(int pages) { this.pages = pages; }
public int getTotal() { return total; }
public void setTotal(int total) { this.total = total; }
public Integer getNextPage() { return nextPage; }
public void setNextPage(Integer nextPage) { this.nextPage = nextPage; }
public Integer getPrevPage() { return prevPage; }
public void setPrevPage(Integer prevPage) { this.prevPage = prevPage; }
}
}
}
Post DTO
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;
@JsonIgnoreProperties(ignoreUnknown = true)
public class GhostPost {
private String id;
private String uuid;
private String title;
private String slug;
private String html;
private String plaintext;
private String commentId;
private String featureImage;
private boolean featured;
private String visibility;
private String status;
@JsonProperty("created_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime createdAt;
@JsonProperty("updated_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime updatedAt;
@JsonProperty("published_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime publishedAt;
@JsonProperty("custom_excerpt")
private String customExcerpt;
@JsonProperty("codeinjection_head")
private String codeInjectionHead;
@JsonProperty("codeinjection_foot")
private String codeInjectionFoot;
@JsonProperty("og_image")
private String ogImage;
@JsonProperty("og_title")
private String ogTitle;
@JsonProperty("og_description")
private String ogDescription;
@JsonProperty("twitter_image")
private String twitterImage;
@JsonProperty("twitter_title")
private String twitterTitle;
@JsonProperty("twitter_description")
private String twitterDescription;
@JsonProperty("meta_title")
private String metaTitle;
@JsonProperty("meta_description")
private String metaDescription;
@JsonProperty("reading_time")
private Integer readingTime;
private List<GhostAuthor> authors;
private List<GhostTag> tags;
private GhostAuthor primaryAuthor;
private GhostTag primaryTag;
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getUuid() { return uuid; }
public void setUuid(String uuid) { this.uuid = uuid; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getSlug() { return slug; }
public void setSlug(String slug) { this.slug = slug; }
public String getHtml() { return html; }
public void setHtml(String html) { this.html = html; }
public String getPlaintext() { return plaintext; }
public void setPlaintext(String plaintext) { this.plaintext = plaintext; }
public String getCommentId() { return commentId; }
public void setCommentId(String commentId) { this.commentId = commentId; }
public String getFeatureImage() { return featureImage; }
public void setFeatureImage(String featureImage) { this.featureImage = featureImage; }
public boolean isFeatured() { return featured; }
public void setFeatured(boolean featured) { this.featured = featured; }
public String getVisibility() { return visibility; }
public void setVisibility(String visibility) { this.visibility = visibility; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getPublishedAt() { return publishedAt; }
public void setPublishedAt(LocalDateTime publishedAt) { this.publishedAt = publishedAt; }
public String getCustomExcerpt() { return customExcerpt; }
public void setCustomExcerpt(String customExcerpt) { this.customExcerpt = customExcerpt; }
public String getCodeInjectionHead() { return codeInjectionHead; }
public void setCodeInjectionHead(String codeInjectionHead) { this.codeInjectionHead = codeInjectionHead; }
public String getCodeInjectionFoot() { return codeInjectionFoot; }
public void setCodeInjectionFoot(String codeInjectionFoot) { this.codeInjectionFoot = codeInjectionFoot; }
public String getOgImage() { return ogImage; }
public void setOgImage(String ogImage) { this.ogImage = ogImage; }
public String getOgTitle() { return ogTitle; }
public void setOgTitle(String ogTitle) { this.ogTitle = ogTitle; }
public String getOgDescription() { return ogDescription; }
public void setOgDescription(String ogDescription) { this.ogDescription = ogDescription; }
public String getTwitterImage() { return twitterImage; }
public void setTwitterImage(String twitterImage) { this.twitterImage = twitterImage; }
public String getTwitterTitle() { return twitterTitle; }
public void setTwitterTitle(String twitterTitle) { this.twitterTitle = twitterTitle; }
public String getTwitterDescription() { return twitterDescription; }
public void setTwitterDescription(String twitterDescription) { this.twitterDescription = twitterDescription; }
public String getMetaTitle() { return metaTitle; }
public void setMetaTitle(String metaTitle) { this.metaTitle = metaTitle; }
public String getMetaDescription() { return metaDescription; }
public void setMetaDescription(String metaDescription) { this.metaDescription = metaDescription; }
public Integer getReadingTime() { return readingTime; }
public void setReadingTime(Integer readingTime) { this.readingTime = readingTime; }
public List<GhostAuthor> getAuthors() { return authors; }
public void setAuthors(List<GhostAuthor> authors) { this.authors = authors; }
public List<GhostTag> getTags() { return tags; }
public void setTags(List<GhostTag> tags) { this.tags = tags; }
public GhostAuthor getPrimaryAuthor() { return primaryAuthor; }
public void setPrimaryAuthor(GhostAuthor primaryAuthor) { this.primaryAuthor = primaryAuthor; }
public GhostTag getPrimaryTag() { return primaryTag; }
public void setPrimaryTag(GhostTag primaryTag) { this.primaryTag = primaryTag; }
}
Author DTO
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDateTime;
@JsonIgnoreProperties(ignoreUnknown = true)
public class GhostAuthor {
private String id;
private String name;
private String slug;
private String email;
private String profileImage;
private String coverImage;
private String bio;
private String website;
private String location;
private String facebook;
private String twitter;
private String metaTitle;
private String metaDescription;
private String url;
@JsonProperty("created_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime createdAt;
@JsonProperty("updated_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime updatedAt;
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getSlug() { return slug; }
public void setSlug(String slug) { this.slug = slug; }
public String getEmail() { return email; }
public void setEmail(String email) { this.email = email; }
public String getProfileImage() { return profileImage; }
public void setProfileImage(String profileImage) { this.profileImage = profileImage; }
public String getCoverImage() { return coverImage; }
public void setCoverImage(String coverImage) { this.coverImage = coverImage; }
public String getBio() { return bio; }
public void setBio(String bio) { this.bio = bio; }
public String getWebsite() { return website; }
public void setWebsite(String website) { this.website = website; }
public String getLocation() { return location; }
public void setLocation(String location) { this.location = location; }
public String getFacebook() { return facebook; }
public void setFacebook(String facebook) { this.facebook = facebook; }
public String getTwitter() { return twitter; }
public void setTwitter(String twitter) { this.twitter = twitter; }
public String getMetaTitle() { return metaTitle; }
public void setMetaTitle(String metaTitle) { this.metaTitle = metaTitle; }
public String getMetaDescription() { return metaDescription; }
public void setMetaDescription(String metaDescription) { this.metaDescription = metaDescription; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
Tag DTO
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDateTime;
@JsonIgnoreProperties(ignoreUnknown = true)
public class GhostTag {
private String id;
private String name;
private String slug;
private String description;
private String featureImage;
private String visibility;
private String metaTitle;
private String metaDescription;
private String url;
@JsonProperty("created_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime createdAt;
@JsonProperty("updated_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime updatedAt;
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getName() { return name; }
public void setName(String name) { this.name = name; }
public String getSlug() { return slug; }
public void setSlug(String slug) { this.slug = slug; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getFeatureImage() { return featureImage; }
public void setFeatureImage(String featureImage) { this.featureImage = featureImage; }
public String getVisibility() { return visibility; }
public void setVisibility(String visibility) { this.visibility = visibility; }
public String getMetaTitle() { return metaTitle; }
public void setMetaTitle(String metaTitle) { this.metaTitle = metaTitle; }
public String getMetaDescription() { return metaDescription; }
public void setMetaDescription(String metaDescription) { this.metaDescription = metaDescription; }
public String getUrl() { return url; }
public void setUrl(String url) { this.url = url; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
}
Page DTO
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.time.LocalDateTime;
@JsonIgnoreProperties(ignoreUnknown = true)
public class GhostPage {
private String id;
private String uuid;
private String title;
private String slug;
private String html;
private String plaintext;
private String featureImage;
private boolean featured;
private String visibility;
private String status;
@JsonProperty("created_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime createdAt;
@JsonProperty("updated_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime updatedAt;
@JsonProperty("published_at")
@JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'")
private LocalDateTime publishedAt;
// Getters and Setters
public String getId() { return id; }
public void setId(String id) { this.id = id; }
public String getUuid() { return uuid; }
public void setUuid(String uuid) { this.uuid = uuid; }
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getSlug() { return slug; }
public void setSlug(String slug) { this.slug = slug; }
public String getHtml() { return html; }
public void setHtml(String html) { this.html = html; }
public String getPlaintext() { return plaintext; }
public void setPlaintext(String plaintext) { this.plaintext = plaintext; }
public String getFeatureImage() { return featureImage; }
public void setFeatureImage(String featureImage) { this.featureImage = featureImage; }
public boolean isFeatured() { return featured; }
public void setFeatured(boolean featured) { this.featured = featured; }
public String getVisibility() { return visibility; }
public void setVisibility(String visibility) { this.visibility = visibility; }
public String getStatus() { return status; }
public void setStatus(String status) { this.status = status; }
public LocalDateTime getCreatedAt() { return createdAt; }
public void setCreatedAt(LocalDateTime createdAt) { this.createdAt = createdAt; }
public LocalDateTime getUpdatedAt() { return updatedAt; }
public void setUpdatedAt(LocalDateTime updatedAt) { this.updatedAt = updatedAt; }
public LocalDateTime getPublishedAt() { return publishedAt; }
public void setPublishedAt(LocalDateTime publishedAt) { this.publishedAt = publishedAt; }
}
5. Ghost Content API Client
Main API Client Service
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
@Service
public class GhostContentApiClient {
private static final Logger logger = LoggerFactory.getLogger(GhostContentApiClient.class);
private final GhostConfig config;
private final OkHttpClient httpClient;
private final ObjectMapper objectMapper;
@Autowired
public GhostContentApiClient(GhostConfig config, ObjectMapper objectMapper) {
this.config = config;
this.objectMapper = objectMapper;
this.httpClient = new OkHttpClient.Builder()
.connectTimeout(config.getTimeout(), TimeUnit.MILLISECONDS)
.readTimeout(config.getTimeout(), TimeUnit.MILLISECONDS)
.writeTimeout(config.getTimeout(), TimeUnit.MILLISECONDS)
.addInterceptor(new RetryInterceptor(config.getRetry().getMaxAttempts(),
config.getRetry().getBackoffDelay()))
.build();
}
// Posts Operations
@Cacheable(value = "ghostPosts", unless = "#result == null || #result.data.isEmpty()")
public GhostResponse<GhostPost> getPosts() throws GhostApiException {
return getPosts(Collections.emptyMap());
}
@Cacheable(value = "ghostPosts", key = "{#page, #limit, #include}", unless = "#result == null || #result.data.isEmpty()")
public GhostResponse<GhostPost> getPosts(int page, int limit, String include) throws GhostApiException {
Map<String, String> params = new HashMap<>();
params.put("page", String.valueOf(page));
params.put("limit", String.valueOf(limit));
if (include != null) {
params.put("include", include);
}
return getPosts(params);
}
@Cacheable(value = "ghostPostsByTag", key = "#tagSlug", unless = "#result == null || #result.data.isEmpty()")
public GhostResponse<GhostPost> getPostsByTag(String tagSlug) throws GhostApiException {
Map<String, String> params = new HashMap<>();
params.put("filter", "tag:" + tagSlug);
params.put("include", "authors,tags");
return getPosts(params);
}
@Cacheable(value = "ghostPostsByAuthor", key = "#authorSlug", unless = "#result == null || #result.data.isEmpty()")
public GhostResponse<GhostPost> getPostsByAuthor(String authorSlug) throws GhostApiException {
Map<String, String> params = new HashMap<>();
params.put("filter", "author:" + authorSlug);
params.put("include", "authors,tags");
return getPosts(params);
}
@Cacheable(value = "ghostFeaturedPosts", unless = "#result == null || #result.data.isEmpty()")
public GhostResponse<GhostPost> getFeaturedPosts() throws GhostApiException {
Map<String, String> params = new HashMap<>();
params.put("filter", "featured:true");
params.put("include", "authors,tags");
return getPosts(params);
}
private GhostResponse<GhostPost> getPosts(Map<String, String> queryParams) throws GhostApiException {
return makeRequest("/posts/", queryParams, new TypeReference<GhostResponse<GhostPost>>() {});
}
@Cacheable(value = "ghostPost", key = "#slug", unless = "#result == null")
public GhostPost getPostBySlug(String slug) throws GhostApiException {
Map<String, String> params = new HashMap<>();
params.put("filter", "slug:" + slug);
params.put("include", "authors,tags");
GhostResponse<GhostPost> response = getPosts(params);
if (response.getData() != null && !response.getData().isEmpty()) {
return response.getData().get(0);
}
return null;
}
@Cacheable(value = "ghostPost", key = "#id", unless = "#result == null")
public GhostPost getPostById(String id) throws GhostApiException {
try {
HttpUrl url = buildUrl("/posts/" + id + "/", Collections.emptyMap());
Request request = new Request.Builder().url(url).get().build();
return executeRequest(request, new TypeReference<GhostResponse<GhostPost>>() {})
.getData()
.get(0);
} catch (GhostApiException e) {
logger.warn("Failed to get post by ID: {}", id, e);
return null;
}
}
// Authors Operations
@Cacheable(value = "ghostAuthors", unless = "#result == null || #result.data.isEmpty()")
public GhostResponse<GhostAuthor> getAuthors() throws GhostApiException {
return getAuthors(Collections.emptyMap());
}
@Cacheable(value = "ghostAuthor", key = "#slug", unless = "#result == null")
public GhostAuthor getAuthorBySlug(String slug) throws GhostApiException {
Map<String, String> params = new HashMap<>();
params.put("filter", "slug:" + slug);
GhostResponse<GhostAuthor> response = getAuthors(params);
if (response.getData() != null && !response.getData().isEmpty()) {
return response.getData().get(0);
}
return null;
}
private GhostResponse<GhostAuthor> getAuthors(Map<String, String> queryParams) throws GhostApiException {
return makeRequest("/authors/", queryParams, new TypeReference<GhostResponse<GhostAuthor>>() {});
}
// Tags Operations
@Cacheable(value = "ghostTags", unless = "#result == null || #result.data.isEmpty()")
public GhostResponse<GhostTag> getTags() throws GhostApiException {
return getTags(Collections.emptyMap());
}
@Cacheable(value = "ghostTag", key = "#slug", unless = "#result == null")
public GhostTag getTagBySlug(String slug) throws GhostApiException {
Map<String, String> params = new HashMap<>();
params.put("filter", "slug:" + slug);
GhostResponse<GhostTag> response = getTags(params);
if (response.getData() != null && !response.getData().isEmpty()) {
return response.getData().get(0);
}
return null;
}
private GhostResponse<GhostTag> getTags(Map<String, String> queryParams) throws GhostApiException {
return makeRequest("/tags/", queryParams, new TypeReference<GhostResponse<GhostTag>>() {});
}
// Pages Operations
@Cacheable(value = "ghostPages", unless = "#result == null || #result.data.isEmpty()")
public GhostResponse<GhostPage> getPages() throws GhostApiException {
return getPages(Collections.emptyMap());
}
@Cacheable(value = "ghostPage", key = "#slug", unless = "#result == null")
public GhostPage getPageBySlug(String slug) throws GhostApiException {
Map<String, String> params = new HashMap<>();
params.put("filter", "slug:" + slug);
GhostResponse<GhostPage> response = getPages(params);
if (response.getData() != null && !response.getData().isEmpty()) {
return response.getData().get(0);
}
return null;
}
private GhostResponse<GhostPage> getPages(Map<String, String> queryParams) throws GhostApiException {
return makeRequest("/pages/", queryParams, new TypeReference<GhostResponse<GhostPage>>() {});
}
// Settings
@Cacheable(value = "ghostSettings", unless = "#result == null")
public GhostSettings getSettings() throws GhostApiException {
return makeRequest("/settings/", Collections.emptyMap(), new TypeReference<GhostResponse<GhostSettings>>() {})
.getData()
.get(0);
}
// Generic Request Method
private <T> GhostResponse<T> makeRequest(String endpoint, Map<String, String> queryParams,
TypeReference<GhostResponse<T>> typeRef) throws GhostApiException {
try {
HttpUrl url = buildUrl(endpoint, queryParams);
Request request = new Request.Builder().url(url).get().build();
return executeRequest(request, typeRef);
} catch (IOException e) {
throw new GhostApiException("Error making request to " + endpoint, e);
}
}
private HttpUrl buildUrl(String endpoint, Map<String, String> queryParams) {
HttpUrl.Builder urlBuilder = HttpUrl.parse(config.getUrl() + "/ghost/api/content/" +
config.getVersion() + endpoint).newBuilder();
// Add API key
urlBuilder.addQueryParameter("key", config.getContentKey());
// Add other query parameters
queryParams.forEach(urlBuilder::addQueryParameter);
return urlBuilder.build();
}
private <T> T executeRequest(Request request, TypeReference<T> typeRef) throws IOException, GhostApiException {
try (Response response = httpClient.newCall(request).execute()) {
if (!response.isSuccessful()) {
throw new GhostApiException("HTTP " + response.code() + " - " + response.message());
}
String responseBody = response.body().string();
return objectMapper.readValue(responseBody, typeRef);
}
}
}
Retry Interceptor
import okhttp3.*;
import org.jetbrains.annotations.NotNull;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import java.io.IOException;
import java.util.concurrent.TimeUnit;
public class RetryInterceptor implements Interceptor {
private static final Logger logger = LoggerFactory.getLogger(RetryInterceptor.class);
private final int maxRetries;
private final long backoffDelay;
public RetryInterceptor(int maxRetries, long backoffDelay) {
this.maxRetries = maxRetries;
this.backoffDelay = backoffDelay;
}
@NotNull
@Override
public Response intercept(@NotNull Chain chain) throws IOException {
Request request = chain.request();
Response response = null;
IOException exception = null;
for (int attempt = 1; attempt <= maxRetries; attempt++) {
try {
response = chain.proceed(request);
if (response.isSuccessful()) {
return response;
}
// Only retry on server errors (5xx) or rate limiting
if (response.code() < 500 && response.code() != 429) {
return response;
}
logger.warn("Ghost API request failed with status {} on attempt {}/{}",
response.code(), attempt, maxRetries);
} catch (IOException e) {
exception = e;
logger.warn("Ghost API request failed with IOException on attempt {}/{}: {}",
attempt, maxRetries, e.getMessage());
}
if (attempt < maxRetries) {
try {
long delay = backoffDelay * (long) Math.pow(2, attempt - 1);
TimeUnit.MILLISECONDS.sleep(delay);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new IOException("Request interrupted", e);
}
}
if (response != null) {
response.close();
}
}
if (exception != null) {
throw exception;
}
return response;
}
}
Settings DTO
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import java.util.List;
import java.util.Map;
@JsonIgnoreProperties(ignoreUnknown = true)
public class GhostSettings {
private String title;
private String description;
private String logo;
private String icon;
private String coverImage;
private String facebook;
private String twitter;
private String language;
private String timezone;
private String codeinjectionHead;
private String codeinjectionFoot;
private String navigation;
private Map<String, String> meta;
private List<Map<String, String>> members;
@JsonProperty("secondary_navigation")
private List<Map<String, String>> secondaryNavigation;
@JsonProperty("og_image")
private String ogImage;
@JsonProperty("og_title")
private String ogTitle;
@JsonProperty("og_description")
private String ogDescription;
@JsonProperty("twitter_image")
private String twitterImage;
@JsonProperty("twitter_title")
private String twitterTitle;
@JsonProperty("twitter_description")
private String twitterDescription;
@JsonProperty("meta_title")
private String metaTitle;
@JsonProperty("meta_description")
private String metaDescription;
@JsonProperty("url")
private String siteUrl;
// Getters and Setters
public String getTitle() { return title; }
public void setTitle(String title) { this.title = title; }
public String getDescription() { return description; }
public void setDescription(String description) { this.description = description; }
public String getLogo() { return logo; }
public void setLogo(String logo) { this.logo = logo; }
public String getIcon() { return icon; }
public void setIcon(String icon) { this.icon = icon; }
public String getCoverImage() { return coverImage; }
public void setCoverImage(String coverImage) { this.coverImage = coverImage; }
public String getFacebook() { return facebook; }
public void setFacebook(String facebook) { this.facebook = facebook; }
public String getTwitter() { return twitter; }
public void setTwitter(String twitter) { this.twitter = twitter; }
public String getLanguage() { return language; }
public void setLanguage(String language) { this.language = language; }
public String getTimezone() { return timezone; }
public void setTimezone(String timezone) { this.timezone = timezone; }
public String getCodeinjectionHead() { return codeinjectionHead; }
public void setCodeinjectionHead(String codeinjectionHead) { this.codeinjectionHead = codeinjectionHead; }
public String getCodeinjectionFoot() { return codeinjectionFoot; }
public void setCodeinjectionFoot(String codeinjectionFoot) { this.codeinjectionFoot = codeinjectionFoot; }
public String getNavigation() { return navigation; }
public void setNavigation(String navigation) { this.navigation = navigation; }
public Map<String, String> getMeta() { return meta; }
public void setMeta(Map<String, String> meta) { this.meta = meta; }
public List<Map<String, String>> getMembers() { return members; }
public void setMembers(List<Map<String, String>> members) { this.members = members; }
public List<Map<String, String>> getSecondaryNavigation() { return secondaryNavigation; }
public void setSecondaryNavigation(List<Map<String, String>> secondaryNavigation) { this.secondaryNavigation = secondaryNavigation; }
public String getOgImage() { return ogImage; }
public void setOgImage(String ogImage) { this.ogImage = ogImage; }
public String getOgTitle() { return ogTitle; }
public void setOgTitle(String ogTitle) { this.ogTitle = ogTitle; }
public String getOgDescription() { return ogDescription; }
public void setOgDescription(String ogDescription) { this.ogDescription = ogDescription; }
public String getTwitterImage() { return twitterImage; }
public void setTwitterImage(String twitterImage) { this.twitterImage = twitterImage; }
public String getTwitterTitle() { return twitterTitle; }
public void setTwitterTitle(String twitterTitle) { this.twitterTitle = twitterTitle; }
public String getTwitterDescription() { return twitterDescription; }
public void setTwitterDescription(String twitterDescription) { this.twitterDescription = twitterDescription; }
public String getMetaTitle() { return metaTitle; }
public void setMetaTitle(String metaTitle) { this.metaTitle = metaTitle; }
public String getMetaDescription() { return metaDescription; }
public void setMetaDescription(String metaDescription) { this.metaDescription = metaDescription; }
public String getSiteUrl() { return siteUrl; }
public void setSiteUrl(String siteUrl) { this.siteUrl = siteUrl; }
}
6. Cache Configuration
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfig {
@Bean
public CacheManager cacheManager(GhostConfig ghostConfig) {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
if (ghostConfig.getCache().isEnabled()) {
Caffeine<Object, Object> caffeine = Caffeine.newBuilder()
.expireAfterWrite(ghostConfig.getCache().getTtl(), TimeUnit.SECONDS)
.maximumSize(1000)
.recordStats();
cacheManager.setCaffeine(caffeine);
} else {
// Disable caching
cacheManager.setCacheNames(java.util.Collections.emptyList());
}
return cacheManager;
}
}
7. Spring Boot Controller
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.util.List;
@RestController
@RequestMapping("/api/ghost")
public class GhostContentController {
@Autowired
private GhostContentApiClient ghostClient;
@GetMapping("/posts")
public ResponseEntity<GhostResponse<GhostPost>> getPosts(
@RequestParam(defaultValue = "1") int page,
@RequestParam(defaultValue = "10") int limit,
@RequestParam(required = false) String include) {
try {
GhostResponse<GhostPost> response = ghostClient.getPosts(page, limit, include);
return ResponseEntity.ok(response);
} catch (GhostApiException e) {
return ResponseEntity.status(500).body(null);
}
}
@GetMapping("/posts/featured")
public ResponseEntity<GhostResponse<GhostPost>> getFeaturedPosts() {
try {
GhostResponse<GhostPost> response = ghostClient.getFeaturedPosts();
return ResponseEntity.ok(response);
} catch (GhostApiException e) {
return ResponseEntity.status(500).body(null);
}
}
@GetMapping("/posts/slug/{slug}")
public ResponseEntity<GhostPost> getPostBySlug(@PathVariable String slug) {
try {
GhostPost post = ghostClient.getPostBySlug(slug);
if (post != null) {
return ResponseEntity.ok(post);
}
return ResponseEntity.notFound().build();
} catch (GhostApiException e) {
return ResponseEntity.status(500).body(null);
}
}
@GetMapping("/posts/tag/{tagSlug}")
public ResponseEntity<GhostResponse<GhostPost>> getPostsByTag(@PathVariable String tagSlug) {
try {
GhostResponse<GhostPost> response = ghostClient.getPostsByTag(tagSlug);
return ResponseEntity.ok(response);
} catch (GhostApiException e) {
return ResponseEntity.status(500).body(null);
}
}
@GetMapping("/posts/author/{authorSlug}")
public ResponseEntity<GhostResponse<GhostPost>> getPostsByAuthor(@PathVariable String authorSlug) {
try {
GhostResponse<GhostPost> response = ghostClient.getPostsByAuthor(authorSlug);
return ResponseEntity.ok(response);
} catch (GhostApiException e) {
return ResponseEntity.status(500).body(null);
}
}
@GetMapping("/authors")
public ResponseEntity<GhostResponse<GhostAuthor>> getAuthors() {
try {
GhostResponse<GhostAuthor> response = ghostClient.getAuthors();
return ResponseEntity.ok(response);
} catch (GhostApiException e) {
return ResponseEntity.status(500).body(null);
}
}
@GetMapping("/authors/slug/{slug}")
public ResponseEntity<GhostAuthor> getAuthorBySlug(@PathVariable String slug) {
try {
GhostAuthor author = ghostClient.getAuthorBySlug(slug);
if (author != null) {
return ResponseEntity.ok(author);
}
return ResponseEntity.notFound().build();
} catch (GhostApiException e) {
return ResponseEntity.status(500).body(null);
}
}
@GetMapping("/tags")
public ResponseEntity<GhostResponse<GhostTag>> getTags() {
try {
GhostResponse<GhostTag> response = ghostClient.getTags();
return ResponseEntity.ok(response);
} catch (GhostApiException e) {
return ResponseEntity.status(500).body(null);
}
}
@GetMapping("/pages/slug/{slug}")
public ResponseEntity<GhostPage> getPageBySlug(@PathVariable String slug) {
try {
GhostPage page = ghostClient.getPageBySlug(slug);
if (page != null) {
return ResponseEntity.ok(page);
}
return ResponseEntity.notFound().build();
} catch (GhostApiException e) {
return ResponseEntity.status(500).body(null);
}
}
@GetMapping("/settings")
public ResponseEntity<GhostSettings> getSettings() {
try {
GhostSettings settings = ghostClient.getSettings();
return ResponseEntity.ok(settings);
} catch (GhostApiException e) {
return ResponseEntity.status(500).body(null);
}
}
}
8. Custom Exception
public class GhostApiException extends Exception {
public GhostApiException(String message) {
super(message);
}
public GhostApiException(String message, Throwable cause) {
super(message, cause);
}
}
9. Usage Examples
Spring Boot Application
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
@SpringBootApplication
@EnableConfigurationProperties(GhostConfig.class)
public class GhostContentApiApplication {
public static void main(String[] args) {
SpringApplication.run(GhostContentApiApplication.class, args);
}
}
Example Service Usage
@Service
public class BlogService {
@Autowired
private GhostContentApiClient ghostClient;
public void demonstrateUsage() {
try {
// Get recent posts with authors and tags
GhostResponse<GhostPost> postsResponse = ghostClient.getPosts(1, 10, "authors,tags");
List<GhostPost> posts = postsResponse.getData();
// Get a specific post by slug
GhostPost post = ghostClient.getPostBySlug("welcome-to-ghost");
if (post != null) {
System.out.println("Post Title: " + post.getTitle());
System.out.println("Author: " + post.getPrimaryAuthor().getName());
}
// Get posts by tag
GhostResponse<GhostPost> techPosts = ghostClient.getPostsByTag("technology");
// Get all authors
GhostResponse<GhostAuthor> authorsResponse = ghostClient.getAuthors();
List<GhostAuthor> authors = authorsResponse.getData();
// Get site settings
GhostSettings settings = ghostClient.getSettings();
System.out.println("Site Title: " + settings.getTitle());
System.out.println("Site Description: " + settings.getDescription());
} catch (GhostApiException e) {
e.printStackTrace();
}
}
}
This comprehensive Java client for Ghost Content API provides complete functionality for retrieving posts, pages, authors, tags, and settings from a Ghost publication, with proper caching, error handling, and retry mechanisms.