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

> Discover how Ghost recommendations and discovery features work. Explore their service-repository architecture, engagement tracking, and cross-site synchronization powered by web mentions in this technical guide.

- Repository: [Ghost/Ghost](https://github.com/TryGhost/Ghost)
- Tags: deep-dive
- Published: 2026-05-18

---

**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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/ghost/core/core/server/models/recommendation-click-event.js) | Records member clicks on recommendations. |
| **RecommendationSubscribeEvent** | [`ghost/core/core/server/models/recommendation-subscribe-event.js`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main//.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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/recommendations.js) | `GET, POST, PUT, DELETE` | Admin | CRUD operations protected by `CanManageRecommendations` permission. |
| `/recommendations/` | [`recommendations-public.js`](https://github.com/TryGhost/Ghost/blob/main/recommendations-public.js) | `GET` | Public | Read-only list for theme rendering. |
| `/incoming-recommendations/` | [`incoming-recommendations.js`](https://github.com/TryGhost/Ghost/blob/main/incoming-recommendations.js) | `POST` | Public | Receives web-mention payloads from external sites. |
| `/members/recommendations/` | [`members.js`](https://github.com/TryGhost/Ghost/blob/main/members.js) | `GET` | Member | Personalized view with click/subscribe URLs. |
| `/members/recommendations/:id/click` | [`members.js`](https://github.com/TryGhost/Ghost/blob/main/members.js) | `POST` | Member | Registers click events. |
| `/members/recommendations/:id/subscribe` | [`members.js`](https://github.com/TryGhost/Ghost/blob/main/members.js) | `POST` | Member | Registers one-click-subscribe events. |

The `RecommendationController` in [`ghost/core/core/server/services/recommendations/service/recommendation-controller.ts`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/recommendation-list.tsx), [`add-recommendation-modal.tsx`](https://github.com/TryGhost/Ghost/blob/main/add-recommendation-modal.tsx), and [`edit-recommendation-modal.tsx`](https://github.com/TryGhost/Ghost/blob/main/edit-recommendation-modal.tsx) interact with the backend via the API client in [`apps/admin-x-framework/src/api/recommendations.ts`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main//.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

```bash
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

```typescript
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

```handlebars
{{#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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main//.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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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`](https://github.com/TryGhost/Ghost/blob/main/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.