Full-Featured Blogging Platform with Rich Content Management
A comprehensive Blog API built with GraphQL that provides full content management capabilities including posts, categories, tags, comments, and user management. Features rich text editing, image uploads, SEO optimization, and advanced filtering.
Web, Mobile, Admin Panel
Apollo Server
Resolvers & Services
JWT & Permissions
Image Processing
Main Database
Cache Layer
Media Storage
Comprehensive blogging platform with modern content management capabilities.
Create, edit, and publish posts with rich text content, featured images, excerpts, and scheduling. Support for drafts, published, and archived states.
Organize content with hierarchical categories and flexible tagging system. Create custom taxonomies for better content organization.
Nested comment threads with moderation capabilities. Support for likes, spam filtering, and email notifications for comment replies.
Multi-role system with Admin, Editor, Author, and Reader roles. Profile management, avatar uploads, and activity tracking.
Full-text search with filters for categories, tags, authors, and date ranges. Relevance scoring and search suggestions.
Track post views, engagement metrics, popular content, and user activity. Integration with Google Analytics.
Upload and manage images with automatic thumbnail generation, compression, and CDN integration for fast delivery.
Automatic meta tags, Open Graph, Twitter Cards, XML sitemap generation, and structured data for better search rankings.
Headless CMS approach allows building websites, mobile apps, and custom frontends using the same GraphQL API.
Complete type definitions for the blog API including posts, users, comments, and media.
type Post {
id: ID!
title: String!
slug: String!
content: String!
excerpt: String
featuredImage: Image
status: PostStatus!
author: User!
category: Category
tags: [Tag!]!
comments: [Comment!]!
commentCount: Int!
likes: Int!
views: Int!
readingTime: Int!
publishedAt: DateTime
createdAt: DateTime!
updatedAt: DateTime!
seo: SEOMetadata
}
enum PostStatus {
DRAFT
PUBLISHED
ARCHIVED
SCHEDULED
}
type Category {
id: ID!
name: String!
slug: String!
description: String
parent: Category
children: [Category!]!
posts: [Post!]!
postCount: Int!
image: Image
createdAt: DateTime!
}
type Tag {
id: ID!
name: String!
slug: String!
posts: [Post!]!
postCount: Int!
createdAt: DateTime!
}
type Comment {
id: ID!
content: String!
author: User!
post: Post!
parent: Comment
replies: [Comment!]!
status: CommentStatus!
likes: Int!
createdAt: DateTime!
updatedAt: DateTime
}
enum CommentStatus {
PENDING
APPROVED
SPAM
TRASH
}
type User {
id: ID!
email: String!
username: String!
firstName: String
lastName: String
fullName: String!
bio: String
avatar: Image
role: UserRole!
posts: [Post!]!
comments: [Comment!]!
followers: [User!]!
following: [User!]!
followerCount: Int!
followingCount: Int!
website: String
socialLinks: SocialLinks
createdAt: DateTime!
}
enum UserRole {
ADMIN
EDITOR
AUTHOR
READER
}
type SocialLinks {
twitter: String
facebook: String
instagram: String
linkedin: String
github: String
}
type Image {
id: ID!
url: String!
thumbnail: String
medium: String
large: String
width: Int!
height: Int!
size: Int!
mimeType: String!
alt: String
caption: String
uploadedBy: User!
uploadedAt: DateTime!
}
type SEOMetadata {
metaTitle: String
metaDescription: String
keywords: [String!]
ogImage: String
canonicalUrl: String
noIndex: Boolean!
}
scalar DateTime
scalar JSON
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
type CommentConnection {
edges: [CommentEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type CommentEdge {
node: Comment!
cursor: String!
}
type SearchResult {
posts: [Post!]!
users: [User!]!
tags: [Tag!]!
totalResults: Int!
took: Int!
}
type Analytics {
totalPosts: Int!
totalViews: Int!
totalComments: Int!
totalUsers: Int!
popularPosts: [Post!]!
recentActivity: [Activity!]!
viewsByDate: [DateMetric!]!
}
type Activity {
id: ID!
type: ActivityType!
user: User!
post: Post
comment: Comment
description: String!
createdAt: DateTime!
}
enum ActivityType {
POST_CREATED
POST_UPDATED
POST_PUBLISHED
COMMENT_ADDED
USER_REGISTERED
POST_LIKED
}
type DateMetric {
date: String!
count: Int!
}
Comprehensive API operations for content management and user interactions.
type Query {
# Post queries
post(id: ID, slug: String): Post
posts(
filter: PostFilter
sort: PostSort
first: Int
after: String
last: Int
before: String
): PostConnection!
# Search
search(
query: String!
type: SearchType
limit: Int
): SearchResult!
# Category queries
category(id: ID, slug: String): Category
categories(parentId: ID): [Category!]!
# Tag queries
tag(id: ID, slug: String): Tag
tags(limit: Int): [Tag!]!
popularTags(limit: Int): [Tag!]!
# Comment queries
comment(id: ID!): Comment
comments(
postId: ID!
status: CommentStatus
first: Int
after: String
): CommentConnection!
# User queries
user(id: ID, username: String): User
users(role: UserRole, limit: Int, offset: Int): [User!]!
me: User
# Analytics
analytics(startDate: DateTime, endDate: DateTime): Analytics!
# Media
image(id: ID!): Image
images(limit: Int, offset: Int): [Image!]!
}
input PostFilter {
status: PostStatus
categoryId: ID
tagIds: [ID!]
authorId: ID
search: String
dateRange: DateRangeInput
}
enum PostSort {
CREATED_DESC
CREATED_ASC
PUBLISHED_DESC
PUBLISHED_ASC
VIEWS_DESC
LIKES_DESC
TITLE_ASC
TITLE_DESC
}
enum SearchType {
ALL
POSTS
USERS
TAGS
}
input DateRangeInput {
start: DateTime
end: DateTime
}
type Mutation {
# Authentication
register(input: RegisterInput!): AuthPayload!
login(email: String!, password: String!): AuthPayload!
logout: Boolean!
refreshToken: AuthPayload!
# Post mutations
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
publishPost(id: ID!): Post!
schedulePost(id: ID!, publishAt: DateTime!): Post!
likePost(id: ID!): Post!
unlikePost(id: ID!): Post!
incrementPostViews(id: ID!): Post!
# Category mutations
createCategory(input: CreateCategoryInput!): Category!
updateCategory(id: ID!, input: UpdateCategoryInput!): Category!
deleteCategory(id: ID!): Boolean!
# Tag mutations
createTag(input: CreateTagInput!): Tag!
updateTag(id: ID!, input: UpdateTagInput!): Tag!
deleteTag(id: ID!): Boolean!
# Comment mutations
createComment(input: CreateCommentInput!): Comment!
updateComment(id: ID!, input: UpdateCommentInput!): Comment!
deleteComment(id: ID!): Boolean!
approveComment(id: ID!): Comment!
rejectComment(id: ID!): Comment!
markAsSpam(id: ID!): Comment!
likeComment(id: ID!): Comment!
# User mutations
updateProfile(input: UpdateProfileInput!): User!
changePassword(oldPassword: String!, newPassword: String!): Boolean!
followUser(userId: ID!): User!
unfollowUser(userId: ID!): User!
updateUserRole(userId: ID!, role: UserRole!): User!
# Media mutations
uploadImage(file: Upload!, alt: String, caption: String): Image!
updateImage(id: ID!, input: UpdateImageInput!): Image!
deleteImage(id: ID!): Boolean!
}
input RegisterInput {
email: String!
password: String!
username: String!
firstName: String
lastName: String
}
input CreatePostInput {
title: String!
content: String!
excerpt: String
featuredImageId: ID
categoryId: ID
tagIds: [ID!]
status: PostStatus
seo: SEOInput
}
input UpdatePostInput {
title: String
content: String
excerpt: String
featuredImageId: ID
categoryId: ID
tagIds: [ID!]
status: PostStatus
seo: SEOInput
}
input SEOInput {
metaTitle: String
metaDescription: String
keywords: [String!]
ogImage: String
canonicalUrl: String
noIndex: Boolean
}
input CreateCategoryInput {
name: String!
slug: String
description: String
parentId: ID
imageId: ID
}
input UpdateCategoryInput {
name: String
slug: String
description: String
parentId: ID
imageId: ID
}
input CreateTagInput {
name: String!
slug: String
}
input UpdateTagInput {
name: String
slug: String
}
input CreateCommentInput {
postId: ID!
content: String!
parentId: ID
}
input UpdateCommentInput {
content: String
}
input UpdateProfileInput {
username: String
firstName: String
lastName: String
bio: String
avatarId: ID
website: String
socialLinks: SocialLinksInput
}
input SocialLinksInput {
twitter: String
facebook: String
instagram: String
linkedin: String
github: String
}
input UpdateImageInput {
alt: String
caption: String
}
type AuthPayload {
token: String!
refreshToken: String!
user: User!
expiresIn: Int!
}
scalar Upload
Key resolver implementations with business logic and database operations.
// resolvers/post.js
const { UserInputError, ForbiddenError } = require('apollo-server');
const { generateSlug, calculateReadingTime } = require('../utils');
const postResolvers = {
Query: {
post: async (_, { id, slug }, { dataSources, user }) => {
const post = id
? await dataSources.postAPI.getPostById(id)
: await dataSources.postAPI.getPostBySlug(slug);
if (!post) {
throw new UserInputError('Post not found');
}
// Only show published posts to non-authenticated users
if (post.status !== 'PUBLISHED' && (!user || user.id !== post.authorId)) {
throw new ForbiddenError('Access denied');
}
return post;
},
posts: async (_, { filter, sort, first, after, last, before }, { dataSources }) => {
return await dataSources.postAPI.getPosts({
filter,
sort,
first,
after,
last,
before
});
},
search: async (_, { query, type, limit }, { dataSources }) => {
return await dataSources.searchAPI.search(query, type, limit);
}
},
Mutation: {
createPost: async (_, { input }, { user, dataSources }) => {
if (!user) {
throw new ForbiddenError('Authentication required');
}
if (!['ADMIN', 'EDITOR', 'AUTHOR'].includes(user.role)) {
throw new ForbiddenError('Insufficient permissions');
}
const slug = generateSlug(input.title);
const readingTime = calculateReadingTime(input.content);
const post = await dataSources.postAPI.createPost({
...input,
slug,
readingTime,
authorId: user.id,
status: input.status || 'DRAFT'
});
// Create activity log
await dataSources.activityAPI.createActivity({
type: 'POST_CREATED',
userId: user.id,
postId: post.id,
description: `created post "${post.title}"`
});
return post;
},
updatePost: async (_, { id, input }, { user, dataSources }) => {
if (!user) {
throw new ForbiddenError('Authentication required');
}
const post = await dataSources.postAPI.getPostById(id);
if (!post) {
throw new UserInputError('Post not found');
}
// Check permissions
const canEdit = user.role === 'ADMIN' ||
user.role === 'EDITOR' ||
post.authorId === user.id;
if (!canEdit) {
throw new ForbiddenError('No permission to edit this post');
}
// Update slug if title changed
if (input.title && input.title !== post.title) {
input.slug = generateSlug(input.title);
}
// Recalculate reading time if content changed
if (input.content) {
input.readingTime = calculateReadingTime(input.content);
}
const updatedPost = await dataSources.postAPI.updatePost(id, input);
// Create activity log
await dataSources.activityAPI.createActivity({
type: 'POST_UPDATED',
userId: user.id,
postId: id,
description: `updated post "${updatedPost.title}"`
});
return updatedPost;
},
publishPost: async (_, { id }, { user, dataSources }) => {
if (!user || !['ADMIN', 'EDITOR', 'AUTHOR'].includes(user.role)) {
throw new ForbiddenError('Insufficient permissions');
}
const post = await dataSources.postAPI.getPostById(id);
if (!post) {
throw new UserInputError('Post not found');
}
if (user.role === 'AUTHOR' && post.authorId !== user.id) {
throw new ForbiddenError('Cannot publish other authors\' posts');
}
const publishedPost = await dataSources.postAPI.updatePost(id, {
status: 'PUBLISHED',
publishedAt: new Date().toISOString()
});
// Create activity log
await dataSources.activityAPI.createActivity({
type: 'POST_PUBLISHED',
userId: user.id,
postId: id,
description: `published post "${publishedPost.title}"`
});
// Clear cache
await dataSources.cacheAPI.invalidate(`post:${id}`);
return publishedPost;
},
likePost: async (_, { id }, { user, dataSources }) => {
if (!user) {
throw new ForbiddenError('Authentication required');
}
const like = await dataSources.likeAPI.likePost(id, user.id);
const post = await dataSources.postAPI.getPostById(id);
return post;
},
incrementPostViews: async (_, { id }, { dataSources }) => {
await dataSources.postAPI.incrementViews(id);
return await dataSources.postAPI.getPostById(id);
}
},
Post: {
author: (post, _, { dataSources }) => {
return dataSources.userAPI.getUserById(post.authorId);
},
category: (post, _, { dataSources }) => {
return post.categoryId
? dataSources.categoryAPI.getCategoryById(post.categoryId)
: null;
},
tags: (post, _, { dataSources }) => {
return dataSources.tagAPI.getTagsByPostId(post.id);
},
comments: async (post, _, { dataSources }) => {
return await dataSources.commentAPI.getCommentsByPostId(post.id);
},
commentCount: async (post, _, { dataSources }) => {
return await dataSources.commentAPI.getCommentCount(post.id);
},
featuredImage: (post, _, { dataSources }) => {
return post.featuredImageId
? dataSources.imageAPI.getImageById(post.featuredImageId)
: null;
}
}
};
module.exports = postResolvers;
// resolvers/comment.js
const { UserInputError, ForbiddenError } = require('apollo-server');
const { sendNotificationEmail } = require('../services/email');
const commentResolvers = {
Query: {
comments: async (_, { postId, status, first, after }, { dataSources, user }) => {
// Show only approved comments to non-admins
const commentStatus = (user && ['ADMIN', 'EDITOR'].includes(user.role))
? status
: 'APPROVED';
return await dataSources.commentAPI.getComments({
postId,
status: commentStatus,
first,
after
});
}
},
Mutation: {
createComment: async (_, { input }, { user, dataSources }) => {
if (!user) {
throw new ForbiddenError('Authentication required to comment');
}
const { postId, content, parentId } = input;
// Verify post exists
const post = await dataSources.postAPI.getPostById(postId);
if (!post) {
throw new UserInputError('Post not found');
}
// Check if replying to existing comment
if (parentId) {
const parentComment = await dataSources.commentAPI.getCommentById(parentId);
if (!parentComment || parentComment.postId !== postId) {
throw new UserInputError('Invalid parent comment');
}
}
// Create comment
const comment = await dataSources.commentAPI.createComment({
postId,
authorId: user.id,
content,
parentId,
status: 'APPROVED' // Auto-approve for now, can add moderation
});
// Send notification to post author
if (post.authorId !== user.id) {
await sendNotificationEmail({
to: post.author.email,
subject: 'New comment on your post',
template: 'new-comment',
data: {
postTitle: post.title,
commenterName: user.fullName,
commentContent: content,
postUrl: `/post/${post.slug}`
}
});
}
// If reply, notify parent comment author
if (parentId) {
const parentComment = await dataSources.commentAPI.getCommentById(parentId);
if (parentComment.authorId !== user.id) {
const parentAuthor = await dataSources.userAPI.getUserById(parentComment.authorId);
await sendNotificationEmail({
to: parentAuthor.email,
subject: 'Someone replied to your comment',
template: 'comment-reply',
data: {
replierName: user.fullName,
replyContent: content,
postTitle: post.title,
postUrl: `/post/${post.slug}`
}
});
}
}
// Create activity
await dataSources.activityAPI.createActivity({
type: 'COMMENT_ADDED',
userId: user.id,
postId,
commentId: comment.id,
description: `commented on "${post.title}"`
});
return comment;
},
approveComment: async (_, { id }, { user, dataSources }) => {
if (!user || !['ADMIN', 'EDITOR'].includes(user.role)) {
throw new ForbiddenError('Only admins and editors can moderate comments');
}
return await dataSources.commentAPI.updateCommentStatus(id, 'APPROVED');
},
markAsSpam: async (_, { id }, { user, dataSources }) => {
if (!user || !['ADMIN', 'EDITOR'].includes(user.role)) {
throw new ForbiddenError('Insufficient permissions');
}
return await dataSources.commentAPI.updateCommentStatus(id, 'SPAM');
},
deleteComment: async (_, { id }, { user, dataSources }) => {
if (!user) {
throw new ForbiddenError('Authentication required');
}
const comment = await dataSources.commentAPI.getCommentById(id);
if (!comment) {
throw new UserInputError('Comment not found');
}
// Check permissions
const canDelete = user.role === 'ADMIN' ||
user.role === 'EDITOR' ||
comment.authorId === user.id;
if (!canDelete) {
throw new ForbiddenError('No permission to delete this comment');
}
await dataSources.commentAPI.deleteComment(id);
return true;
},
likeComment: async (_, { id }, { user, dataSources }) => {
if (!user) {
throw new ForbiddenError('Authentication required');
}
await dataSources.likeAPI.likeComment(id, user.id);
return await dataSources.commentAPI.getCommentById(id);
}
},
Comment: {
author: (comment, _, { dataSources }) => {
return dataSources.userAPI.getUserById(comment.authorId);
},
post: (comment, _, { dataSources }) => {
return dataSources.postAPI.getPostById(comment.postId);
},
parent: (comment, _, { dataSources }) => {
return comment.parentId
? dataSources.commentAPI.getCommentById(comment.parentId)
: null;
},
replies: async (comment, _, { dataSources }) => {
return await dataSources.commentAPI.getReplies(comment.id);
}
}
};
module.exports = commentResolvers;
Secure user authentication with JWT tokens and role-based access control.
// services/auth.js
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const { AuthenticationError } = require('apollo-server');
const JWT_SECRET = process.env.JWT_SECRET;
const JWT_REFRESH_SECRET = process.env.JWT_REFRESH_SECRET;
const JWT_EXPIRES_IN = '1h';
const REFRESH_TOKEN_EXPIRES_IN = '7d';
class AuthService {
// Generate JWT token
generateToken(user) {
return jwt.sign(
{
id: user.id,
email: user.email,
role: user.role
},
JWT_SECRET,
{ expiresIn: JWT_EXPIRES_IN }
);
}
// Generate refresh token
generateRefreshToken(user) {
return jwt.sign(
{ id: user.id },
JWT_REFRESH_SECRET,
{ expiresIn: REFRESH_TOKEN_EXPIRES_IN }
);
}
// Verify token
verifyToken(token) {
try {
return jwt.verify(token, JWT_SECRET);
} catch (error) {
throw new AuthenticationError('Invalid or expired token');
}
}
// Verify refresh token
verifyRefreshToken(token) {
try {
return jwt.verify(token, JWT_REFRESH_SECRET);
} catch (error) {
throw new AuthenticationError('Invalid or expired refresh token');
}
}
// Hash password
async hashPassword(password) {
return await bcrypt.hash(password, 12);
}
// Compare password
async comparePassword(password, hashedPassword) {
return await bcrypt.compare(password, hashedPassword);
}
// Register user
async register(userAPI, input) {
const { email, password, username, firstName, lastName } = input;
// Check if user exists
const existingUser = await userAPI.getUserByEmail(email);
if (existingUser) {
throw new UserInputError('Email already in use');
}
const existingUsername = await userAPI.getUserByUsername(username);
if (existingUsername) {
throw new UserInputError('Username already taken');
}
// Hash password
const hashedPassword = await this.hashPassword(password);
// Create user
const user = await userAPI.createUser({
email,
username,
password: hashedPassword,
firstName,
lastName,
role: 'READER' // Default role
});
// Generate tokens
const token = this.generateToken(user);
const refreshToken = this.generateRefreshToken(user);
// Store refresh token
await userAPI.saveRefreshToken(user.id, refreshToken);
return {
token,
refreshToken,
user,
expiresIn: 3600 // 1 hour in seconds
};
}
// Login user
async login(userAPI, email, password) {
// Find user
const user = await userAPI.getUserByEmail(email);
if (!user) {
throw new AuthenticationError('Invalid credentials');
}
// Verify password
const isValid = await this.comparePassword(password, user.password);
if (!isValid) {
throw new AuthenticationError('Invalid credentials');
}
// Generate tokens
const token = this.generateToken(user);
const refreshToken = this.generateRefreshToken(user);
// Store refresh token
await userAPI.saveRefreshToken(user.id, refreshToken);
return {
token,
refreshToken,
user,
expiresIn: 3600
};
}
// Refresh token
async refreshToken(userAPI, refreshToken) {
// Verify refresh token
const payload = this.verifyRefreshToken(refreshToken);
// Get user
const user = await userAPI.getUserById(payload.id);
if (!user) {
throw new AuthenticationError('User not found');
}
// Verify stored refresh token
const isValid = await userAPI.verifyRefreshToken(user.id, refreshToken);
if (!isValid) {
throw new AuthenticationError('Invalid refresh token');
}
// Generate new tokens
const newToken = this.generateToken(user);
const newRefreshToken = this.generateRefreshToken(user);
// Store new refresh token
await userAPI.saveRefreshToken(user.id, newRefreshToken);
return {
token: newToken,
refreshToken: newRefreshToken,
user,
expiresIn: 3600
};
}
}
module.exports = new AuthService();
// directives/auth.js
const { SchemaDirectiveVisitor } = require('apollo-server');
const { defaultFieldResolver } = require('graphql');
const { AuthenticationError, ForbiddenError } = require('apollo-server');
// @auth directive - requires authentication
class AuthDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
field.resolve = async function (...args) {
const context = args[2];
if (!context.user) {
throw new AuthenticationError('Authentication required');
}
return resolve.apply(this, args);
};
}
}
// @hasRole directive - requires specific role
class HasRoleDirective extends SchemaDirectiveVisitor {
visitFieldDefinition(field) {
const { resolve = defaultFieldResolver } = field;
const { roles } = this.args;
field.resolve = async function (...args) {
const context = args[2];
if (!context.user) {
throw new AuthenticationError('Authentication required');
}
if (!roles.includes(context.user.role)) {
throw new ForbiddenError(
`Requires one of these roles: ${roles.join(', ')}`
);
}
return resolve.apply(this, args);
};
}
}
// Usage in schema
const typeDefs = gql`
directive @auth on FIELD_DEFINITION
directive @hasRole(roles: [UserRole!]!) on FIELD_DEFINITION
type Query {
me: User @auth
analytics: Analytics @hasRole(roles: [ADMIN, EDITOR])
}
type Mutation {
createPost(input: CreatePostInput!): Post! @hasRole(roles: [ADMIN, EDITOR, AUTHOR])
deleteUser(id: ID!): Boolean! @hasRole(roles: [ADMIN])
updateProfile(input: UpdateProfileInput!): User! @auth
}
`;
module.exports = { AuthDirective, HasRoleDirective };
Optimization strategies for search engines and fast content delivery.
Automatic generation of meta tags, Open Graph, Twitter Cards, and structured data (JSON-LD) for better search visibility.
Dynamic sitemap generation with priority and change frequency for efficient crawling by search engines.
Cache frequently accessed posts and queries with smart invalidation strategies for lightning-fast response times.
// utils/seo.js
// Generate slug from title
function generateSlug(title) {
return title
.toLowerCase()
.trim()
.replace(/[^\w\s-]/g, '')
.replace(/[\s_-]+/g, '-')
.replace(/^-+|-+$/g, '');
}
// Calculate reading time
function calculateReadingTime(content) {
const wordsPerMinute = 200;
const words = content.trim().split(/\s+/).length;
return Math.ceil(words / wordsPerMinute);
}
// Generate meta description from content
function generateMetaDescription(content, maxLength = 160) {
const plainText = content
.replace(/<[^>]*>/g, '') // Remove HTML tags
.replace(/\s+/g, ' ') // Normalize whitespace
.trim();
if (plainText.length <= maxLength) {
return plainText;
}
return plainText.substring(0, maxLength - 3) + '...';
}
// Generate structured data (JSON-LD)
function generateStructuredData(post) {
return {
'@context': 'https://schema.org',
'@type': 'BlogPosting',
headline: post.title,
description: post.excerpt || generateMetaDescription(post.content),
image: post.featuredImage?.url,
author: {
'@type': 'Person',
name: post.author.fullName,
url: `/author/${post.author.username}`
},
publisher: {
'@type': 'Organization',
name: 'Your Blog Name',
logo: {
'@type': 'ImageObject',
url: 'https://yourblog.com/logo.png'
}
},
datePublished: post.publishedAt,
dateModified: post.updatedAt,
mainEntityOfPage: {
'@type': 'WebPage',
'@id': `https://yourblog.com/post/${post.slug}`
}
};
}
// Generate XML sitemap
async function generateSitemap(postAPI) {
const posts = await postAPI.getAllPublishedPosts();
let xml = '\n';
xml += '\n';
// Homepage
xml += ' \n';
xml += ' https://yourblog.com/ \n';
xml += ' daily \n';
xml += ' 1.0 \n';
xml += ' \n';
// Posts
posts.forEach(post => {
xml += ' \n';
xml += ` https://yourblog.com/post/${post.slug} \n`;
xml += ` ${post.updatedAt} \n`;
xml += ' weekly \n';
xml += ' 0.8 \n';
xml += ' \n';
});
xml += ' ';
return xml;
}
// Generate RSS feed
async function generateRSSFeed(postAPI) {
const posts = await postAPI.getRecentPosts(20);
let xml = '\n';
xml += '\n';
xml += ' \n';
xml += ' Your Blog Name \n';
xml += ' https://yourblog.com\n';
xml += ' Blog description \n';
posts.forEach(post => {
xml += ' - \n';
xml += `
${post.title} \n`;
xml += ` https://yourblog.com/post/${post.slug}\n`;
xml += ` ${post.excerpt || ''} \n`;
xml += ` ${new Date(post.publishedAt).toUTCString()} \n`;
xml += ' \n';
});
xml += ' \n';
xml += ' ';
return xml;
}
module.exports = {
generateSlug,
calculateReadingTime,
generateMetaDescription,
generateStructuredData,
generateSitemap,
generateRSSFeed
};
// services/cache.js
const Redis = require('ioredis');
class CacheService {
constructor() {
this.redis = new Redis({
host: process.env.REDIS_HOST || 'localhost',
port: process.env.REDIS_PORT || 6379,
password: process.env.REDIS_PASSWORD
});
}
// Cache post
async cachePost(post) {
const key = `post:${post.id}`;
await this.redis.setex(key, 3600, JSON.stringify(post)); // 1 hour
}
// Get cached post
async getCachedPost(postId) {
const key = `post:${postId}`;
const cached = await this.redis.get(key);
return cached ? JSON.parse(cached) : null;
}
// Cache post list
async cachePostList(filter, posts) {
const key = `posts:${JSON.stringify(filter)}`;
await this.redis.setex(key, 600, JSON.stringify(posts)); // 10 minutes
}
// Invalidate post cache
async invalidatePost(postId) {
await this.redis.del(`post:${postId}`);
// Also invalidate related lists
const keys = await this.redis.keys('posts:*');
if (keys.length > 0) {
await this.redis.del(...keys);
}
}
// Cache popular posts
async cachePopularPosts(posts) {
await this.redis.setex('popular:posts', 1800, JSON.stringify(posts)); // 30 minutes
}
// Get cached popular posts
async getCachedPopularPosts() {
const cached = await this.redis.get('popular:posts');
return cached ? JSON.parse(cached) : null;
}
}
module.exports = new CacheService();
Production deployment with Docker, environment configuration, and best practices.
# docker-compose.yml
version: '3.8'
services:
# GraphQL API
api:
build:
context: .
dockerfile: Dockerfile
ports:
- "4000:4000"
environment:
- NODE_ENV=production
- DATABASE_URL=postgresql://postgres:password@postgres:5432/blog
- REDIS_URL=redis://redis:6379
- JWT_SECRET=${JWT_SECRET}
- AWS_S3_BUCKET=${AWS_S3_BUCKET}
- SMTP_HOST=${SMTP_HOST}
- SMTP_USER=${SMTP_USER}
- SMTP_PASS=${SMTP_PASS}
depends_on:
- postgres
- redis
restart: unless-stopped
# PostgreSQL
postgres:
image: postgres:15-alpine
environment:
- POSTGRES_USER=postgres
- POSTGRES_PASSWORD=password
- POSTGRES_DB=blog
volumes:
- postgres_data:/var/lib/postgresql/data
- ./scripts/init.sql:/docker-entrypoint-initdb.d/init.sql
ports:
- "5432:5432"
restart: unless-stopped
# Redis
redis:
image: redis:7-alpine
command: redis-server --appendonly yes
volumes:
- redis_data:/data
ports:
- "6379:6379"
restart: unless-stopped
# Nginx (Optional - for serving frontend)
nginx:
image: nginx:alpine
ports:
- "80:80"
- "443:443"
volumes:
- ./nginx.conf:/etc/nginx/nginx.conf
- ./ssl:/etc/nginx/ssl
depends_on:
- api
restart: unless-stopped
volumes:
postgres_data:
redis_data:
# .env.production
NODE_ENV=production
PORT=4000
# Database
DATABASE_URL=postgresql://user:password@host:5432/blog
# Redis
REDIS_URL=redis://host:6379
REDIS_HOST=host
REDIS_PORT=6379
# JWT
JWT_SECRET=your-secret-key-change-in-production
JWT_REFRESH_SECRET=your-refresh-secret
JWT_EXPIRES_IN=1h
# AWS S3
AWS_ACCESS_KEY_ID=your-access-key
AWS_SECRET_ACCESS_KEY=your-secret-key
AWS_S3_BUCKET=blog-media
AWS_REGION=us-east-1
# Email (SMTP)
SMTP_HOST=smtp.gmail.com
SMTP_PORT=587
SMTP_USER=your-email@gmail.com
SMTP_PASS=your-app-password
SMTP_FROM=noreply@yourblog.com
# Site Configuration
SITE_URL=https://yourblog.com
SITE_NAME=Your Blog Name
# API
CORS_ORIGIN=https://yourblog.com,https://admin.yourblog.com
# Rate Limiting
RATE_LIMIT_MAX=100
RATE_LIMIT_WINDOW=900000
# Logging
LOG_LEVEL=info
SENTRY_DSN=your-sentry-dsn
Flexible content structure with categories, tags, and custom fields makes the system adaptable to different blogging needs.
Role-based access control and input validation prevent unauthorized access and malicious content injection.
Caching with Redis and DataLoader significantly reduce database queries and improve response times for popular content.
Proper meta tags, structured data, and sitemap generation are essential for content discoverability in search engines.
Comment system with moderation and email notifications keeps readers engaged while preventing spam.
Tracking views, engagement, and popular content helps understand what resonates with your audience.
This Blog API demonstrates a production-ready content management system built with GraphQL. The architecture supports multiple clients (web, mobile, admin) through a unified API with comprehensive features for content creation, user management, and SEO optimization.
Key achievements include role-based permissions, rich text editing, comment moderation, media management, and performance optimization through caching. The API-first design enables building any type of frontend while maintaining consistent business logic.