# Fresh

## Route

- URL: `GET /discover/fresh`
- Controller: `App\Http\Controllers\Web\DiscoverController::fresh()`
- Service: `App\Services\ArtworkSearchService::discoverFresh()`

## What the page reads

Fresh is Meilisearch-backed, then hydrated from MySQL.

The page does not directly query `artworks` for ranking order.
It relies on the search index being up to date.

If the search-backed result comes back empty, the controller falls back to a direct MySQL query ordered by `published_at DESC, id DESC` so the page does not render blank while search is catching up.

## Search query

`discoverFresh()` uses:

- filter: `is_public = true AND is_approved = true`
- sort:
  - `published_at_ts:desc`

`published_at_ts` is a numeric timestamp field stored in the search document specifically so same-day uploads can be ordered correctly by hour and minute.

## Why the dedicated timestamp field exists

Historically, the index sorted by a date-only `created_at` string, which meant all uploads on the same calendar day could collapse to the same sort value.

The current implementation uses `published_at_ts` to preserve intra-day ordering and avoid newer uploads being buried behind older uploads from the same date.

## Page behavior

Fresh now uses the raw newest-first search result without any curated blending or grid filler injection.

That means:

- page 1 is not mixed with spotlight content
- page 1 is not padded with older trending artworks
- deeper pages follow the same ordering model as page 1

If older artworks appear near the top, the likely causes are stale search documents or stale cache, not intentional feed mixing.

If the page renders completely empty even though recent public artworks exist, the DB fallback should populate it. A blank page after that points to a real data visibility problem, not just search freshness.

## Data sources

Ranking eligibility depends on:

- `is_public`
- `is_approved`
- presence in Meilisearch
- `published_at_ts` in the indexed document

Hydration reads full rows from MySQL after the search query returns IDs.

When the fallback path is used, the page is served directly from MySQL and does not require Meilisearch for that request.

## Relevant jobs and schedules

Fresh does not have a dedicated score calculation job.
It depends on publication and indexing freshness.

Relevant active schedules:

- `artworks:publish-scheduled` every minute
- `skinbase:flush-redis-stats` every 5 minutes (not for ordering, but for displayed stats freshness)

Index freshness depends on:

- normal Scout indexing from artwork updates
- scheduled-publication indexing after an artwork transitions from scheduled to published
- manual/full imports after search-document schema changes

## Cache behavior

- Cache key: `discover.fresh.{page}`
- TTL: 300 seconds

## Notes

- Fresh is the page most sensitive to stale indexing because it is supposed to surface the latest publish action immediately.
- If an artwork is public in MySQL but absent from Fresh, the usual causes are:
  - it has not been indexed yet
  - app cache has not expired yet
  - the artwork is not actually public and approved at the same time