🎯 Exemples recommandés
Balanced sample collections from various categories for you to explore
Exemples Ghost
Exemples de Ghost CMS incluant les thèmes, l'intégration d'API, les routes personnalisées et la gestion de contenu
💻 Ghost Hello World javascript
🟢 simple
⭐⭐
Exemples de base de configuration et de paramétrage de Ghost CMS
⏱️ 15 min
🏷️ ghost, cms, publishing, api
Prerequisites:
Node.js basics, REST API concepts
// Ghost CMS Hello World Examples
// 1. Ghost Admin API Initialization
const GhostAdminAPI = require('@tryghost/admin-api');
// Initialize Ghost Admin API
const api = new GhostAdminAPI({
url: 'https://your-site.com',
key: 'your-admin-api-key',
version: 'v5.0'
});
// 2. Basic Ghost Content API
const GhostContentAPI = require('@tryghost/content-api');
// Initialize Ghost Content API
const contentApi = new GhostContentAPI({
url: 'https://your-site.com',
key: 'your-content-api-key',
version: 'v5.0'
});
// 3. Fetch Posts
async function fetchPosts() {
try {
const posts = await contentApi.posts.browse({
limit: 5,
include: 'tags,authors'
});
console.log('Recent posts:', posts);
return posts;
} catch (error) {
console.error('Error fetching posts:', error);
}
}
// 4. Create a New Post
async function createPost() {
try {
const newPost = await api.posts.add({
title: 'Hello from Ghost API!',
html: '<p>This is my first post created via the Ghost Admin API.</p>',
status: 'draft',
tags: [{ name: 'API' }, { name: 'Tutorial' }]
});
console.log('New post created:', newPost);
return newPost;
} catch (error) {
console.error('Error creating post:', error);
}
}
// 5. Ghost Theme Structure
// Default theme structure for Ghost
const themeStructure = {
"package.json": {
"name": "my-ghost-theme",
"version": "1.0.0",
"description": "A custom Ghost theme",
"engines": {
"ghost": ">=5.0.0"
}
},
"default.hbs": `<!DOCTYPE html>
<html lang="{{lang}}">
<head>
<meta charset="utf-8">
<title>{{meta_title}}</title>
<meta name="description" content="{{meta_description}}">
{{ghost_head}}
</head>
<body>
<div class="gh-site">
{{> header}}
<main class="gh-main">
{{{body}}}
</main>
{{> footer}}
</div>
{{ghost_foot}}
</body>
</html>`,
"post.hbs": `<article class="{{post_class}}">
<header class="post-header">
<h1 class="post-title">{{title}}</h1>
<time datetime="{{date format="YYYY-MM-DD"}}">{{date}}</time>
{{#primary_author}}
<div class="post-author">
{{> "components/author-card"}}
</div>
{{/primary_author}}
</header>
<div class="post-content">
{{content}}
</div>
<footer class="post-footer">
{{#if tags}}
<div class="post-tags">
{{#foreach tags}}
<span class="tag">{{name}}</span>
{{/foreach}}
</div>
{{/if}}
</footer>
</article>`,
"index.hbs": `{{#foreach posts}}
<article class="post-card {{post_class}}">
<a class="post-card-image-link" href="{{url}}">
{{#if feature_image}}
<img class="post-card-image" src="{{feature_image}}" alt="{{title}}" />
{{/if}}
</a>
<div class="post-card-content">
<a class="post-card-content-link" href="{{url}}">
<header class="post-card-header">
<h2 class="post-card-title">{{title}}</h2>
</header>
<section class="post-card-excerpt">
<p>{{excerpt words="33"}}</p>
</section>
</a>
<footer class="post-card-meta">
<ul class="author-list">
{{#foreach authors}}
<li class="author-list-item">
{{#if profile_image}}
<img class="author-profile-image" src="{{profile_image}}" alt="{{name}}" />
{{else}}
<span class="author-profile-image">{{name}}</span>
{{/if}}
</li>
{{/foreach}}
</ul>
<span class="post-card-date">
<time datetime="{{date format="YYYY-MM-DD"}}">{{date}}</time>
</span>
</footer>
</div>
</article>
{{/foreach}}`
};
// 6. Ghost Helper Function
const ghostHelpers = {
// Custom helper for reading time calculation
readingTime: function(content) {
const wordsPerMinute = 200;
const words = content.split(/\s+/).length;
const readingTime = Math.ceil(words / wordsPerMinute);
return readingTime;
},
// Helper for featured posts
getFeaturedPosts: function(posts) {
return posts.filter(post => post.featured);
},
// Helper for related posts
getRelatedPosts: function(currentPost, allPosts, limit = 3) {
return allPosts
.filter(post => post.id !== currentPost.id)
.filter(post => {
// Check for common tags
const currentTags = currentPost.tags.map(tag => tag.id);
const postTags = post.tags.map(tag => tag.id);
return postTags.some(tag => currentTags.includes(tag));
})
.slice(0, limit);
}
};
// 7. Ghost Webhook Handler
const express = require('express');
const app = express();
app.use(express.json());
// Ghost webhook endpoint
app.post('/ghost/webhook', (req, res) => {
const { type, post } = req.body;
switch (type) {
case 'post.added':
console.log('New post added:', post.title);
// Send notification, update cache, etc.
break;
case 'post.edited':
console.log('Post edited:', post.title);
// Rebuild static files, update search index, etc.
break;
case 'post.deleted':
console.log('Post deleted:', post.title);
// Clean up related content, update sitemap, etc.
break;
case 'site.changed':
console.log('Site configuration changed');
// Update configuration, clear caches, etc.
break;
}
res.status(200).send('Webhook received');
});
// 8. Ghost Custom Route Example
const customRoutes = {
// Custom route for popular posts
'/popular': {
data: 'posts',
filter: 'featured:true',
limit: 10,
template: 'popular-posts'
},
// Custom route for author archive
'/author/:slug': {
data: 'author',
match: '/author/:slug',
template: 'author'
},
// Custom route for tag archive with pagination
'/tag/:slug/page/:page': {
data: 'tag',
match: '/tag/:slug/page/:page',
template: 'tag'
}
};
// 9. Ghost Member API Integration
async function getMemberData(memberId) {
try {
const member = await api.members.read({ id: memberId });
// Get member's subscription status
const subscription = await api.subscriptions.browse({
filter: `member_id:${memberId}`
});
return {
member,
subscription: subscription[0] || null,
hasAccess: subscription.some(sub => sub.status === 'active')
};
} catch (error) {
console.error('Error fetching member data:', error);
return null;
}
}
// 10. Ghost Content API with Caching
const NodeCache = require('node-cache');
const cache = new NodeCache({ stdTTL: 300 }); // 5 minutes cache
async function getCachedPosts() {
const cacheKey = 'all_posts';
let posts = cache.get(cacheKey);
if (!posts) {
posts = await contentApi.posts.browse({
limit: 'all',
include: 'tags,authors'
});
cache.set(cacheKey, posts);
console.log('Posts cached');
}
return posts;
}
// Usage examples
console.log('Ghost CMS Setup Complete!');
console.log('1. Initialize APIs with your credentials');
console.log('2. Create your theme structure');
console.log('3. Set up webhooks for real-time updates');
console.log('4. Implement caching for better performance');
console.log('5. Add custom routes as needed');
// Export for module usage
module.exports = {
api,
contentApi,
ghostHelpers,
themeStructure,
customRoutes,
getMemberData,
getCachedPosts
};
💻 Développement de Thèmes Ghost handlebars
🟡 intermediate
⭐⭐⭐⭐
Développement avancé de thèmes Ghost avec helpers Handlebars et intégrations personnalisées
⏱️ 30 min
🏷️ ghost, themes, handlebars, development
Prerequisites:
Ghost basics, Handlebars templates, CSS/JavaScript
{{!-- Ghost Theme Development Examples --}}
{{! 1. Advanced Theme Structure with Partials }}
{{! partials/components/author-card.hbs }}
<div class="author-card">
{{#if profile_image}}
<img class="author-profile-image" src="{{profile_image}}" alt="{{name}}" />
{{else}}
<div class="author-profile-image">{{name}}</div>
{{/if}}
<div class="author-info">
<h4 class="author-name">{{name}}</h4>
{{#if bio}}
<p class="author-bio">{{bio}}</p>
{{/if}}
<a href="{{url}}" class="author-link">More posts</a>
</div>
</div>
{{! 2. Custom Theme Configuration }}
{{! ghost.config.js }}
module.exports = {
development: {
url: 'http://localhost:2368',
database: {
client: 'sqlite3',
connection: {
filename: './content/data/ghost-dev.db'
}
},
server: {
host: '127.0.0.1',
port: '2368'
}
},
production: {
url: process.env.SITE_URL,
database: {
client: 'mysql',
connection: {
host: process.env.DB_HOST,
user: process.env.DB_USER,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME
}
}
}
};
{{! 3. Advanced Pagination }}
{{! partials/pagination.hbs }}
<nav class="pagination">
{{#if prev}}
<a class="newer-posts" href="{{page_url prev}}">
<span aria-hidden="true">←</span> Newer Posts
</a>
{{/if}}
<span class="page-number">Page {{page}} of {{pages}}</span>
{{#if next}}
<a class="older-posts" href="{{page_url next}}">
Older Posts <span aria-hidden="true">→</span>
</a>
{{/if}}
</nav>
{{! 4. Dynamic Content Blocks }}
{{! partials/content-blocks.hbs }}
{{#content}}
{{#has tag="#format-gallery"}}
<div class="content-block gallery-block">
{{#if content}}
<div class="gallery-grid">
{{#foreach images}}
<div class="gallery-item">
<img src="{{url}}" alt="{{alt}}" />
{{#if caption}}
<p class="gallery-caption">{{caption}}</p>
{{/if}}
</div>
{{/foreach}}
</div>
{{/if}}
</div>
{{else if has tag="#format-video"}}
<div class="content-block video-block">
{{#if content}}
<div class="video-container">
{{{content}}}
</div>
{{/if}}
</div>
{{else if has tag="#format-pullquote"}}
<div class="content-block pullquote-block">
{{#if content}}
<blockquote class="pullquote">
{{content}}
</blockquote>
{{/if}}
</div>
{{else}}
<div class="content-block text-block">
{{#if content}}
{{content}}
{{/if}}
</div>
{{/has}}
{{/content}}
{{! 5. Search Integration }}
{{! partials/search.hbs }}
<div class="search-container">
<input type="search" id="search-input" placeholder="Search posts..." />
<div id="search-results" class="search-results" style="display: none;">
<div class="search-loading">Searching...</div>
</div>
</div>
<script>
// Client-side search integration
const searchInput = document.getElementById('search-input');
const searchResults = document.getElementById('search-results');
let postsData = [];
// Fetch all posts for search
fetch('/content/api/search/posts.json')
.then(response => response.json())
.then(data => {
postsData = data;
});
searchInput.addEventListener('input', function(e) {
const query = e.target.value.toLowerCase();
if (query.length < 3) {
searchResults.style.display = 'none';
return;
}
const results = postsData.filter(post =>
post.title.toLowerCase().includes(query) ||
post.excerpt.toLowerCase().includes(query) ||
post.tags.some(tag => tag.name.toLowerCase().includes(query))
);
displaySearchResults(results);
});
function displaySearchResults(results) {
if (results.length === 0) {
searchResults.innerHTML = '<div class="search-no-results">No posts found</div>';
searchResults.style.display = 'block';
return;
}
const html = results.map(post => `
<article class="search-result">
<h3><a href="${post.url}">${post.title}</a></h3>
<p>${post.excerpt}</p>
<div class="search-meta">
<time>${new Date(post.published_at).toLocaleDateString()}</time>
<span class="search-tags">
${post.tags.map(tag => `<span class="tag">${tag.name}</span>`).join('')}
</span>
</div>
</article>
`).join('');
searchResults.innerHTML = html;
searchResults.style.display = 'block';
}
</script>
{{! 6. Newsletter Signup Form }}
{{! partials/newsletter.hbs }}
<div class="newsletter-signup">
<h3>Subscribe to our newsletter</h3>
<p>Get the latest posts delivered right to your inbox.</p>
<form data-members-form="subscribe" class="newsletter-form">
<div class="form-group">
<input
data-members-email
type="email"
placeholder="Your email address"
required
autocomplete="email"
/>
</div>
<button type="submit" class="newsletter-button">
<span class="newsletter-default-text">Subscribe</span>
<span class="newsletter-loading-text">Subscribing...</span>
<span class="newsletter-success-text">Subscribed!</span>
<span class="newsletter-error-text">Error</span>
</button>
<div class="message-success">
<strong>Great!</strong> Check your inbox and click the link to confirm your subscription.
</div>
<div class="message-error">
<strong>Error:</strong> Please enter a valid email address.
</div>
</form>
</div>
{{! 7. Member-Only Content }}
{{#if access}}
{{! Content available to all }}
<div class="content-accessible">
{{content}}
</div>
{{else if @member}}
{{! Content available to members }}
<div class="content-members">
{{content}}
</div>
{{else if @site.members_enabled}}
{{! Content requires membership }}
<div class="content-locked">
<div class="locked-message">
<h2>This content is for members only</h2>
<p>Subscribe to get access to this post and other member-only content.</p>
<div class="signup-options">
{{#if @site.members_signup_form}}
<form data-members-form="signup" class="signup-form">
<h3>Join our community</h3>
<div class="form-group">
<input
data-members-name
type="text"
placeholder="Your name"
required
autocomplete="name"
/>
</div>
<div class="form-group">
<input
data-members-email
type="email"
placeholder="Your email address"
required
autocomplete="email"
/>
</div>
<button type="submit" class="signup-button">Subscribe</button>
</form>
{{/if}}
</div>
</div>
</div>
{{/if}}
{{! 8. Social Sharing }}
{{! partials/social-share.hbs }}
<div class="social-share">
<h4>Share this post</h4>
<div class="share-buttons">
<a href="https://twitter.com/intent/tweet?text={{encode title}}&url={{url absolute="true"}}"
class="share-button twitter" target="_blank" rel="noopener">
Twitter
</a>
<a href="https://www.facebook.com/sharer/sharer.php?u={{url absolute="true"}}"
class="share-button facebook" target="_blank" rel="noopener">
Facebook
</a>
<a href="https://www.linkedin.com/sharing/share-offsite/?url={{url absolute="true"}}"
class="share-button linkedin" target="_blank" rel="noopener">
LinkedIn
</a>
<a href="mailto:?subject={{encode title}}&body={{encode excerpt}} {{url absolute="true"}}"
class="share-button email">
Email
</a>
</div>
</div>
{{! 9. Reading Progress Bar }}
<div class="reading-progress-bar">
<div class="reading-progress"></div>
</div>
<script>
// Reading progress calculation
document.addEventListener('DOMContentLoaded', function() {
const progressBar = document.querySelector('.reading-progress');
const article = document.querySelector('article');
if (!progressBar || !article) return;
function updateProgress() {
const articleTop = article.offsetTop;
const articleHeight = article.offsetHeight;
const viewportHeight = window.innerHeight;
const scrollY = window.scrollY;
const progress = Math.min(
Math.max(
(scrollY - articleTop + viewportHeight) / articleHeight,
0
),
1
);
progressBar.style.width = `${progress * 100}%`;
}
window.addEventListener('scroll', updateProgress);
updateProgress();
});
</script>
{{! 10. Advanced Post Meta }}
{{! partials/post-meta.hbs }}
<div class="post-meta">
<div class="post-meta-left">
{{#primary_author}}
<div class="post-author">
{{#if profile_image}}
<a href="{{url}}" class="author-avatar">
<img src="{{img_url profile_image size="xs"}}" alt="{{name}}" />
</a>
{{/if}}
<div class="author-info">
<h4 class="author-name">
<a href="{{url}}">{{name}}</a>
</h4>
{{#if twitter}}
<a href="https://twitter.com/{{twitter}}" class="author-twitter" target="_blank">
@{{twitter}}
</a>
{{/if}}
</div>
</div>
{{/primary_author}}
</div>
<div class="post-meta-right">
<time datetime="{{date format="YYYY-MM-DD"}}" class="post-date">
{{date format="D MMMM YYYY"}}
</time>
{{#if reading_time}}
<span class="post-reading-time">
{{reading_time}} min read
</span>
{{/if}}
{{#if comments_enabled}}
<a href="{{url}}#comments" class="post-comments-link">
<span class="comment-count">
{{comment_count "0" "1" "%"}}
</span>
</a>
{{/if}}
</div>
</div>
💻 Intégration d'API Ghost javascript
🟡 intermediate
⭐⭐⭐⭐
Exemples complets d'intégration d'API d'administration et de contenu Ghost avec webhooks et endpoints personnalisés
⏱️ 45 min
🏷️ ghost, api, integration, webhooks
Prerequisites:
Ghost CMS, Node.js, REST APIs, Webhooks
// Ghost API Integration Examples
const GhostAdminAPI = require('@tryghost/admin-api');
const GhostContentAPI = require('@tryghost/content-api');
const express = require('express');
const bodyParser = require('body-parser');
const crypto = require('crypto');
// Initialize Ghost APIs
const ghostAdmin = new GhostAdminAPI({
url: process.env.GHOST_URL,
key: process.env.GHOST_ADMIN_API_KEY,
version: 'v5.0'
});
const ghostContent = new GhostContentAPI({
url: process.env.GHOST_URL,
key: process.env.GHOST_CONTENT_API_KEY,
version: 'v5.0'
});
const app = express();
app.use(bodyParser.json());
// 1. Webhook Verification Middleware
function verifyGhostWebhook(req, res, next) {
const signature = req.get('X-Ghost-Signature');
const key = process.env.GHOST_WEBHOOK_SECRET;
if (!signature) {
return res.status(401).send('No signature provided');
}
const hmac = crypto.createHmac('sha256', key);
hmac.update(JSON.stringify(req.body));
const expectedSignature = `sha256=${hmac.digest('hex')}`;
if (crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature))) {
next();
} else {
res.status(401).send('Invalid signature');
}
}
// 2. Content Management Service
class ContentManager {
constructor(api) {
this.api = api;
this.cache = new Map();
}
// Create or update post with metadata
async savePost(postData) {
try {
const post = await this.api.posts.add({
title: postData.title,
html: postData.content,
status: postData.status || 'draft',
featured: postData.featured || false,
excerpt: postData.excerpt,
tags: postData.tags || [],
authors: postData.authors || [],
meta_title: postData.seoTitle,
meta_description: postData.seoDescription,
og_image: postData.featuredImage,
published_at: postData.publishDate,
custom_excerpt: postData.customExcerpt
});
// Clear relevant cache
this.invalidateCache();
return post;
} catch (error) {
console.error('Error saving post:', error);
throw error;
}
}
// Bulk upload posts from external source
async bulkImportPosts(postsData) {
const results = [];
for (const postData of postsData) {
try {
const post = await this.savePost(postData);
results.push({ success: true, post, original: postData });
} catch (error) {
results.push({ success: false, error, original: postData });
}
}
return results;
}
// Get posts with advanced filtering
async getPosts(options = {}) {
const cacheKey = JSON.stringify(options);
if (this.cache.has(cacheKey)) {
return this.cache.get(cacheKey);
}
try {
const posts = await this.api.posts.browse({
limit: options.limit || 10,
page: options.page || 1,
include: options.include || ['tags', 'authors'],
filter: options.filter,
order: options.order || 'published_at DESC'
});
// Cache for 5 minutes
this.cache.set(cacheKey, posts);
setTimeout(() => this.cache.delete(cacheKey), 5 * 60 * 1000);
return posts;
} catch (error) {
console.error('Error fetching posts:', error);
throw error;
}
}
invalidateCache() {
this.cache.clear();
}
}
// 3. Member Management Service
class MemberManager {
constructor(api) {
this.api = api;
}
// Create member with subscription
async createMember(memberData) {
try {
const member = await this.api.members.add({
name: memberData.name,
email: memberData.email,
subscribed: memberData.subscribed || true,
comped: memberData.comped || false,
note: memberData.note
});
// Create subscription if provided
if (memberData.subscription) {
await this.createSubscription(member.id, memberData.subscription);
}
return member;
} catch (error) {
console.error('Error creating member:', error);
throw error;
}
}
// Create subscription for member
async createSubscription(memberId, subscriptionData) {
try {
const subscription = await this.api.subscriptions.add({
customer_id: subscriptionData.customerId,
plan: subscriptionData.plan,
status: subscriptionData.status || 'active',
start_date: subscriptionData.startDate,
current_period_start: subscriptionData.currentPeriodStart,
current_period_end: subscriptionData.currentPeriodEnd,
cancel_at_period_end: subscriptionData.cancelAtPeriodEnd || false
});
return subscription;
} catch (error) {
console.error('Error creating subscription:', error);
throw error;
}
}
// Get member with subscription info
async getMemberWithSubscription(memberId) {
try {
const [member, subscriptions] = await Promise.all([
this.api.members.read({ id: memberId }),
this.api.subscriptions.browse({
filter: `member_id:${memberId}`
})
]);
return {
member,
subscriptions,
hasActiveSubscription: subscriptions.some(sub => sub.status === 'active')
};
} catch (error) {
console.error('Error fetching member:', error);
throw error;
}
}
}
// 4. SEO and Analytics Service
class SEOManager {
constructor() {
this.sitemapCache = null;
}
// Generate sitemap
async generateSitemap() {
try {
const posts = await ghostContent.posts.browse({
limit: 'all',
fields: 'url,updated_at'
});
const pages = await ghostContent.pages.browse({
limit: 'all',
fields: 'url,updated_at'
});
const tags = await ghostContent.tags.browse({
limit: 'all',
fields: 'url'
});
const authors = await ghostContent.authors.browse({
limit: 'all',
fields: 'url'
});
const sitemap = this.buildSitemapXML(posts, pages, tags, authors);
this.sitemapCache = sitemap;
return sitemap;
} catch (error) {
console.error('Error generating sitemap:', error);
throw error;
}
}
buildSitemapXML(posts, pages, tags, authors) {
const baseUrl = process.env.SITE_URL;
const items = [...posts, ...pages, ...tags, ...authors];
const urlEntries = items.map(item => {
const lastmod = item.updated_at ? new Date(item.updated_at).toISOString() : new Date().toISOString();
return `
<url>
<loc>${baseUrl}${item.url}</loc>
<lastmod>${lastmod}</lastmod>
<changefreq>weekly</changefreq>
<priority>${item.featured ? '0.8' : '0.6'}</priority>
</url>`;
}).join('');
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urlEntries}
</urlset>`;
}
// Generate structured data for posts
generateArticleStructuredData(post) {
return {
"@context": "https://schema.org",
"@type": "Article",
"headline": post.title,
"description": post.excerpt,
"image": post.feature_image ? [post.feature_image] : [],
"datePublished": post.published_at,
"dateModified": post.updated_at,
"author": {
"@type": "Person",
"name": post.primary_author.name
},
"publisher": {
"@type": "Organization",
"name": process.env.SITE_NAME || "Ghost Blog",
"logo": {
"@type": "ImageObject",
"url": process.env.SITE_LOGO
}
},
"mainEntityOfPage": {
"@type": "WebPage",
"@id": `${process.env.SITE_URL}${post.url}`
}
};
}
}
// 5. Webhook Handlers
app.post('/ghost/webhook', verifyGhostWebhook, async (req, res) => {
const { type, post, member, site } = req.body;
try {
switch (type) {
case 'post.added':
await handlePostAdded(post);
break;
case 'post.edited':
await handlePostEdited(post);
break;
case 'post.deleted':
await handlePostDeleted(post);
break;
case 'member.added':
await handleMemberAdded(member);
break;
case 'member.deleted':
await handleMemberDeleted(member);
break;
case 'site.changed':
await handleSiteChanged(site);
break;
}
res.status(200).send('Webhook processed successfully');
} catch (error) {
console.error('Webhook processing error:', error);
res.status(500).send('Webhook processing failed');
}
});
async function handlePostAdded(post) {
console.log('New post published:', post.title);
// Update sitemap
await seoManager.generateSitemap();
// Send notifications
await sendNewPostNotification(post);
// Update search index
await updateSearchIndex('add', post);
}
async function handlePostEdited(post) {
console.log('Post updated:', post.title);
// Update sitemap
await seoManager.generateSitemap();
// Update search index
await updateSearchIndex('update', post);
}
async function handlePostDeleted(post) {
console.log('Post deleted:', post.title);
// Update sitemap
await seoManager.generateSitemap();
// Remove from search index
await updateSearchIndex('delete', post);
}
// 6. Custom API Endpoints
const contentManager = new ContentManager(ghostContent);
const memberManager = new MemberManager(ghostAdmin);
const seoManager = new SEOManager();
// Search endpoint
app.get('/api/search', async (req, res) => {
try {
const { q, type = 'posts', limit = 10 } = req.query;
if (!q) {
return res.status(400).json({ error: 'Search query is required' });
}
let results = [];
if (type === 'posts' || type === 'all') {
const posts = await contentManager.getPosts({
filter: `title:${q}*+excerpt:${q}*`,
limit
});
results.push(...posts.map(post => ({ ...post, type: 'post' })));
}
if (type === 'tags' || type === 'all') {
const tags = await ghostContent.tags.browse({
limit,
include: 'count.posts'
});
const filteredTags = tags.filter(tag =>
tag.name.toLowerCase().includes(q.toLowerCase())
);
results.push(...filteredTags.map(tag => ({ ...tag, type: 'tag' })));
}
if (type === 'authors' || type === 'all') {
const authors = await ghostContent.authors.browse({
limit
});
const filteredAuthors = authors.filter(author =>
author.name.toLowerCase().includes(q.toLowerCase())
);
results.push(...filteredAuthors.map(author => ({ ...author, type: 'author' })));
}
res.json({ results });
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Popular posts endpoint
app.get('/api/popular-posts', async (req, res) => {
try {
const { limit = 5 } = req.query;
const posts = await contentManager.getPosts({
filter: 'featured:true',
limit: parseInt(limit),
order: 'published_at DESC'
});
res.json(posts);
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Member dashboard data
app.get('/api/member/dashboard', async (req, res) => {
try {
const memberId = req.headers['x-member-id'];
if (!memberId) {
return res.status(401).json({ error: 'Member ID required' });
}
const memberData = await memberManager.getMemberWithSubscription(memberId);
// Get member's reading history (custom implementation)
const readingHistory = await getMemberReadingHistory(memberId);
res.json({
member: memberData,
readingHistory
});
} catch (error) {
res.status(500).json({ error: error.message });
}
});
// Sitemap endpoint
app.get('/sitemap.xml', async (req, res) => {
try {
let sitemap = seoManager.sitemapCache;
if (!sitemap) {
sitemap = await seoManager.generateSitemap();
}
res.set('Content-Type', 'application/xml');
res.send(sitemap);
} catch (error) {
res.status(500).send('Error generating sitemap');
}
});
// 7. Helper functions
async function sendNewPostNotification(post) {
// Implement email notification, webhook, etc.
console.log('Notification sent for new post:', post.title);
}
async function updateSearchIndex(action, post) {
// Implement search index update (Elasticsearch, Algolia, etc.)
console.log('Search index updated:', action, post.title);
}
async function getMemberReadingHistory(memberId) {
// Implement reading history tracking
return [];
}
// Start server
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log(`Ghost API integration server running on port ${PORT}`);
});
module.exports = {
ContentManager,
MemberManager,
SEOManager,
ghostAdmin,
ghostContent
};