docs(future): plan — view unpublished trips on frontend when logged in
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,113 @@
|
||||
# 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`.
|
||||
Reference in New Issue
Block a user