[MTM] Hide draft articles from the front page and mark drafts when viewed directly #141

Merged
jonny merged 1 commit from develop into main 2026-09-26 17:31:03 +02:00
13 changed files with 84 additions and 7 deletions
Showing only changes of commit a31c218784 - Show all commits

Hide draft articles from the front page and mark drafts when viewed directly

Add an Article::published() scope and use it on the front page (which
previously showed drafts), the /blog listing and the feeds. Draft
articles are still reachable at their URL, but now show a draft notice,
a "[Draft]" title prefix and a noindex robots meta tag.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Jonny Barnes 2026-09-26 16:20:50 +01:00
Signed by: jonny
SSH key fingerprint: SHA256:CTuSlns5U7qlD9jqHvtnVmfYV3Zwl2Z7WnJ4/dqOaL8

View file

@ -17,7 +17,7 @@ class ArticlesController extends Controller
*/ */
public function index(?int $year = null, ?int $month = null): View public function index(?int $year = null, ?int $month = null): View
{ {
$articles = Article::where('published', '1') $articles = Article::published()
->date($year, $month) ->date($year, $month)
->orderBy('updated_at', 'desc') ->orderBy('updated_at', 'desc')
->simplePaginate(5); ->simplePaginate(5);

View file

@ -15,7 +15,7 @@ class FeedsController extends Controller
*/ */
public function blogJson(): JsonResponse public function blogJson(): JsonResponse
{ {
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get(); $articles = Article::published()->latest('updated_at')->take(20)->get();
$data = [ $data = [
'version' => 'https://jsonfeed.org/version/1.1', 'version' => 'https://jsonfeed.org/version/1.1',
'title' => 'The JSON Feed for '.config('user.display_name').'’s blog', 'title' => 'The JSON Feed for '.config('user.display_name').'’s blog',
@ -89,7 +89,7 @@ class FeedsController extends Controller
*/ */
public function blogJf2(): JsonResponse public function blogJf2(): JsonResponse
{ {
$articles = Article::where('published', '1')->latest('updated_at')->take(20)->get(); $articles = Article::published()->latest('updated_at')->take(20)->get();
$items = []; $items = [];
foreach ($articles as $article) { foreach ($articles as $article) {
$items[] = [ $items[] = [

View file

@ -26,7 +26,7 @@ class FrontPageController extends Controller
->withCount(['webmentions AS reposts' => function ($query) { ->withCount(['webmentions AS reposts' => function ($query) {
$query->where('type', 'repost-of'); $query->where('type', 'repost-of');
}])->get(); }])->get();
$articles = Article::latest()->get(); $articles = Article::published()->latest()->get();
$bookmarks = Bookmark::latest()->with('tags')->get(); $bookmarks = Bookmark::latest()->with('tags')->get();
$likes = Like::latest()->get(); $likes = Like::latest()->get();

View file

@ -101,6 +101,14 @@ class Article extends Model
); );
} }
/**
* Scope a query to only include published (non-draft) articles.
*/
public function scopePublished(Builder $query): Builder
{
return $query->where('published', 1);
}
/** /**
* Scope a query to only include articles from a particular year/month. * Scope a query to only include articles from a particular year/month.
*/ */

File diff suppressed because one or more lines are too long

Binary file not shown.

File diff suppressed because one or more lines are too long

Binary file not shown.

View file

@ -63,3 +63,16 @@
} }
} }
@layer components {
.draft-notice {
margin-block-end: 2ex;
padding: 1ex 1em;
border: 2px dashed var(--clr-border);
border-radius: 8px;
background: light-dark(
oklch(95% 0.08 85deg),
oklch(30% 0.08 85deg)
);
}
}

View file

@ -1,7 +1,18 @@
@extends('master') @extends('master')
@section('title'){{ strip_tags($article->title) }} « @stop @section('title'){{ $article->published ? '' : '[Draft] ' }}{{ strip_tags($article->title) }} « @stop
@if (! $article->published)
@push('head')
<meta name="robots" content="noindex, nofollow">
@endpush
@endif
@section('content') @section('content')
@unless ($article->published)
<aside class="draft-notice" role="note">
<strong>Draft</strong> — this post hasn’t been published yet. It isn’t listed on the site or in feeds, so only people with this link can see it.
</aside>
@endunless
@include('templates.article', ['article' => $article]) @include('templates.article', ['article' => $article])
@stop @stop

View file

@ -22,6 +22,7 @@
@if (File::exists(public_path('gpg.key'))) @if (File::exists(public_path('gpg.key')))
<link rel="pgpkey" href="{{ config('app.url')}}/gpg.key"> <link rel="pgpkey" href="{{ config('app.url')}}/gpg.key">
@endif @endif
@stack('head')
</head> </head>
<body> <body>
<header> <header>

View file

@ -29,6 +29,39 @@ class ArticlesTest extends TestCase
$response->assertViewIs('articles.show'); $response->assertViewIs('articles.show');
} }
#[Test]
public function articles_page_does_not_list_drafts(): void
{
Article::factory()->create(['title' => 'Published Article']);
Article::factory()->create(['title' => 'Draft Article', 'published' => 0]);
$this->get('/blog')
->assertSee('Published Article')
->assertDontSee('Draft Article');
}
#[Test]
public function draft_article_is_viewable_and_marked_as_draft(): void
{
$article = Article::factory()->create(['published' => 0]);
$this->get($article->link)
->assertOk()
->assertSee('draft-notice', false)
->assertSee('<meta name="robots" content="noindex, nofollow">', false)
->assertSee('<title>[Draft] ', false);
}
#[Test]
public function published_article_is_not_marked_as_draft(): void
{
$article = Article::factory()->create();
$this->get($article->link)
->assertDontSee('draft-notice', false)
->assertDontSee('noindex', false);
}
#[Test] #[Test]
public function wrong_date_in_url_redirects_to_correct_date() public function wrong_date_in_url_redirects_to_correct_date()
{ {

View file

@ -31,4 +31,15 @@ class FrontPageTest extends TestCase
->assertSee('https://example.com') ->assertSee('https://example.com')
->assertSee('Like 1'); ->assertSee('Like 1');
} }
#[Test]
public function front_page_does_not_show_draft_articles(): void
{
Article::factory()->create(['title' => 'Published Article']);
Article::factory()->create(['title' => 'Draft Article', 'published' => 0]);
$this->get('/')
->assertSee('Published Article')
->assertDontSee('Draft Article');
}
} }