feat: restore sablog browse modes and clean imported HTML
Keep the public site readable after import (nbsp, highlighter, wrapping code, compact pager) and finish the admin extras already in this tree: sticky levels, preview, page cache, and live llms.txt. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -34,11 +34,12 @@ class ArchiveController extends Controller
|
||||
$start = Carbon::create((int) $year, (int) $month, 1)->startOfMonth();
|
||||
$end = (clone $start)->endOfMonth();
|
||||
|
||||
$posts = Post::query()
|
||||
->whereBetween('published_at', [$start, $end])
|
||||
->published()
|
||||
->orderByDesc('published_at')
|
||||
->paginate((int) blog_setting('per_page', config('blog.per_page')));
|
||||
$posts = blog_paginate(
|
||||
Post::query()
|
||||
->whereBetween('published_at', [$start, $end])
|
||||
->published()
|
||||
->orderByDesc('published_at')
|
||||
);
|
||||
|
||||
return theme_view('list', compact('posts'))
|
||||
->with('archiveTitle', "{$year} 年 {$month} 月");
|
||||
|
||||
@@ -2,11 +2,9 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Blog\Controllers;
|
||||
|
||||
use App\Models\Category;
|
||||
use App\Models\Post;
|
||||
|
||||
class CategoryController extends Controller
|
||||
{
|
||||
@@ -14,10 +12,9 @@ class CategoryController extends Controller
|
||||
{
|
||||
$category = Category::query()->where('slug', $slug)->orWhere('id', (int) $slug)->firstOrFail();
|
||||
|
||||
$posts = $category->posts()
|
||||
->published()
|
||||
->latest('published_at')
|
||||
->paginate((int) blog_setting('per_page', config('blog.per_page')));
|
||||
$posts = blog_paginate(
|
||||
$category->posts()->published()->orderedForList()
|
||||
);
|
||||
|
||||
return theme_view('list', compact('category', 'posts'));
|
||||
}
|
||||
|
||||
@@ -2,12 +2,10 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Blog\Controllers;
|
||||
|
||||
use App\Models\Post;
|
||||
use Illuminate\Http\Response;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class FeedController extends Controller
|
||||
{
|
||||
@@ -54,7 +52,9 @@ class FeedController extends Controller
|
||||
foreach ($disallow as $path) {
|
||||
$lines[] = "Disallow: {$path}";
|
||||
}
|
||||
$lines[] = "Sitemap: ".url($sitemap);
|
||||
$lines[] = 'Sitemap: '.url($sitemap);
|
||||
$lines[] = 'Allow: /llms.txt';
|
||||
$lines[] = 'Allow: /posts/*.md';
|
||||
|
||||
return response(implode("\n", $lines)."\n")->header('Content-Type', 'text/plain; charset=utf-8');
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Blog\Controllers;
|
||||
|
||||
use App\Models\Post;
|
||||
@@ -11,11 +10,9 @@ class HomeController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$posts = Post::query()
|
||||
->published()
|
||||
->orderByDesc('is_sticky')
|
||||
->latest('published_at')
|
||||
->paginate((int) blog_setting('per_page', config('blog.per_page')));
|
||||
$posts = blog_paginate(
|
||||
Post::query()->published()->orderedForList()
|
||||
);
|
||||
|
||||
return theme_view('index', compact('posts'));
|
||||
}
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Blog\Controllers;
|
||||
|
||||
use App\Models\Post;
|
||||
use Illuminate\Support\Str;
|
||||
use League\HTMLToMarkdown\HtmlConverter;
|
||||
|
||||
class LlmsController extends Controller
|
||||
{
|
||||
/**
|
||||
* /llms.txt — live index for agents. Not a static file.
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$siteName = (string) blog_setting('site_name', config('blog.name'));
|
||||
$siteDescription = (string) blog_setting('site_description', config('blog.description'));
|
||||
|
||||
$posts = Post::query()
|
||||
->published()
|
||||
->latest('published_at')
|
||||
->limit(200)
|
||||
->get();
|
||||
|
||||
$lines = [
|
||||
'# '.$siteName,
|
||||
'',
|
||||
'> '.$siteDescription,
|
||||
'',
|
||||
'## Feeds',
|
||||
'',
|
||||
'- [RSS]('.route('feed.rss').')',
|
||||
'- [Sitemap]('.route('feed.sitemap').')',
|
||||
'- [llms.txt]('.route('feed.llms').')',
|
||||
'',
|
||||
'## Articles',
|
||||
'',
|
||||
];
|
||||
|
||||
foreach ($posts as $post) {
|
||||
if ($this->isRestricted($post)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$summary = Str::limit(trim(preg_replace('/\s+/', ' ', strip_tags($post->excerpt_or_fallback)) ?? ''), 120);
|
||||
$markdownUrl = $this->markdownUrl($post);
|
||||
$htmlUrl = $post->url;
|
||||
$title = $post->title;
|
||||
|
||||
$lines[] = $markdownUrl
|
||||
? '- ['.$title.']('.$markdownUrl.') — '.$summary.' ([html]('.$htmlUrl.'))'
|
||||
: '- ['.$title.']('.$htmlUrl.') — '.$summary;
|
||||
}
|
||||
|
||||
return response(implode("\n", $lines)."\n", 200, [
|
||||
'Content-Type' => 'text/plain; charset=utf-8',
|
||||
'Cache-Control' => 'no-store',
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* /posts/{slug}.md — full markdown for a published public post.
|
||||
*/
|
||||
public function postMarkdown(string $slug)
|
||||
{
|
||||
$post = Post::query()
|
||||
->where('slug', $slug)
|
||||
->orWhere('id', (int) $slug)
|
||||
->first();
|
||||
|
||||
abort_unless($post && $post->status === 'published' && ! $this->isRestricted($post), 404);
|
||||
|
||||
$body = $post->content_format === 'markdown'
|
||||
? (string) $post->content
|
||||
: $this->htmlToMarkdown((string) $post->content);
|
||||
|
||||
$out = '# '.$post->title."\n\n";
|
||||
if ($post->excerpt) {
|
||||
$out .= '> '.$post->excerpt."\n\n";
|
||||
}
|
||||
$out .= $body."\n";
|
||||
|
||||
return response($out, 200, [
|
||||
'Content-Type' => 'text/markdown; charset=utf-8',
|
||||
'Cache-Control' => 'no-store',
|
||||
]);
|
||||
}
|
||||
|
||||
private function isRestricted(Post $post): bool
|
||||
{
|
||||
if (filled($post->read_password)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return ! empty(($post->meta ?? [])['members_only']);
|
||||
}
|
||||
|
||||
private function markdownUrl(Post $post): ?string
|
||||
{
|
||||
$slug = trim((string) $post->slug);
|
||||
|
||||
if ($slug === '') {
|
||||
return route('geo.post.markdown', ['slug' => (string) $post->id]);
|
||||
}
|
||||
|
||||
return route('geo.post.markdown', ['slug' => $slug]);
|
||||
}
|
||||
|
||||
private function htmlToMarkdown(string $html): string
|
||||
{
|
||||
$converter = new HtmlConverter([
|
||||
'strip_tags' => false,
|
||||
'hard_break' => true,
|
||||
]);
|
||||
|
||||
return $converter->convert($html);
|
||||
}
|
||||
}
|
||||
@@ -2,17 +2,15 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Blog\Controllers;
|
||||
|
||||
use App\Blog\Services\PostContentRenderer;
|
||||
use App\Models\Post;
|
||||
use Illuminate\Pagination\Paginator;
|
||||
|
||||
class PostController extends Controller
|
||||
{
|
||||
public function __construct(private PostContentRenderer $renderer)
|
||||
{
|
||||
}
|
||||
public function __construct(private PostContentRenderer $renderer) {}
|
||||
|
||||
public function show(string $id, ?string $page = null)
|
||||
{
|
||||
@@ -21,13 +19,16 @@ class PostController extends Controller
|
||||
->orWhere('slug', $id)
|
||||
->first();
|
||||
|
||||
abort_unless($post && $post->status === 'published', 404);
|
||||
abort_unless($post, 404);
|
||||
|
||||
$canPreview = $this->canPreviewUnpublished();
|
||||
abort_unless($post->status === 'published' || $canPreview, 404);
|
||||
|
||||
$this->ensureReadable($post);
|
||||
|
||||
// sablog 式 /show-{id}-{page}.shtml 的评论分页
|
||||
if ($page !== null) {
|
||||
\Illuminate\Pagination\Paginator::currentPageResolver(fn () => max(1, (int) $page));
|
||||
Paginator::currentPageResolver(fn () => max(1, (int) $page));
|
||||
}
|
||||
|
||||
// 浏览量由 /track-view 端点统计(页面整页缓存下控制器不执行,避免双计)
|
||||
@@ -44,9 +45,17 @@ class PostController extends Controller
|
||||
'comments' => $comments,
|
||||
'contentHtml' => $rendered['html'],
|
||||
'toc' => $rendered['toc'],
|
||||
'isPreview' => $post->status !== 'published' && $canPreview,
|
||||
]);
|
||||
}
|
||||
|
||||
private function canPreviewUnpublished(): bool
|
||||
{
|
||||
$user = auth()->user();
|
||||
|
||||
return $user && ($user->isAdmin() || $user->hasRole('editor'));
|
||||
}
|
||||
|
||||
private function ensureReadable(Post $post): void
|
||||
{
|
||||
if (! $post->read_password) {
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Blog\Controllers;
|
||||
|
||||
use App\Models\Post;
|
||||
@@ -13,11 +12,9 @@ class SearchController extends Controller
|
||||
{
|
||||
$keyword = trim(request('q', request('keyword', '')));
|
||||
|
||||
$posts = Post::query()
|
||||
->published()
|
||||
->search($keyword)
|
||||
->orderByDesc('published_at')
|
||||
->paginate((int) blog_setting('per_page', config('blog.per_page')));
|
||||
$posts = blog_paginate(
|
||||
Post::query()->published()->search($keyword)->orderedForList()
|
||||
);
|
||||
|
||||
return theme_view('search', compact('posts', 'keyword'));
|
||||
}
|
||||
|
||||
@@ -2,17 +2,17 @@
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
|
||||
namespace App\Blog\Controllers;
|
||||
|
||||
use App\Models\Post;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Spatie\Tags\Tag;
|
||||
|
||||
class TagController extends Controller
|
||||
{
|
||||
public function index()
|
||||
{
|
||||
$counts = \Illuminate\Support\Facades\DB::table('taggables')
|
||||
$counts = DB::table('taggables')
|
||||
->selectRaw('tag_id, COUNT(*) as total')
|
||||
->where('taggable_type', Post::class)
|
||||
->groupBy('tag_id')
|
||||
@@ -37,11 +37,12 @@ class TagController extends Controller
|
||||
|
||||
abort_unless($tag, 404);
|
||||
|
||||
$posts = Post::query()
|
||||
->whereHas('tags', fn ($q) => $q->where('tags.id', $tag->id))
|
||||
->published()
|
||||
->latest('published_at')
|
||||
->paginate((int) blog_setting('per_page', config('blog.per_page')));
|
||||
$posts = blog_paginate(
|
||||
Post::query()
|
||||
->whereHas('tags', fn ($q) => $q->where('tags.id', $tag->id))
|
||||
->published()
|
||||
->orderedForList()
|
||||
);
|
||||
|
||||
return theme_view('tag', compact('tag', 'posts'));
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user