Homepage Listing & Promotion Strategy
How to show tools on the main site page — handling new tool visibility, natural fade-out,
paid promotion placement, and the dedicated route that ties them together.
Builds on the Ranking Strategy but adds the promotional layer
on top of rank_score.
Why a Separate Homepage Route
Yes — build a dedicated GET /api/tools/homepage route.
The existing GET /api/tools is a browse/search endpoint: flat, paginated,
sortable. The homepage is fundamentally different — it is a curated surface
with multiple named sections, each with its own ordering logic and cache TTL.
Trying to represent that through query parameters on the browse route is a losing battle.
A dedicated route also lets the frontend fetch the entire homepage in a single request instead of four parallel calls, and lets you cache it as one unit with appropriate TTLs per section rather than fighting with CDN keys for filtered browse pages.
| Endpoint | Purpose | Order |
|---|---|---|
GET /api/tools |
Browse, search, filter — user-driven discovery | rank_score DESC (default), any sortBy |
GET /api/tools/homepage |
Main site page — curated, slot-based, multi-section | Fixed slot-layer architecture (see below) |
Slot-Layer Architecture
The homepage listing is not a flat ranked list. It is a stack of layers, each filling a fixed number of slots. Tools in a higher layer are always shown before tools in a lower layer. Within each layer, the ordering rule is different.
Admin Featured
featured = true by admin. Max 6–12 slots.
Ordered by featured_at DESC (most recently featured first) or by admin-set position.
Never mixed with other layers — always occupies its own dedicated section on the page.
Paid Promoted
promotion record (slot = 'homepage',
promoted_until > now()). Max 3–5 slots. Visually labelled
"Sponsored" so users know. Ordered by promoted_from ASC
(first to purchase wins the top promoted spot).
New & Rising
recency_bonus that decreases each week. Provides guaranteed
early exposure without permanently inflating rank. Max 8–12 slots.
Ordered by rank_score + recency_bonus DESC.
Ranked
rank_score DESC.
Tools already shown in layers 1–3 are excluded from this layer to avoid
duplication. Paginated — homepage shows first 24, "Load more" continues from here.
New Tool Step-Down Fade
The previous strategy used a binary recency boost: +30 for 30 days, then 0. This creates a cliff — a tool drops suddenly after 30 days. Replace it with a weekly step-down so the fade is gradual and natural.
recency_bonus is computed at recompute-job time (every 6 h), stored in the
tools.recency_bonus column, and added to rank_score only in the
Layer 3 query. It does not modify the base rank_score used
in Layer 4, so a new tool gets its early-stage spotlight without polluting the organic
ranked list.
A tool that earns high views and ratings during weeks 1–4 will have a naturally
high rank_score by the time recency_bonus hits 0 — it graduates
into Layer 4 at a strong position. A tool that gets no engagement gracefully sinks to its
true position. No manual intervention needed.
| Days since publish | recency_bonus | Effect |
|---|---|---|
| 0 – 7 | +50 | Strong launch window — appears high in Layer 3 |
| 8 – 14 | +30 | Still elevated — second week visibility |
| 15 – 21 | +15 | Fading — only high-quality tools hold top spots |
| 22 – 30 | +5 | Nearly organic — transition period |
| 30+ | 0 | Graduated — competes on rank_score alone |
Paid Promotion
Two promotion models exist: flat-fee time slots (pay X for Y days in slot Z) and bid-based auctions (highest current bid wins the slot in real time). Use flat-fee for now.
Bidding is complex, requires real-time slot resolution (kills caching), and creates anxiety for users who don't know if their bid will hold. Flat-fee is simple, predictable, and purchasable through your existing Stripe checkout flow. Move to bidding only if you have enough volume that slots are contested — that's a good problem to have.
Three promotion slot types. Users purchase a slot for a fixed duration. Slots are limited in number so they remain scarce and valuable.
Slot availability rule: when a user tries to purchase promotion,
the backend checks how many active promotions exist for that slot. If it is at the maximum,
the purchase is rejected — or optionally queued for the next available opening.
First to purchase wins the open slot; promoted_from ASC breaks ties.
Database Schema
New promotions table. Add recency_bonus column to
tools.
Route Design
Public endpoint. No auth required. Returns all four layers in a single structured response. The frontend renders each layer into its own UI section.
Response shape:
promoted layer is assembled at query time using a live DB lookup
(promoted_until > now()). The rest of the response is served from a
pre-computed cached view. This means only the promoted section bypasses the cache — see
the cache strategy below.
Query logic (pseudo-SQL):
Cache Strategy
The homepage has a mixed cacheability problem: featured and ranked layers are stable for hours; promoted tools expire at arbitrary times. The solution is to split the response at the CDN layer.
Require all promotions to start and end at hour boundaries
(e.g. midnight, 06:00, 12:00, 18:00). When a user purchases a promotion, the backend
rounds promoted_from up to the next hour boundary. This aligns promotion
expiry with the cache TTL cycle, so you never need to purge mid-TTL for a promotion
ending at a random minute.
Combined with a 1-hour TTL on the entire homepage response, you get: promoted slots that stay valid for the full TTL, expire cleanly at the boundary, and the next CDN miss picks up the updated promotion list. No real-time slot resolution needed.
| Section | TTL | Purge Trigger |
|---|---|---|
| /api/tools/homepage (full response) | 1h | Admin featured change, promotion purchase/expiry at hour boundary |
| /api/tools (browse) | 6h | Rank recompute job |
| /api/tools?category=* | 6h | Rank recompute job |
| /api/tools/:slug | 24h | Owner updates the tool |
promoted_from to the next hour boundary and let the TTL expire naturally.
This avoids cache stampedes when multiple promotions are purchased in quick succession.
Publisher Flow
What happens from the moment a user publishes their tool through to it appearing in each homepage layer:
-
Tool published — row inserted with
status = 'active',published_at = now().recency_bonusstarts at +50 (computed immediately at publish time, not waiting for the next recompute job). -
Immediately visible — the tool's own detail page (
/tools/:slug) is live and shareable right away. No cache issue here. -
Appears in Layer 3 (New & Rising) — within the next homepage cache TTL expiry (max 1 hour). The backend purges the homepage cache on publish so this can happen immediately if desired.
-
Appears in Layer 4 (Ranked) — after the next rank recompute job runs (within 6 hours). Until then it only appears in Layer 3.
-
Optional: user purchases promotion — goes through Stripe checkout, backend creates a
promotionsrow withpromoted_fromrounded to next hour boundary. Tool jumps to Layer 2 at that boundary. -
After 30 days —
recency_bonusreaches 0 at next recompute. Tool graduates out of Layer 3 and competes in Layer 4 onrank_scorealone. A tool that earned good views and ratings during weeks 1–4 will have a naturally strongrank_scoreby graduation.
Decision Summary
| Question | Decision | Control |
|---|---|---|
| Dedicated homepage route? | Yes — GET /api/tools/homepage |
Automatic |
| Featured layer | Admin-curated, max 12 slots, immediate purge on change | Admin only |
| Paid promotion model | Flat-fee time slots (not bidding), 3 slot types | User-purchased |
| Promotion timing | Start/end at hour boundaries to align with CDN TTL | System-enforced |
| New tool visibility | Layer 3 with step-down recency_bonus (+50→+30→+15→+5→0) | Recompute job |
| Natural fade mechanism | Weekly step-down, graduates into Layer 4 at day 30 | Automatic |
| Homepage cache TTL | 1 hour — matches promotion boundary alignment | CDN + recompute |
| Browse route unchanged? | Yes — GET /api/tools stays flat, rank_score sorted |
Automatic |