feat: restore sablog browse modes and clean imported HTML
Build and Deploy / verify (push) Successful in 40s
Build and Deploy / deploy (push) Successful in 36s

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:
gouki
2026-09-18 21:33:02 +00:00
co-authored by Cursor
parent 469160dc3f
commit ae47447062
98 changed files with 8618 additions and 901 deletions
@@ -6,6 +6,7 @@ namespace Plugins\Demo\HelloWorld;
use App\Blog\Support\PluginManager;
use App\Blog\Support\PluginServiceProvider;
use App\Filament\Resources\Posts\Schemas\PostForm;
use App\Models\Comment;
use App\Models\Post;
use App\Models\Setting;
@@ -53,8 +54,7 @@ class ServiceProvider extends PluginServiceProvider
// 5. 后台表单注入示例:给文章编辑页加一个演示字段(meta.demo_note)
$manager->addAction('filament.post_form', function (Schema $schema) {
$schema->components([
...$schema->getComponents(),
PostForm::appendToSidebar($schema, [
Section::make('Hello World 演示')
->collapsible()
->schema([
+3 -3
View File
@@ -1,9 +1,9 @@
{
"title": "SEO & GEO",
"version": "1.0.0",
"description": "完善 SEO(结构化数据/OG 标签)+ GEO(llms.txt / Markdown 导出,面向 AI 搜索引擎优化)",
"version": "1.0.1",
"description": "完善 SEO(结构化数据/OG 标签)。llms.txt 和 Markdown 导出由核心路由提供。",
"author": "LaraLog",
"type": "core",
"provider": "Plugins\\Neatstudio\\BlogSeo\\ServiceProvider",
"usage": "无需额外配置,启用即生效:\n- SEO:页面 title/description/keywords、OG/Twitter 标签、JSON-LD(BlogPosting/WebSite)自动输出\n- GEO:/llms.txt 站点摘要、/posts/{slug}.md 文章 Markdown 导出\n- /sitemap.xml、/robots.txt 由核心路由提供\n站点名/描述等在「博客设置 → 站点」配置,社交账号在「博客设置 → 社交账号」配置"
"usage": "无需额外配置,启用即生效:\n- SEO:页面 title/description/keywords、OG/Twitter 标签、JSON-LD(BlogPosting/WebSite)自动输出\n- GEO:核心路由 /llms.txt、/posts/{slug}.md(停用本插件后仍可用)\n- /sitemap.xml、/robots.txt 由核心路由提供\n站点名/描述等在「博客设置 → 站点」配置,社交账号在「博客设置 → 社交账号」配置"
}
@@ -1,11 +0,0 @@
<?php
declare(strict_types=1);
use Illuminate\Support\Facades\Route;
use Plugins\Neatstudio\BlogSeo\Http\SeoController;
// GEO:面向 AI 搜索引擎(LLM 爬虫)
Route::get('/llms.txt', [SeoController::class, 'llmsTxt'])->name('geo.llms');
Route::get('/posts/{slug}.md', [SeoController::class, 'postMarkdown'])->where('slug', '[A-Za-z0-9\-]+')->name('geo.post.markdown');
@@ -1,71 +0,0 @@
<?php
declare(strict_types=1);
namespace Plugins\Neatstudio\BlogSeo\Http;
use App\Models\Post;
use Illuminate\Http\Request;
use Illuminate\Support\Str;
class SeoController
{
/**
* GEO:/llms.txt — 面向 LLM 的站点索引
*/
public function llmsTxt()
{
$siteName = blog_setting('site_name', config('blog.name'));
$siteDescription = blog_setting('site_description', config('blog.description'));
$posts = Post::published()->latest('published_at')->limit(200)->get();
$lines = [];
$lines[] = '# '.$siteName;
$lines[] = '';
$lines[] = '> '.$siteDescription;
$lines[] = '';
$lines[] = '## 文章(Markdown 全文可访问 /posts/{slug}.md)';
$lines[] = '';
foreach ($posts as $post) {
$lines[] = '- ['.$post->title.']('.route('posts.show', $post->id).') — '.Str::limit(strip_tags($post->excerpt_or_fallback), 120);
}
return response(implode("\n", $lines)."\n")
->header('Content-Type', 'text/plain; charset=utf-8');
}
/**
* GEO:/posts/{slug}.md — 面向 AI 爬虫的纯 Markdown 导出
*/
public function postMarkdown(Request $request, string $slug)
{
$post = Post::query()->where('slug', $slug)->firstOrFail();
if ($post->status !== 'published') {
abort(404);
}
$body = $post->content_format === 'markdown' ? $post->content : $this->htmlToMarkdown($post->content);
$out = "# {$post->title}\n\n";
if ($post->excerpt) {
$out .= '> '.$post->excerpt."\n\n";
}
$out .= $body."\n";
return response($out)->header('Content-Type', 'text/markdown; charset=utf-8');
}
private function htmlToMarkdown(string $html): string
{
$converter = new \League\HTMLToMarkdown\HtmlConverter([
'strip_tags' => false,
'hard_break' => true,
]);
return $converter->convert($html);
}
}
@@ -2,20 +2,16 @@
declare(strict_types=1);
namespace Plugins\Neatstudio\BlogSeo;
use App\Blog\Support\PluginManager;
use App\Blog\Support\PluginServiceProvider;
use Plugins\Neatstudio\BlogSeo\Http\Middleware\SeoMiddleware;
use Plugins\Neatstudio\BlogSeo\Http\SeoController;
class ServiceProvider extends PluginServiceProvider
{
protected function boot(PluginManager $manager): void
{
$this->loadRoutes(__DIR__.'/../routes/web.php');
app('router')->pushMiddlewareToGroup('web', SeoMiddleware::class);
$manager->addFilter('seo.structured_data', function (array $data) {
@@ -2,15 +2,14 @@
declare(strict_types=1);
namespace Plugins\Neatstudio\Membership;
use App\Blog\Support\PluginManager;
use App\Blog\Support\PluginServiceProvider;
use App\Filament\Resources\Posts\Schemas\PostForm;
use App\Models\Post;
use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Toggle;
use Filament\Schemas\Components\Section;
use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -53,7 +52,7 @@ class ServiceProvider extends PluginServiceProvider
}, 10);
// 付费文章:[paid]...[/paid] —— 无权限访客替换为 teaser;有权限替换为付费内容块(避免原始标签裸露在页面)
$manager->addFilter('post.rendered', function (string $html, \App\Models\Post $post) {
$manager->addFilter('post.rendered', function (string $html, Post $post) {
$service = app(Services\MembershipService::class);
$user = auth()->user();
@@ -81,25 +80,18 @@ class ServiceProvider extends PluginServiceProvider
return $html;
}, 10);
// 后台文章表单注入:付费设置(meta.price 单篇价格 + meta.members_only 会员专享)
// 后台文章表单注入:右侧「访问设置」追加会员专享 / 价格
$manager->addAction('filament.post_form', function (Schema $schema) {
$schema->components([
...$schema->getComponents(),
Section::make('付费设置')
->description('需启用支付插件并配置支付渠道;0 = 免费')
->columns(3)
->collapsible()
->schema([
TextInput::make('meta.price')
->label('单篇价格(分)')
->numeric()
->minValue(0)
->default(0)
->helperText('例:9900 = ¥99;填 0 表示单篇免费'),
Toggle::make('meta.members_only')
->label('会员专享')
->helperText('整篇文章仅会员可读'),
]),
PostForm::appendToAccessSection($schema, [
Toggle::make('meta.members_only')
->label('会员专享')
->helperText('整篇文章仅会员可读'),
TextInput::make('meta.price')
->label('价格')
->numeric()
->minValue(0)
->default(0)
->helperText('单位:分,例 9900 = ¥99;0 表示单篇免费'),
]);
});