How Ghost Recommendations and Discovery Features Work: A Complete Technical Guide

Ghost recommendations and discovery features allow site owners to suggest external content to members, track engagement through clicks and one-click-subscribes, and synchronize recommendations across sites via web mentions, all implemented through a service-repository architecture in the TryGhost/Ghost core codebase.

The TryGhost/Ghost repository implements a full-stack recommendation system that enables publishers to curate external content for their audience. This feature set combines metadata enrichment, event tracking, and federated discovery mechanisms to create a two-way recommendation network between Ghost sites.

Core Data Models

The foundation of Ghost recommendations rests on three Bookshelf models that map to database tables created by migrations under core/server/data/migrations/versions/5.xx/.

Model File Path Purpose
Recommendation ghost/core/core/server/models/recommendation.js Stores URL, title, excerpt, image, favicon, and the one-click-subscribe flag.
RecommendationClickEvent ghost/core/core/server/models/recommendation-click-event.js Records member clicks on recommendations.
RecommendationSubscribeEvent ghost/core/core/server/models/recommendation-subscribe-event.js Records one-click-subscribe actions.

Service Layer Architecture

The RecommendationService in ghost/core/core/server/services/recommendations/service/recommendation-service.ts encapsulates all business logic. It receives collaborators via constructor injection, enabling the same code to run with either persistent Bookshelf repositories or in-memory repositories for testing.

The service handles six primary responsibilities:

  • CRUD Operations: Delegates to RecommendationRepository via addRecommendation, editRecommendation, deleteRecommendation, readRecommendation, and listRecommendations.
  • Metadata Enrichment: Calls RecommendationMetadataService during init and on a background timer (_updateRecommendationMetadata) to fetch Open Graph tags, titles, images, and favicons.
  • Well-Known Output: Updates /.well-known/recommendations.json via WellknownService.set after any change.
  • Web Mention Propagation: Uses MentionSendingService in sendMentionToRecommendation to notify target URLs of recommendation changes.
  • Feature Flag Management: Toggles the recommendations:enabled setting via updateRecommendationsEnabledSetting based on recommendation existence.
  • Event Tracking: Stores clicks and subscriptions via trackClicked and trackSubscribed in in-memory repositories.

Repository Pattern and Persistence

Ghost abstracts data access through the RecommendationRepository interface defined in ghost/core/core/server/services/recommendations/service/recommendation-repository.ts.

Two concrete implementations exist:

BookshelfRecommendationRepository (ghost/core/core/server/services/recommendations/service/bookshelf-recommendation-repository.ts) provides SQL database persistence with methods including getById, getByUrl, save, getAll, getPage, and getCount.

InMemoryRecommendationRepository (ghost/core/core/server/services/recommendations/service/in-memory-recommendation-repository.ts) provides volatile storage used exclusively in the test suite. The service layer never queries the database directly; it operates solely against the repository interface.

API Endpoints and Public Interface

The HTTP layer exposes recommendations through several endpoints in core/server/api/endpoints/:

Endpoint File Method Access Purpose
/recommendations/ recommendations.js GET, POST, PUT, DELETE Admin CRUD operations protected by CanManageRecommendations permission.
/recommendations/ recommendations-public.js GET Public Read-only list for theme rendering.
/incoming-recommendations/ incoming-recommendations.js POST Public Receives web-mention payloads from external sites.
/members/recommendations/ members.js GET Member Personalized view with click/subscribe URLs.
/members/recommendations/:id/click members.js POST Member Registers click events.
/members/recommendations/:id/subscribe members.js POST Member Registers one-click-subscribe events.

The RecommendationController in ghost/core/core/server/services/recommendations/service/recommendation-controller.ts instantiates services and forwards HTTP requests to the appropriate service methods.

Frontend Integration

Ghost surfaces recommendations through two distinct frontend interfaces.

Handlebars Theme Helper

The {{recommendations}} helper defined in ghost/core/core/frontend/helpers/recommendations.js fetches data from the public API endpoint and exposes it as JSON to themes. This allows theme developers to iterate over recommendations in templates.

React Admin UI

The staff-facing management interface resides in apps/admin-x-settings/src/components/settings/growth/recommendations/. Components such as recommendation-list.tsx, add-recommendation-modal.tsx, and edit-recommendation-modal.tsx interact with the backend via the API client in apps/admin-x-framework/src/api/recommendations.ts.

Discovery and Web Mention Protocol

Ghost implements a federated discovery mechanism that enables sites to recommend each other bidirectionally.

When a recommendation is created or deleted, the service invokes sendMentionToRecommendation to POST a web-mention to the target URL. External Ghost sites can then respond by POSTing to /incoming-recommendations/, creating inbound recommendations stored in the same database table. Simultaneously, WellknownService maintains /.well-known/recommendations.json to allow external services to discover recommended sites without UI scraping.

Background Metadata Management

During RecommendationService.init(), a delayed background job iterates through all stored recommendations and refreshes Open Graph metadata. This ensures titles, images, and the oneClickSubscribe flag remain current as external content changes. In production environments, this logic is scheduled to run periodically.

Practical Code Examples

Adding a Recommendation via API

curl -X POST https://my-ghost-site.com/ghost/api/admin/recommendations/ \
  -H "Authorization: Bearer <ADMIN_TOKEN>" \
  -H "Content-Type: application/json" \
  -d '{
        "url": "https://example.com/blog/post",
        "title": "Awesome post",
        "excerpt": "A short summary",
        "featuredImage": "https://example.com/og-image.png",
        "favicon": "https://example.com/favicon.ico",
        "oneClickSubscribe": true
      }'

Fetching Recommendations in the Admin UI

import { recommendations } from '@tryghost/admin-x-framework/api';

async function getMemberRecommendations(memberId: string) {
    const { data } = await recommendations.list({ member });
    return data;
}

Tracking Clicks from a Theme

{{#each recommendations}}
  <a href="{{url}}" data-recommendation-id="{{id}}" class="recommendation-link">
    {{title}}
  </a>
{{/each}}

<script>
document.addEventListener('click', async (e) => {
    const el = e.target.closest('.recommendation-link');
    if (!el) return;
    
    await fetch(`/ghost/api/members/recommendations/${el.dataset.recommendationId}/click/`, {
        method: 'POST',
        credentials: 'include'
    });
});
</script>

Summary

  • Ghost recommendations use a service-repository pattern with three Bookshelf models for data, events, and subscriptions.
  • The RecommendationService in recommendation-service.ts handles CRUD, Open Graph enrichment, web mentions, and event tracking.
  • Two repository implementations exist: Bookshelf for production SQL storage and In-Memory for testing.
  • Public and admin API endpoints provide read access and full CRUD capabilities, respectively.
  • Frontend integration includes a Handlebars helper for themes and a React admin interface for staff management.
  • Web mentions and /.well-known/recommendations.json enable federated discovery between Ghost sites.
  • Background jobs keep recommendation metadata synchronized with external sources.

Frequently Asked Questions

How does Ghost track which members click on recommendations?

Ghost tracks clicks through the RecommendationClickEvent model stored in ghost/core/core/server/models/recommendation-click-event.js. When a member clicks a recommendation link, the frontend sends a POST request to /members/recommendations/:id/click, which delegates to RecommendationService.trackClicked and persists the event with the member ID and recommendation ID.

What is the difference between the admin and public recommendation endpoints?

The admin endpoint (/recommendations/) in ghost/core/core/server/api/endpoints/recommendations.js supports full CRUD operations and requires the CanManageRecommendations permission. The public endpoint (/recommendations/) in ghost/core/core/server/api/endpoints/recommendations-public.js is read-only and exposes only active recommendations for theme rendering and member viewing.

How does Ghost keep recommendation metadata like images and titles up to date?

The RecommendationService initiates a background refresh during initialization that calls RecommendationMetadataService to fetch current Open Graph tags for every stored URL. This updates titles, excerpts, images, and the one-click-subscribe flag automatically without manual intervention.

Can external non-Ghost sites send recommendations to a Ghost site?

Yes. Ghost accepts inbound recommendations via the /incoming-recommendations/ endpoint defined in ghost/core/core/server/api/endpoints/incoming-recommendations.js. External sites can POST recommendation payloads to this endpoint, enabling Ghost sites to display recommendations from any web property that implements the web-mention protocol.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →