feat: Markdown 正文自动 TOC(锚点+折叠目录)+ 社交账号扩充国内平台(B站/小红书/抖音/视频号/公众号/其他)+ 侧边栏社交展示 + JSON-LD sameAs 全平台聚合 + 首页 h1(SEO 语义)

This commit is contained in:
ak
2026-08-11 23:49:19 +08:00
parent 489a145259
commit 56735804a1
26 changed files with 249 additions and 13 deletions
+7 -2
View File
@@ -32,9 +32,14 @@ class PostController extends Controller
->orderBy('created_at', (int) blog_setting('comment_order', 1) === 1 ? 'asc' : 'desc')
->paginate(20);
$contentHtml = $this->renderer->render($post);
$rendered = $this->renderer->renderWithToc($post);
return theme_view('show', compact('post', 'comments', 'contentHtml'));
return theme_view('show', [
'post' => $post,
'comments' => $comments,
'contentHtml' => $rendered['html'],
'toc' => $rendered['toc'],
]);
}
private function ensureReadable(Post $post): void
+37
View File
@@ -46,6 +46,43 @@ class PostContentRenderer
return app(\App\Blog\Support\PluginManager::class)->applyFilters('post.rendered', $html, $post);
}
/**
* 渲染并提取目录(TOC)。
*
* @return array{html: string, toc: array<int, array{id: string, text: string, level: int}>}
*/
public function renderWithToc(Post $post): array
{
$html = $this->render($post);
$toc = [];
$index = 0;
$html = preg_replace_callback(
'/<h([2-6])([^>]*)>(.*?)<\/h\1>/is',
function (array $m) use (&$toc, &$index): string {
$level = (int) $m[1];
$attrs = $m[2];
$text = trim(strip_tags($m[3]));
$id = 'toc-'.(++$index);
$toc[] = ['id' => $id, 'text' => $text, 'level' => $level];
// 已存在 id 则保留原 id,否则注入锚点
if (preg_match('/id="([^"]+)"/', $attrs, $idMatch)) {
$toc[array_key_last($toc)]['id'] = $idMatch[1];
return $m[0];
}
return '<h'.$level.$attrs.' id="'.$id.'">'.$m[3].'</h'.$level.'>';
},
$html
);
return ['html' => $html, 'toc' => $toc];
}
public function toHtml(string $markdown): string
{
return $this->converter->convert($markdown)->getContent();
+74
View File
@@ -0,0 +1,74 @@
<?php
declare(strict_types=1);
namespace App\Blog\Support;
use App\Models\Setting;
/**
* 社交账号聚合:后台「社交账号」Tab 配置 -> 前端展示 + SEO sameAs。
*/
class SocialLinks
{
public const PLATFORMS = [
'social_weibo' => '微博',
'social_zhihu' => '知乎',
'social_bilibili' => '哔哩哔哩',
'social_xiaohongshu' => '小红书',
'social_douyin' => '抖音',
'social_wechat_channel' => '视频号',
'social_wechat_public' => '微信公众号',
'social_github' => 'GitHub',
'social_twitter' => 'Twitter / X',
'social_facebook' => 'Facebook',
'social_email' => '邮箱',
];
/**
* 全部社交账号:['platform' => 平台名, 'value' => URL/名称, 'url' => 是否为外链]
*/
public static function all(): array
{
$items = [];
foreach (self::PLATFORMS as $key => $platform) {
$value = trim((string) Setting::get($key, ''));
if ($value === '') {
continue;
}
$items[] = [
'platform' => $platform,
'value' => $value,
'url' => filter_var($value, FILTER_VALIDATE_URL) ? $value : null,
];
}
// 其他平台(KeyValue
$other = Setting::get('social_other', []);
$other = is_array($other) ? $other : (json_decode((string) $other, true) ?: []);
foreach ($other as $platform => $value) {
if (trim((string) $value) === '') {
continue;
}
$items[] = [
'platform' => (string) $platform,
'value' => (string) $value,
'url' => filter_var((string) $value, FILTER_VALIDATE_URL) ? (string) $value : null,
];
}
return $items;
}
/**
* 可进 JSON-LD sameAs URL 列表。
*/
public static function sameAs(): array
{
return array_values(array_unique(array_filter(array_map(
fn (array $item) => $item['url'],
self::all()
))));
}
}
@@ -62,6 +62,8 @@ class SidebarComposer
];
}));
$view->with('socialLinks', \App\Blog\Support\SocialLinks::all());
$view->with('siteName', blog_setting('site_name', config('blog.name')));
$view->with('siteDescription', blog_setting('site_description', config('blog.description')));
$view->with('siteIcp', blog_setting('site_icp', config('blog.icp')));
+17 -1
View File
@@ -72,7 +72,13 @@ class BlogSettings extends Page
'social_twitter' => Setting::get('social_twitter', ''),
'social_facebook' => Setting::get('social_facebook', ''),
'social_zhihu' => Setting::get('social_zhihu', ''),
'social_bilibili' => Setting::get('social_bilibili', ''),
'social_xiaohongshu' => Setting::get('social_xiaohongshu', ''),
'social_douyin' => Setting::get('social_douyin', ''),
'social_wechat_channel' => Setting::get('social_wechat_channel', ''),
'social_wechat_public' => Setting::get('social_wechat_public', ''),
'social_email' => Setting::get('social_email', ''),
'social_other' => Setting::get('social_other', []),
]);
}
@@ -132,11 +138,21 @@ class BlogSettings extends Page
->columns(2)
->schema([
TextInput::make('social_weibo')->label('微博')->url()->placeholder('https://weibo.com/xxx'),
TextInput::make('social_zhihu')->label('知乎')->url()->placeholder('https://zhihu.com/people/xxx'),
TextInput::make('social_bilibili')->label('哔哩哔哩')->url()->placeholder('https://space.bilibili.com/xxx'),
TextInput::make('social_xiaohongshu')->label('小红书')->url()->placeholder('https://xiaohongshu.com/user/profile/xxx'),
TextInput::make('social_douyin')->label('抖音')->url()->placeholder('https://www.douyin.com/user/xxx'),
TextInput::make('social_wechat_channel')->label('视频号')->url()->placeholder('https://channels.weixin.qq.com/xxx'),
TextInput::make('social_wechat_public')->label('微信公众号')->placeholder('公众号名称 / 微信号(无外链时用于展示)'),
TextInput::make('social_github')->label('GitHub')->url()->placeholder('https://github.com/xxx'),
TextInput::make('social_twitter')->label('Twitter / X')->url()->placeholder('https://x.com/xxx'),
TextInput::make('social_facebook')->label('Facebook')->url()->placeholder('https://facebook.com/xxx'),
TextInput::make('social_zhihu')->label('知乎')->url()->placeholder('https://zhihu.com/people/xxx'),
TextInput::make('social_email')->label('邮箱')->email()->placeholder('contact@example.com'),
\Filament\Forms\Components\KeyValue::make('social_other')
->label('其他平台')
->keyLabel('平台名')
->valueLabel('链接 / 名称')
->columnSpanFull(),
]),
Tab::make('维护')
->schema([