Files
intotheeast-com/docs/future/2026-07-08-view-unpublished-trips-when-logged-in.md

114 lines
4.5 KiB
Markdown

# 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/<draft-slug>` 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`.