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
RecommendationRepositoryviaaddRecommendation,editRecommendation,deleteRecommendation,readRecommendation, andlistRecommendations. - Metadata Enrichment: Calls
RecommendationMetadataServiceduringinitand on a background timer (_updateRecommendationMetadata) to fetch Open Graph tags, titles, images, and favicons. - Well-Known Output: Updates
/.well-known/recommendations.jsonviaWellknownService.setafter any change. - Web Mention Propagation: Uses
MentionSendingServiceinsendMentionToRecommendationto notify target URLs of recommendation changes. - Feature Flag Management: Toggles the
recommendations:enabledsetting viaupdateRecommendationsEnabledSettingbased on recommendation existence. - Event Tracking: Stores clicks and subscriptions via
trackClickedandtrackSubscribedin 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
RecommendationServiceinrecommendation-service.tshandles 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.jsonenable 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →