Ghost 示例

Ghost CMS示例,包括主题、API集成、自定义路由和内容管理

💻 Ghost Hello World javascript

🟢 simple ⭐⭐

基础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
};

💻 Ghost 主题开发 handlebars

🟡 intermediate ⭐⭐⭐⭐

高级Ghost主题开发,包含Handlebars助手和自定义集成

⏱️ 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">&larr;</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">&rarr;</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 集成 javascript

🟡 intermediate ⭐⭐⭐⭐

完整的Ghost管理API和内容API集成示例,包含Webhooks和自定义端点

⏱️ 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
};