Ghost — лучшая CMS для серьёзных издателей: встроенные подписки, email-рассылки, монетизация платного контента. Astro — лучший фронтенд для читателей: PageSpeed 100, CDN, нулевой клиентский JS. Их связка — это профессиональный уровень издания контента в 2026 году.
Архитектура Ghost + Astro
┌─────────────────┐ ┌──────────────────────┐
│ Ghost Admin │ │ Cloudflare Pages │
│ │ │ │
│ Авторы пишут │──────▶│ Astro SSG (HTML) │
│ статьи в Ghost │ API │ PageSpeed: 100 │
│ │ │ CDN: глобально │
├─────────────────┤ └──────────────────────┘
│ Ghost Backend │
│ │ ┌──────────────────────┐
│ Node.js + MySQL │──────▶│ Webhook → Rebuild │
│ Подписки/Email │ │ Публикация = 30 сек │
│ Newsletter │ └──────────────────────┘
└─────────────────┘ Ghost хранит контент, управляет подписчиками, отправляет рассылки.
Astro показывает этот контент читателям с максимальной скоростью.
Установка Ghost
Ghost(Pro) — официальный облачный хостинг:
- Starter: $9/мес (1 500 подписчиков)
- Creator: $25/мес (неограниченно)
Self-hosted на VPS:
# Требуется Node.js 20+, MySQL 8, Nginx
npm install ghost-cli -g
mkdir /var/www/ghost && cd /var/www/ghost
ghost install Content API: получение данных из Ghost
# В Ghost Admin → Settings → Integrations → Custom Integration
# Скопируйте Content API Key // src/lib/ghost.ts
const GHOST_URL = import.meta.env.GHOST_URL; // https://your-blog.ghost.io
const GHOST_KEY = import.meta.env.GHOST_CONTENT_KEY; // API key из Admin
export interface GhostPost {
id: string;
slug: string;
title: string;
html: string;
excerpt: string;
feature_image: string | null;
published_at: string;
tags: { name: string; slug: string }[];
primary_author: { name: string; profile_image: string | null };
reading_time: number;
meta_title: string | null;
meta_description: string | null;
og_image: string | null;
}
export async function getAllPosts(): Promise<GhostPost[]> {
const res = await fetch(
`${GHOST_URL}/ghost/api/content/posts/?key=${GHOST_KEY}&limit=all&include=tags,authors&order=published_at desc`,
);
if (!res.ok) throw new Error(`Ghost API error: ${res.status}`);
const { posts } = await res.json();
return posts;
}
export async function getPostBySlug(slug: string): Promise<GhostPost | null> {
const res = await fetch(
`${GHOST_URL}/ghost/api/content/posts/slug/${slug}/?key=${GHOST_KEY}&include=tags,authors`,
);
if (!res.ok) return null;
const { posts } = await res.json();
return posts[0] ?? null;
}
export async function getAllPages() {
const res = await fetch(
`${GHOST_URL}/ghost/api/content/pages/?key=${GHOST_KEY}&limit=all`,
);
const { pages } = await res.json();
return pages;
}
export async function getSettings() {
const res = await fetch(`${GHOST_URL}/ghost/api/content/settings/?key=${GHOST_KEY}`);
const { settings } = await res.json();
return settings;
} Список статей (главная и блог)
---
// src/pages/blog/index.astro
import { getAllPosts } from '../../lib/ghost';
import BaseLayout from '../../layouts/BaseLayout.astro';
const posts = await getAllPosts();
---
<BaseLayout title="Блог" description="Все статьи">
<h1>Все публикации</h1>
<div class="posts-grid">
{
posts.map((post) => (
<article class="post-card">
{post.feature_image && (
<img
src={post.feature_image}
alt={post.title}
width="800"
height="450"
loading="lazy"
/>
)}
<div class="post-card__content">
<h2>
<a href={`/blog/${post.slug}/`}>{post.title}</a>
</h2>
<p>{post.excerpt}</p>
<div class="post-card__meta">
<time datetime={post.published_at}>
{new Date(post.published_at).toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
})}
</time>
<span>{post.reading_time} мин</span>
</div>
<ul class="tags">
{post.tags.map((tag) => (
<li>
<a href={`/tag/${tag.slug}/`}>{tag.name}</a>
</li>
))}
</ul>
</div>
</article>
))
}
</div>
</BaseLayout> Страница статьи
---
// src/pages/blog/[slug].astro
import { getAllPosts, getPostBySlug } from '../../lib/ghost';
import BaseLayout from '../../layouts/BaseLayout.astro';
export async function getStaticPaths() {
const posts = await getAllPosts();
return posts.map((post) => ({
params: { slug: post.slug },
props: { post },
}));
}
const { post } = Astro.props;
// SEO
const seoTitle = post.meta_title ?? post.title;
const seoDesc = post.meta_description ?? post.excerpt;
const seoImage = post.og_image ?? post.feature_image;
// JSON-LD
const articleSchema = {
'@context': 'https://schema.org',
'@type': 'Article',
headline: post.title,
description: post.excerpt,
datePublished: post.published_at,
author: {
'@type': 'Person',
name: post.primary_author.name,
},
image: post.feature_image ?? undefined,
};
---
<BaseLayout title={seoTitle} description={seoDesc} image={seoImage}>
<script type="application/ld+json" set:html={JSON.stringify(articleSchema)} />
<article>
{
post.feature_image && (
<img
src={post.feature_image}
alt={post.title}
width="1200"
height="630"
loading="eager"
fetchpriority="high"
/>
)
}
<header>
<h1>{post.title}</h1>
<div class="post-meta">
<span>
<img
src={post.primary_author.profile_image ?? ''}
alt=""
width="32"
height="32"
/>
{post.primary_author.name}
</span>
<time datetime={post.published_at}>
{
new Date(post.published_at).toLocaleDateString('ru-RU', {
year: 'numeric',
month: 'long',
day: 'numeric',
})
}
</time>
<span>{post.reading_time} мин чтения</span>
</div>
</header>
<!-- Ghost возвращает готовый HTML -->
<div class="ghost-content" set:html={post.html} />
<footer>
<ul>
{
post.tags.map((tag) => (
<li>
<a href={`/tag/${tag.slug}/`}>{tag.name}</a>
</li>
))
}
</ul>
</footer>
</article>
</BaseLayout> Webhook: Автопересборка при публикации
Когда автор публикует статью в Ghost → Astro должен пересобраться.
В Ghost Admin → Settings → Integrations → Custom Integration → Add webhook:
- Event:
Post published - Target URL:
https://api.cloudflare.com/client/v4/pages/webhooks/deploy_hooks/YOUR_HOOK_ID
Или через Cloudflare Pages → Deployments → Deploy hooks → Create hook. Скопируйте URL и вставьте в Ghost.
Ghost публикует статью
→ Ghost шлёт POST на Cloudflare deploy hook
→ Cloudflare запускает сборку Astro (~30-60 сек)
→ Новая статья доступна на CDN Стилизация Ghost HTML
Ghost возвращает HTML с kg-* классами (Koenig Editor). Добавьте CSS:
/* src/styles/ghost-content.css */
/* Карточки галерей */
.ghost-content .kg-gallery-container {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
gap: 8px;
}
/* Карточки закладок */
.ghost-content .kg-bookmark-card {
border: 1px solid var(--color-border);
border-radius: 8px;
overflow: hidden;
}
/* Видео */
.ghost-content .kg-video-card video {
width: 100%;
height: auto;
}
/* Код */
.ghost-content pre {
background: var(--color-surface-container);
border-radius: 8px;
padding: 16px;
overflow-x: auto;
}
/* Цитаты */
.ghost-content blockquote {
border-left: 4px solid var(--color-primary);
padding-left: 16px;
font-style: italic;
} Форма подписки Ghost Members
Ghost поддерживает нативные подписки. Форма через Ghost Portal:
---
// Загрузить Ghost Portal скрипт
---
<!-- В BaseLayout.astro -->
<script defer src="https://your-blog.ghost.io/public/member-attribution.min.js"></script>
<!-- Кнопка подписки — открывает Ghost Portal -->
<a href="#/portal/signup" class="btn btn--primary"> Подписаться на рассылку </a>
<!-- Или кастомная форма -->
<form
data-members-form="subscribe"
action="https://your-blog.ghost.io/members/api/send-magic-link/"
method="post"
>
<input name="email" type="email" placeholder="Ваш email" required />
<button type="submit">Подписаться</button>
<p data-members-error></p>
</form> Кэширование и производительность
Ghost API может быть медленным при большом количестве постов. Кэшируйте данные при сборке:
// src/lib/ghost.ts — с локальным кэшем для dev-режима
let postsCache: GhostPost[] | null = null;
export async function getAllPosts(): Promise<GhostPost[]> {
if (import.meta.env.DEV && postsCache) {
return postsCache; // Кэш в dev-режиме
}
const res = await fetch(`${GHOST_URL}/ghost/api/content/posts/?...`);
const { posts } = await res.json();
postsCache = posts;
return posts;
} Итог
Ghost + Astro — одна из самых сильных связок для медиапроектов. Редакторы получают удобный интерфейс Ghost с подписками и рассылками. Читатели получают молниеносный статический сайт на CDN. Webhook-автоматизация обеспечивает публикацию за 30-60 секунд. Если вы серьёзный издатель — это ваш стек.
Подробнее о Ghost как CMS — в статье Astro vs Ghost.