🎯 empfohlene Sammlungen
Balanced sample collections from various categories for you to explore
Ghost Beispiele
Ghost CMS Beispiele einschließlich Themes, API-Integration, benutzerdefinierten Routen und Inhaltsverwaltung
💻 Ghost Hello World javascript
🟢 simple
⭐⭐
Grundlegende Ghost CMS Setup- und Konfigurationsbeispiele
⏱️ 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
};
💻 Ghost Theme Entwicklung handlebars
🟡 intermediate
⭐⭐⭐⭐
Erweiterte Ghost Theme Entwicklung mit Handlebars-Helfern und benutzerdefinierten Integrationen
⏱️ 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>
💻 Ghost API Integration javascript
🟡 intermediate
⭐⭐⭐⭐
Vollständige Ghost Admin- und Content-API-Integrationsbeispiele mit Webhooks und benutzerdefinierten Endpoints
⏱️ 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
};