# View unpublished trips (drafts) on the frontend when logged in **Status:** 📋 Not started ## Motivation An unpublished trip (`published: false`) currently returns a hard **404** on its own route, even for the logged-in owner. Example: `http://localhost:8081/trips/denmark-2026` → `HTTP 404` (verified 2026-07-08, anonymous *and* authenticated). The owner should be able to preview a draft trip page at its real URL before publishing, while the public still gets a 404. The rest of the site is **already owner-aware** — the trip template, the trips listing, and the home page all render drafts to `grav.user.authenticated` (via `.published()` filters + `is-draft`/Draft badges). The only missing piece is the **direct route** to a draft's own page. ## Current behaviour — verified mechanism Traced through the Grav core running in the container (Grav 2.0.x): - `Page::routable()` (`system/src/Grav/Common/Page/Page.php`) returns: ```php return $this->routable && $this->published(); ``` So `published: false` ⇒ `routable()` is `false`, regardless of the `routable` flag. - `PagesProcessor.php:67` gates the request on exactly that: ```php if (!$page->routable()) { // build 404, fire onPageNotFound... } ``` - `PagesProcessor.php` ~line 80: after firing `onPageNotFound`, if a listener set `$event->page`, Grav serves **that** page directly with no further routable check: ```php if (isset($event->page)) { unset($this->container['page']); $this->container['page'] = $page = $event->page; } else { throw new RuntimeException('Page Not Found', 404); } ``` That last hook is the clean insertion point. ## Proposed approach — small custom plugin (~40 lines) Mirror the existing `user/plugins/cache-on-save/` custom-plugin pattern. Subscribe to `onPageNotFound` and, for authenticated users only, resolve the requested route including unpublished pages and hand it back: ```php public function onPageNotFound(Event $e) { $user = $this->grav['user']; if (!$user->authenticated) { return; // owners only — public still 404s } $route = $this->grav['uri']->path(); $page = $this->grav['pages']->find($route, true); // include unpublished if ($page && !$page->published()) { $e->page = $page; // serve the draft → 200 $e->stopPropagation(); } } ``` The page then renders with its normal template. Because the theme is already owner-aware, the trip page will display correctly for the logged-in owner. ## Decisions to make before building (brainstorm first) 1. **Scope of page types.** All unpublished pages, or just the trip tree (`/trips/*`)? Entries and stories already show inline as drafts on the owner's trip feed; do they also need standalone-route preview? Leaning: gate to trip/entry/story templates to avoid unintentionally exposing every draft everywhere. 2. **Draft banner.** Add a trip-level "Draft — not published" banner when viewing an unpublished trip (entry-level draft badges already exist; this is the trip equivalent). 3. **Non-existent vs. unpublished.** Ensure a genuinely missing route still 404s — the `find(..., true)` + `!published()` check already distinguishes them, but cover it in a test. ## The one real risk — page-cache leak to the public If Grav caches the 200 we serve to the owner and later hands it to an anonymous visitor, the "owners only" gate is defeated. **Verify, don't assume:** - Grav's Login plugin disables page caching for authenticated sessions by default. - The theme already serves owner-only draft *content* inline today, so this exposure is presumably mitigated somewhere already. Add an explicit **anonymous-request assertion** (draft route → 404 for anon, even after an authenticated hit warmed any cache). ## Testing Playwright spec: - Authenticated owner → `GET /trips/` returns 200 and renders the trip page. - Anonymous → same route returns 404. - Anonymous after an authenticated hit → still 404 (cache-leak guard). - Genuinely missing route → 404 for everyone. ## Effort **Low** — roughly half a day including the Playwright spec. Single custom plugin plus an optional small theme partial for the draft banner. ## References - Investigation session: 2026-07-08 ("hotfixes"). - Pattern to copy: `user/plugins/cache-on-save/`. - Related owner-aware theme logic: `templates/trip.html.twig` (`owner_can_edit`), `templates/trips.html.twig` (`is_owner`), `templates/home.html.twig`.