diff --git a/CHANGELOG.md b/CHANGELOG.md index e94476e..e39c017 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## 2026-09-18 + +- Homepage / lists have SaBlog-style **标准 | 列表** browse modes (`home_view_mode`, `?view=`, cookie). +- Pagination is compact (`第 x / y 页` + fewer page chips) and the content column uses CSS grid so it no longer sits under the sidebar. +- Imported SaBlog HTML no longer shows literal ` `; highlighter / stacked docker `
`.
+- `php artisan sablog:fix-content --all` can persist that cleanup. Code no longer stretches the page.
+- Schema snapshot: `docs/database/mysql-schema-20260918-211824.sql`.
+- Frontend RSS / Sitemap / llms.txt links now open in a new tab.
+- `/llms.txt` is a core live route (plus `/posts/{slug}.md`) so agents can crawl new public posts without a static file.
+- Fixed edit-post crash: comments relation manager now uses Filament v5 `Filament\Actions\Action`.
+- Posts now have sticky levels 0–3 (none / light / strong / headline). Headline always stays at the top of public lists.
+- Edit and list screens have a **预览** button; admin/editor can open unpublished posts on the public site.
+- Admin top bar **网站** opens the frontend in a new tab. Sidebar **管理** now has **页面缓存** and **语言**.
+- Schema snapshot: `docs/database/mysql-schema-20260918-205818.sql`.
+- Post create/edit form is now left content 2 : right tools 1, locked for PC at every breakpoint (not only `lg`).
+- Slug is full width; category, format, and status share one row as single-choice buttons; cover sits above the taller body editor.
+- Right column: publish settings, then access settings (password + membership price fields).
+- Cover / media picker preview is a small thumbnail (max 220×124), not full column width.
+- Schema snapshot: `docs/database/mysql-schema-20260918-203827.sql`.
+
## 2026-09-17
- Admin top-right now shows the current environment (`local` / `prod`) and a **写文章** shortcut to create a post.
diff --git a/Fetures.md b/Fetures.md
index 3edd8e4..2d93974 100644
--- a/Fetures.md
+++ b/Fetures.md
@@ -12,10 +12,31 @@ Modern blog migrated from SaBlog-X: Filament admin, themes, plugins, sablog impo
See `README.md` and `docs/` for install, import, plugins, and themes.
+## Post editor layout
+
+- Create/edit post uses **left 2 : right 1** on PC at every breakpoint (not only `lg`).
+- Left: title, full-width slug, category + format + status on one row (single-choice buttons), cover thumbnail (not full-width), then a tall body editor.
+- Right group 1: published at, sticky level (none / light / strong / headline), comment status, excerpt, keywords.
+- Right group 2: read password; membership plugin adds members-only + price here.
+- Plugins must inject via `PostForm::appendToSidebar()` / `appendToAccessSection()` so they do not break the 2:1 grid.
+
+## Sticky levels
+
+- `sticky_level`: 0 none, 1 light, 2 strong, 3 headline. Headline always stays first on home, category, tag, and search lists.
+- Saving a post keeps `is_sticky` in sync (`true` when level > 0) so older themes still work.
+- List badges show the Chinese label, not a generic “sticky”.
+
+## Post preview
+
+- Edit page and post table have **预览**, opening `/show-{id}.shtml` in a new tab.
+- Filament v5 actions are `Filament\Actions\Action`. The comments relation manager on the edit page must boot in tests, because a stale `Tables\Actions\Action` import only fails when that Livewire table renders.
+- Guests still get 404 for drafts. Admin and editor can open unpublished posts and see a preview banner.
+
## Admin chrome
- Top-right badge shows the current app environment (`local` / `prod` / others).
-- Top-right **写文章** button jumps to `/admin/posts/create`.
+- Top-right **网站** opens the public site in a new tab; **写文章** jumps to `/admin/posts/create`.
+- Sidebar **管理** includes **页面缓存** (view URLs, hit rate, flush) and **语言** (current locale + available packs). Article translations are not built yet; the language page says so.
- Sidebar nav is compacted (Filament default `gap-y-7` / `py-8` is too tall for this site).
## Hello World plugin
@@ -24,6 +45,27 @@ See `README.md` and `docs/` for install, import, plugins, and themes.
- Plugin boot / hook failures are isolated so one plugin cannot take down the whole admin.
- `/hello-world` stays available even if `hello_world_logs` is missing.
+## Frontend feeds
+
+- Header/footer **RSS / Sitemap / llms.txt** open in a new tab (`target="_blank"`).
+- `/llms.txt` is a core route (not a static file). Each request lists current published public posts so new articles show up immediately.
+- Full text for agents: `/posts/{slug}.md`. Password / members-only posts are omitted.
+- These machine-readable paths are excluded from page cache.
+
+## Homepage list / full mode
+
+- Same switch as old SaBlog: **浏览模式: 标准 | 列表**.
+- `list` = title + excerpt cards. `full` = full rendered body on the home / category / tag / search lists.
+- Query `?view=list` or `?view=full` wins, then cookie `ll_home_view`, then setting `home_view_mode`.
+- Page cache key includes the view mode so the two layouts are not mixed.
+
+## Pagination and imported HTML
+
+- Public pagers use `onEachSide(1)` plus “第 x / y 页”, and the main column is CSS grid `minmax(0, 1fr)` so page chips stay in the content column.
+- `SablogHtmlNormalizer` strips leftover ` `, turns SyntaxHighlighter `codeText` / `dp-*` lists into ``, and wraps stacked docker/shell `` lines.
+- Render path always normalizes, so already-imported posts look right without rewriting the database. Persist with `php artisan sablog:fix-content --all`.
+- Code blocks wrap / scroll inside the column instead of stretching the page.
+
## Media library (xpier)
- Package: `xpier/filament-media-library` (admin Media Library + MediaPicker).
diff --git a/README.md b/README.md
index 79484e0..00ac229 100644
--- a/README.md
+++ b/README.md
@@ -5,8 +5,11 @@
- **脚本迁移**:`php artisan sablog:import` 一键从老库迁入全部数据(文章/分类/评论/标签/友链/用户/附件),保留原始 ID,老 URL 全部 301 兼容
- **多主题**:`themes/` 目录即皮肤,后台一键切换/上传 ZIP;内置 sablog 经典风 + modern 简约风两套
- **插件系统**:`plugins/` 目录 + 钩子系统(action/filter),ZIP 安装 + 可选远程市场;启用时自动跑插件迁移
-- **后台顶栏**:右上角显示当前环境(local/prod)+ 一键写文章
-- **内置插件**:SEO & GEO(JSON-LD/OG/llms.txt/Markdown 导出)、AI 评论审核 + 内容润色(Workerman 异步)、支付(支付宝/微信/沙箱)、会员(套餐订阅 + 付费文章)
+- **后台顶栏**:右上角显示当前环境(local/prod)+ 打开网站 + 一键写文章
+- **文章编辑**:左正文 2 : 右功能 1(Slug 整行,分类/格式/状态同一行,封面在正文上方)
+- **内置插件**:SEO & GEO(JSON-LD/OG)、AI 评论审核 + 内容润色(Workerman 异步)、支付(支付宝/微信/沙箱)、会员(套餐订阅 + 付费文章)
+- **首页浏览模式**:和老 sablog 一样可切 **标准(全文) / 列表(摘要)**
+- **Agent 索引**:`/llms.txt`、`/posts/{slug}.md` 是核心路由,随已发布公开文章实时生成
- **附件全走 S3**:R2 / COS / OSS 兼容,新上传零落盘,`attachments:sync-s3` 一键同步老附件(大文件 Multipart)
## 技术栈
@@ -59,6 +62,8 @@ php artisan sablog:import ... --convert-markdown
# 事后单独转换 / 同步附件到 S3
php artisan content:convert --all
+# 已导入正文里的 / 高亮块 / docker 分行,可先渲染时自动清洗,也可落库:
+php artisan sablog:fix-content --all
S3_ACCESS_KEY=... S3_BUCKET=... S3_ENDPOINT=... php artisan attachments:sync-s3 --legacy-dir=/path/to/old/attachments
```
@@ -118,7 +123,7 @@ php artisan workerman:serve stop
- 匿名 GET 页面整页缓存(首页/列表/文章/归档/标签等),TTL 默认 300 秒(`PAGE_CACHE_TTL`),命中时零数据库查询
- 文章浏览量由独立 `/track-view` 端点统计,不受整页缓存影响(实时)
-- 后台「缓存管理」:清空页面/侧边栏/设置/全部缓存、重建编译缓存
+- 后台「管理 → 页面缓存」:查看已缓存地址、命中率,清空前台或全部缓存
- 侧边栏数据与站点设置另有 1 小时缓存
## 对外 API
diff --git a/REMINDER.md b/REMINDER.md
new file mode 100644
index 0000000..0b37848
--- /dev/null
+++ b/REMINDER.md
@@ -0,0 +1,7 @@
+# Reminders
+
+## Filament v5 actions
+
+- Table / header / record actions use `Filament\Actions\Action`.
+- `Filament\Tables\Actions\Action` does not exist. A `use` line only blows up when the class is actually called (for example the comments relation manager on the edit-post page).
+- After changing Filament pages, boot the Livewire component, not only `GET` the HTML.
diff --git a/app/Blog/Controllers/ArchiveController.php b/app/Blog/Controllers/ArchiveController.php
index c4f3885..38dd3b3 100644
--- a/app/Blog/Controllers/ArchiveController.php
+++ b/app/Blog/Controllers/ArchiveController.php
@@ -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} 月");
diff --git a/app/Blog/Controllers/CategoryController.php b/app/Blog/Controllers/CategoryController.php
index d6c4d72..ceef636 100644
--- a/app/Blog/Controllers/CategoryController.php
+++ b/app/Blog/Controllers/CategoryController.php
@@ -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'));
}
diff --git a/app/Blog/Controllers/FeedController.php b/app/Blog/Controllers/FeedController.php
index 1c0e643..cf76752 100644
--- a/app/Blog/Controllers/FeedController.php
+++ b/app/Blog/Controllers/FeedController.php
@@ -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');
}
diff --git a/app/Blog/Controllers/HomeController.php b/app/Blog/Controllers/HomeController.php
index 5951d22..9a76137 100644
--- a/app/Blog/Controllers/HomeController.php
+++ b/app/Blog/Controllers/HomeController.php
@@ -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'));
}
diff --git a/app/Blog/Controllers/LlmsController.php b/app/Blog/Controllers/LlmsController.php
new file mode 100644
index 0000000..00b3d1b
--- /dev/null
+++ b/app/Blog/Controllers/LlmsController.php
@@ -0,0 +1,120 @@
+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);
+ }
+}
diff --git a/app/Blog/Controllers/PostController.php b/app/Blog/Controllers/PostController.php
index b1aa016..bade444 100644
--- a/app/Blog/Controllers/PostController.php
+++ b/app/Blog/Controllers/PostController.php
@@ -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) {
diff --git a/app/Blog/Controllers/SearchController.php b/app/Blog/Controllers/SearchController.php
index a3026ee..dbd4898 100644
--- a/app/Blog/Controllers/SearchController.php
+++ b/app/Blog/Controllers/SearchController.php
@@ -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'));
}
diff --git a/app/Blog/Controllers/TagController.php b/app/Blog/Controllers/TagController.php
index 28073d6..0679d02 100644
--- a/app/Blog/Controllers/TagController.php
+++ b/app/Blog/Controllers/TagController.php
@@ -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'));
}
diff --git a/app/Blog/Services/PostContentRenderer.php b/app/Blog/Services/PostContentRenderer.php
index 5ab8f8e..6e3390f 100644
--- a/app/Blog/Services/PostContentRenderer.php
+++ b/app/Blog/Services/PostContentRenderer.php
@@ -6,6 +6,7 @@ declare(strict_types=1);
namespace App\Blog\Services;
use App\Blog\Support\MarkdownMediaEmbed;
+use App\Blog\Support\SablogHtmlNormalizer;
use App\Models\Post;
use Illuminate\Support\Facades\Cache;
use League\CommonMark\Environment\Environment;
@@ -38,7 +39,7 @@ class PostContentRenderer
public function render(Post $post): string
{
$embed = app(MarkdownMediaEmbed::class);
- $raw = (string) $post->content;
+ $raw = app(SablogHtmlNormalizer::class)->normalize((string) $post->content);
$content = $post->content_format === 'markdown'
? $this->toHtml($embed->expandMarkdown($raw))
diff --git a/app/Blog/Support/HomeViewMode.php b/app/Blog/Support/HomeViewMode.php
new file mode 100644
index 0000000..6cd94e7
--- /dev/null
+++ b/app/Blog/Support/HomeViewMode.php
@@ -0,0 +1,69 @@
+
+ */
+ public static function options(): array
+ {
+ return [
+ self::LIST => '列表',
+ self::FULL => '标准',
+ ];
+ }
+
+ public static function isValid(mixed $value): bool
+ {
+ return is_string($value) && in_array($value, [self::LIST, self::FULL], true);
+ }
+
+ public static function resolve(?Request $request = null): string
+ {
+ $request ??= request();
+
+ foreach ([
+ $request->query('view'),
+ $request->cookie(self::COOKIE),
+ blog_setting('home_view_mode', config('blog.home_view_mode', self::LIST)),
+ ] as $value) {
+ if (self::isValid($value)) {
+ return $value;
+ }
+ }
+
+ return self::LIST;
+ }
+
+ public static function current(): string
+ {
+ $request = request();
+ $mode = self::resolve($request);
+
+ if (self::isValid($request->query('view'))) {
+ cookie()->queue(cookie(self::COOKIE, (string) $request->query('view'), 60 * 24 * 30));
+ }
+
+ return $mode;
+ }
+
+ public static function url(string $mode): string
+ {
+ return request()->fullUrlWithQuery(['view' => $mode]);
+ }
+}
diff --git a/app/Blog/Support/PageCacheMiddleware.php b/app/Blog/Support/PageCacheMiddleware.php
index 2250490..96c84e2 100644
--- a/app/Blog/Support/PageCacheMiddleware.php
+++ b/app/Blog/Support/PageCacheMiddleware.php
@@ -45,7 +45,7 @@ class PageCacheMiddleware
'body' => $response->getContent(),
'type' => $response->headers->get('Content-Type') ?: 'text/html; charset=utf-8',
], $ttl);
- self::rememberKey($key);
+ self::rememberKey($key, $request->fullUrl());
}
return $response;
@@ -53,8 +53,8 @@ class PageCacheMiddleware
public static function flush(): void
{
- foreach ((array) Cache::get(self::TAG.'_keys', []) as $key) {
- Cache::forget($key);
+ foreach (array_keys((array) Cache::get(self::TAG.'_keys', [])) as $key) {
+ Cache::forget((string) $key);
}
Cache::forget(self::TAG.'_keys');
@@ -88,16 +88,50 @@ class PageCacheMiddleware
Cache::forget(self::TAG.'_misses');
}
+ /**
+ * @return list
+ */
+ public static function entries(): array
+ {
+ $entries = [];
+
+ foreach ((array) Cache::get(self::TAG.'_keys', []) as $key => $meta) {
+ $entries[] = [
+ 'key' => (string) $key,
+ 'url' => is_array($meta) ? (string) ($meta['url'] ?? $key) : (string) $key,
+ 'cached_at' => is_array($meta) ? (string) ($meta['cached_at'] ?? '') : '',
+ ];
+ }
+
+ usort($entries, fn (array $left, array $right): int => strcmp($right['cached_at'], $left['cached_at']));
+
+ return $entries;
+ }
+
+ public static function forget(string $key): void
+ {
+ Cache::forget($key);
+ $keys = (array) Cache::get(self::TAG.'_keys', []);
+ unset($keys[$key]);
+ Cache::put(self::TAG.'_keys', $keys, now()->addDay());
+ }
+
private static function increment(string $metric): void
{
$key = self::TAG.'_'.$metric;
Cache::put($key, (int) Cache::get($key, 0) + 1, now()->addDay());
}
- private static function rememberKey(string $key): void
+ private static function rememberKey(string $key, ?string $url = null): void
{
$keys = (array) Cache::get(self::TAG.'_keys', []);
- $keys[$key] = true;
+ $previous = $keys[$key] ?? [];
+
+ $keys[$key] = [
+ 'url' => $url ?: (is_array($previous) ? (string) ($previous['url'] ?? '') : ''),
+ 'cached_at' => now()->toDateTimeString(),
+ ];
+
Cache::put(self::TAG.'_keys', $keys, now()->addDay());
}
@@ -117,16 +151,24 @@ class PageCacheMiddleware
return false;
}
- // 动态端点
+ // 动态端点、订阅和 agent 索引不进整页缓存,新文章要马上能看到
if (str_starts_with($path, 'track-view')) {
return false;
}
+ if (in_array($path, ['rss.xml', 'rss.php', 'sitemap.xml', 'robots.txt', 'llms.txt'], true)) {
+ return false;
+ }
+
+ if (str_ends_with($path, '.md')) {
+ return false;
+ }
+
return true;
}
private function key(Request $request): string
{
- return self::TAG.':'.md5($request->fullUrl());
+ return self::TAG.':'.md5($request->fullUrl().'|view:'.HomeViewMode::resolve($request));
}
}
diff --git a/app/Blog/Support/SablogHtmlNormalizer.php b/app/Blog/Support/SablogHtmlNormalizer.php
new file mode 100644
index 0000000..b2d40cb
--- /dev/null
+++ b/app/Blog/Support/SablogHtmlNormalizer.php
@@ -0,0 +1,376 @@
+
+ */
+ private function languageAliases(): array
+ {
+ return [
+ 'js' => 'javascript',
+ 'javascript' => 'javascript',
+ 'xml' => 'xml',
+ 'html' => 'xml',
+ 'xhtml' => 'xml',
+ 'css' => 'css',
+ 'php' => 'php',
+ 'sql' => 'sql',
+ 'java' => 'java',
+ 'py' => 'python',
+ 'python' => 'python',
+ 'c#' => 'csharp',
+ 'cs' => 'csharp',
+ 'bash' => 'bash',
+ 'shell' => 'bash',
+ 'sh' => 'bash',
+ 'text' => 'text',
+ ];
+ }
+
+ public function normalize(string $content): string
+ {
+ if ($content === '') {
+ return '';
+ }
+
+ $content = $this->decodeNbsp($content);
+
+ if (! preg_match('/<\s*(div|p|ol|ul|li|pre|code|span|br|table)\b/i', $content)) {
+ return $this->recoverFlattenedShell($content);
+ }
+
+ try {
+ $content = $this->transformHtml($content);
+ } catch (Throwable) {
+ // Keep the decoded text when HTML parsing fails.
+ }
+
+ return $this->recoverFlattenedShell($content);
+ }
+
+ public function toMarkdown(string $html, HtmlConverter $converter, PostContentRenderer $renderer): string
+ {
+ return $converter->convert($renderer->toMarkdownSafeTokens($this->normalize($html)));
+ }
+
+ public function plainText(string $content): string
+ {
+ $text = $this->decodeNbsp(strip_tags($content));
+ $text = html_entity_decode($text, ENT_QUOTES | ENT_HTML5, 'UTF-8');
+ $text = str_replace("\xC2\xA0", ' ', $text);
+ $text = preg_replace('/\s+/u', ' ', $text) ?? $text;
+
+ return trim($text);
+ }
+
+ public function decodeNbsp(string $content): string
+ {
+ $content = str_replace(
+ [' ', ' ', ' ', ' ', ' ', "\xC2\xA0"],
+ ' ',
+ $content
+ );
+
+ return str_replace(' ', ' ', $content);
+ }
+
+ private function transformHtml(string $html): string
+ {
+ $document = $this->loadDocument($html);
+ $xpath = new DOMXPath($document);
+
+ $this->convertHighlighterBlocks($document, $xpath);
+ $this->convertHighlighterLists($document, $xpath);
+ $this->convertStackedCodeDivs($document);
+ $this->removeEmptyBlocks($xpath);
+
+ return $this->innerHtml($document);
+ }
+
+ private function convertHighlighterBlocks(DOMDocument $document, DOMXPath $xpath): void
+ {
+ $nodes = [];
+ foreach ($xpath->query('//*[contains(concat(" ", normalize-space(@class), " "), " codeText ")]') ?: [] as $node) {
+ if ($node instanceof DOMElement) {
+ $nodes[] = $node;
+ }
+ }
+
+ foreach ($nodes as $node) {
+ if ($node->parentNode === null) {
+ continue;
+ }
+
+ $this->replaceWithPre($document, $xpath, $node);
+ }
+ }
+
+ private function convertHighlighterLists(DOMDocument $document, DOMXPath $xpath): void
+ {
+ $lists = [];
+ foreach ($xpath->query('//ol[contains(@class, "dp-")]') ?: [] as $node) {
+ if (! $node instanceof DOMElement || $node->parentNode === null) {
+ continue;
+ }
+
+ if ($this->closestTag($node, 'pre') !== null) {
+ continue;
+ }
+
+ $lists[] = $node;
+ }
+
+ foreach ($lists as $list) {
+ $this->replaceWithPre($document, $xpath, $list);
+ }
+ }
+
+ private function convertStackedCodeDivs(DOMDocument $document): void
+ {
+ $xpath = new DOMXPath($document);
+ $parents = [];
+ foreach ($xpath->query('//*') ?: [] as $node) {
+ if ($node instanceof DOMElement) {
+ $parents[] = $node;
+ }
+ }
+
+ foreach ($parents as $parent) {
+ if ($parent->parentNode === null) {
+ continue;
+ }
+
+ if (in_array(strtolower($parent->nodeName), ['pre', 'code'], true)) {
+ continue;
+ }
+
+ $run = [];
+ $flush = function () use (&$run, $document): void {
+ if (count($run) < 2) {
+ $run = [];
+
+ return;
+ }
+
+ $lines = array_map(fn (DOMElement $div): string => $this->plainText($this->innerHtmlOf($div)), $run);
+ if (! $this->looksLikeCodeLines($lines)) {
+ $run = [];
+
+ return;
+ }
+
+ $first = $run[0];
+ $pre = $this->makePre($document, implode("\n", $lines), $this->detectLanguage('', implode("\n", $lines)));
+ $first->parentNode?->insertBefore($pre, $first);
+ foreach ($run as $div) {
+ $div->parentNode?->removeChild($div);
+ }
+ $run = [];
+ };
+
+ foreach (iterator_to_array($parent->childNodes) as $child) {
+ if ($child instanceof DOMNode && $child->nodeType === XML_TEXT_NODE && trim((string) $child->textContent) === '') {
+ continue;
+ }
+
+ if ($child instanceof DOMElement && strtolower($child->nodeName) === 'div' && $this->isSimpleLineDiv($child)) {
+ $run[] = $child;
+ continue;
+ }
+
+ $flush();
+ }
+
+ $flush();
+ }
+ }
+
+ private function removeEmptyBlocks(DOMXPath $xpath): void
+ {
+ foreach ($xpath->query('//p|//div') ?: [] as $node) {
+ if (! $node instanceof DOMElement || $node->parentNode === null) {
+ continue;
+ }
+
+ if ($node->getElementsByTagName('*')->length > 0) {
+ continue;
+ }
+
+ $text = trim(str_replace("\xC2\xA0", ' ', $node->textContent));
+ if ($text === '') {
+ $node->parentNode->removeChild($node);
+ }
+ }
+ }
+
+ private function replaceWithPre(DOMDocument $document, DOMXPath $xpath, DOMElement $node): void
+ {
+ $hint = '';
+ if (preg_match('/dp-([a-z0-9#+]+)/i', $node->getAttribute('class'), $match)) {
+ $hint = $match[1];
+ }
+
+ $lines = [];
+ foreach ($xpath->query('.//li', $node) ?: [] as $item) {
+ $lines[] = $this->plainText($document->saveHTML($item) ?: '');
+ }
+
+ if ($lines === []) {
+ foreach ($xpath->query('.//*[contains(@class, "codeHead")]', $node) ?: [] as $head) {
+ $head->parentNode?->removeChild($head);
+ }
+ $lines = preg_split('/\R/u', $this->plainText($node->textContent)) ?: [];
+ }
+
+ $code = implode("\n", $lines);
+ $pre = $this->makePre($document, $code, $this->detectLanguage($hint, $code));
+ $node->parentNode?->replaceChild($pre, $node);
+ }
+
+ private function makePre(DOMDocument $document, string $code, string $language): DOMElement
+ {
+ $pre = $document->createElement('pre');
+ $codeNode = $document->createElement('code');
+ $codeNode->setAttribute('class', 'language-'.$language);
+ $codeNode->appendChild($document->createTextNode($code));
+ $pre->appendChild($codeNode);
+
+ return $pre;
+ }
+
+ /**
+ * @param list $lines
+ */
+ private function looksLikeCodeLines(array $lines): bool
+ {
+ $codeish = 0;
+ foreach ($lines as $line) {
+ if ($line === '') {
+ continue;
+ }
+
+ if (preg_match('/\\\\$/', $line)
+ || preg_match('/^\s*(docker|npm|npx|pnpm|yarn|git|curl|wget|sudo|ssh|kubectl|composer|php|python|pip|node)\b/i', $line)
+ || preg_match('/^\s+--[\w-]+/', $line)
+ || preg_match('/^\s*\|/', $line)
+ || preg_match('/[{};]$/', $line)
+ ) {
+ $codeish++;
+ }
+ }
+
+ return $codeish >= 2;
+ }
+
+ private function isSimpleLineDiv(DOMElement $div): bool
+ {
+ foreach ($div->childNodes as $child) {
+ if ($child instanceof DOMElement && ! in_array(strtolower($child->nodeName), ['br', 'span', 'code', 'b', 'i', 'em', 'strong'], true)) {
+ return false;
+ }
+ }
+
+ $text = $this->plainText($div->textContent);
+
+ return $text !== '';
+ }
+
+ private function detectLanguage(string $hint, string $code): string
+ {
+ if (preg_match('/^\s*(docker|kubectl|npm|npx|pnpm|yarn|composer|curl|wget|sudo|ssh)\b/m', $code)) {
+ return 'bash';
+ }
+
+ $aliases = $this->languageAliases();
+ $key = strtolower(trim($hint));
+
+ return $aliases[$key] ?? ($key !== '' ? $key : 'text');
+ }
+
+ /**
+ * Recover a docker/shell command that HtmlConverter flattened into one line.
+ */
+ private function recoverFlattenedShell(string $content): string
+ {
+ if (str_contains($content, 'plainText($match[0]));
+ $code = preg_replace('/\\\\\s+/', "\\\n", $code) ?? $code;
+
+ return ''.e($code).'
';
+ },
+ $content
+ ) ?? $content;
+ }
+
+ private function closestTag(DOMNode $node, string $tag): ?DOMElement
+ {
+ $current = $node->parentNode;
+ while ($current instanceof DOMElement) {
+ if (strtolower($current->nodeName) === $tag) {
+ return $current;
+ }
+ $current = $current->parentNode;
+ }
+
+ return null;
+ }
+
+ private function loadDocument(string $html): DOMDocument
+ {
+ $document = new DOMDocument('1.0', 'UTF-8');
+ $previous = libxml_use_internal_errors(true);
+ $wrapped = ''.$html.'';
+ $document->loadHTML(''.$wrapped, LIBXML_NOERROR | LIBXML_HTML_NODEFDTD);
+ libxml_clear_errors();
+ libxml_use_internal_errors($previous);
+
+ return $document;
+ }
+
+ private function innerHtml(DOMDocument $document): string
+ {
+ $root = $document->getElementById('ll-root');
+ if (! $root instanceof DOMElement) {
+ $root = (new DOMXPath($document))->query('//*[@id="ll-root"]')->item(0);
+ }
+
+ if (! $root instanceof DOMElement) {
+ return '';
+ }
+
+ return $this->innerHtmlOf($root);
+ }
+
+ private function innerHtmlOf(DOMElement $element): string
+ {
+ $html = '';
+ foreach ($element->childNodes as $child) {
+ $html .= $element->ownerDocument?->saveHTML($child) ?: '';
+ }
+
+ return $html;
+ }
+}
diff --git a/app/Console/Commands/ConvertContent.php b/app/Console/Commands/ConvertContent.php
index d3854ba..37f3c04 100644
--- a/app/Console/Commands/ConvertContent.php
+++ b/app/Console/Commands/ConvertContent.php
@@ -5,6 +5,7 @@ declare(strict_types=1);
namespace App\Console\Commands;
+use App\Blog\Support\SablogHtmlNormalizer;
use App\Models\Post;
use Illuminate\Console\Command;
use League\HTMLToMarkdown\HtmlConverter;
@@ -36,6 +37,7 @@ class ConvertContent extends Command
]);
$renderer = app(\App\Blog\Services\PostContentRenderer::class);
+ $normalizer = app(SablogHtmlNormalizer::class);
$posts = $query->get();
if ($posts->isEmpty()) {
@@ -49,7 +51,7 @@ class ConvertContent extends Command
foreach ($posts as $post) {
// [attach=xx] 是 markdown 保留字符,先换成令牌,渲染时再解析
- $markdown = $converter->convert($renderer->toMarkdownSafeTokens($post->content));
+ $markdown = $normalizer->toMarkdown((string) $post->content, $converter, $renderer);
if ($this->option('dry-run')) {
$this->newLine();
diff --git a/app/Console/Commands/FixImportedContent.php b/app/Console/Commands/FixImportedContent.php
new file mode 100644
index 0000000..72469dd
--- /dev/null
+++ b/app/Console/Commands/FixImportedContent.php
@@ -0,0 +1,62 @@
+argument('id')) {
+ $query->where('id', $id);
+ } elseif (! $this->option('all')) {
+ $this->error('Pass a post ID or use --all');
+
+ return self::FAILURE;
+ }
+
+ $posts = $query->get();
+ if ($posts->isEmpty()) {
+ $this->info('No posts to repair');
+
+ return self::SUCCESS;
+ }
+
+ $changed = 0;
+ foreach ($posts as $post) {
+ $cleaned = $normalizer->normalize((string) $post->content);
+ if ($cleaned === $post->content) {
+ continue;
+ }
+
+ if ($this->option('dry-run')) {
+ $this->line("[{$post->id}] {$post->title}");
+ continue;
+ }
+
+ $post->content = $cleaned;
+ $post->save();
+ $changed++;
+ }
+
+ $this->info($this->option('dry-run')
+ ? 'Preview only, nothing written'
+ : "Repaired {$changed} posts");
+
+ return self::SUCCESS;
+ }
+}
diff --git a/app/Console/Commands/SablogImport.php b/app/Console/Commands/SablogImport.php
index ff1a0dd..c4ee7f2 100644
--- a/app/Console/Commands/SablogImport.php
+++ b/app/Console/Commands/SablogImport.php
@@ -2,19 +2,18 @@
declare(strict_types=1);
-
namespace App\Console\Commands;
use App\Blog\Services\AttachmentImporter;
-use App\Models\Category;
-use App\Models\Comment;
-use App\Models\Link;
+use App\Blog\Services\PostContentRenderer;
+use App\Blog\Support\SablogHtmlNormalizer;
use App\Models\Post;
use App\Models\Setting;
use App\Models\User;
use App\Support\SlugGenerator;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
+use League\HTMLToMarkdown\HtmlConverter;
use PDO;
use Spatie\Tags\Tag;
@@ -271,6 +270,7 @@ class SablogImport extends Command
'keywords' => $row['keywords'] ?: null,
'status' => $status,
'is_sticky' => (bool) $row['stick'],
+ 'sticky_level' => ((bool) $row['stick']) ? 1 : 0,
'close_comment' => (bool) $row['closecomment'],
'read_password' => $row['readpassword'] ?: null,
'views' => (int) $row['views'],
@@ -362,15 +362,16 @@ class SablogImport extends Command
{
$this->info('转换文章内容为 Markdown ...');
- $converter = new \League\HTMLToMarkdown\HtmlConverter([
+ $converter = new HtmlConverter([
'strip_tags' => false,
'hard_break' => true,
]);
- $renderer = app(\App\Blog\Services\PostContentRenderer::class);
+ $renderer = app(PostContentRenderer::class);
+ $normalizer = app(SablogHtmlNormalizer::class);
$count = 0;
foreach (Post::query()->where('content_format', 'html')->cursor() as $post) {
- $post->content = $converter->convert($renderer->toMarkdownSafeTokens($post->content));
+ $post->content = $normalizer->toMarkdown((string) $post->content, $converter, $renderer);
$post->content_format = 'markdown';
$post->save();
$count++;
diff --git a/app/Filament/Pages/BlogSettings.php b/app/Filament/Pages/BlogSettings.php
index fce4082..909e690 100644
--- a/app/Filament/Pages/BlogSettings.php
+++ b/app/Filament/Pages/BlogSettings.php
@@ -42,6 +42,7 @@ class BlogSettings extends Page
// 外观
'active_theme' => Setting::get('active_theme', config('themes.default')),
'per_page' => Setting::get('per_page', config('blog.per_page')),
+ 'home_view_mode' => Setting::get('home_view_mode', config('blog.home_view_mode', 'list')),
'list_shownum' => Setting::get('list_shownum', '50'),
'listtime' => Setting::get('listtime', 'Y-m-d'),
'normaltime' => Setting::get('normaltime', 'Y-m-d H:i'),
@@ -102,6 +103,13 @@ class BlogSettings extends Page
->columnSpanFull()
->options(collect(app(\App\Blog\Support\ThemeManager::class)->all())->pluck('title', 'name')),
TextInput::make('per_page')->label('首页每页文章数')->numeric(),
+ Select::make('home_view_mode')
+ ->label('首页浏览模式')
+ ->options([
+ 'list' => '列表(标题 + 摘要)',
+ 'full' => '标准(全文)',
+ ])
+ ->helperText('前台右上角仍可临时切换,和老 sablog 的「标准 | 列表」一样'),
TextInput::make('list_shownum')->label('列表每页数量')->numeric()->helperText('分类/标签/搜索等列表页'),
TextInput::make('listtime')->label('列表时间格式')->placeholder('Y-m-d'),
TextInput::make('normaltime')->label('详情时间格式')->placeholder('Y-m-d H:i'),
diff --git a/app/Filament/Pages/Languages.php b/app/Filament/Pages/Languages.php
new file mode 100644
index 0000000..32cfaf5
--- /dev/null
+++ b/app/Filament/Pages/Languages.php
@@ -0,0 +1,91 @@
+,
+ * filament_locales: list,
+ * content_i18n: bool
+ * }
+ */
+ public function getInfoProperty(): array
+ {
+ return [
+ 'locale' => app()->getLocale(),
+ 'fallback' => (string) config('app.fallback_locale'),
+ 'faker' => (string) config('app.faker_locale'),
+ 'app_files' => $this->appLanguageFiles(),
+ 'filament_locales' => $this->filamentLocales(),
+ 'content_i18n' => false,
+ ];
+ }
+
+ /**
+ * @return list
+ */
+ private function appLanguageFiles(): array
+ {
+ $found = [];
+
+ foreach (File::glob(lang_path('*.json')) as $file) {
+ $found[] = basename((string) $file, '.json');
+ }
+
+ foreach (File::directories(lang_path()) as $directory) {
+ $name = basename($directory);
+ if ($name === 'vendor') {
+ continue;
+ }
+ $found[] = $name;
+ }
+
+ $found = array_values(array_unique($found));
+ sort($found);
+
+ return $found;
+ }
+
+ /**
+ * @return list
+ */
+ private function filamentLocales(): array
+ {
+ $path = lang_path('vendor/filament');
+ if (! File::isDirectory($path)) {
+ return [];
+ }
+
+ $locales = array_map(
+ static fn (string $directory): string => basename($directory),
+ File::directories($path)
+ );
+ sort($locales);
+
+ return $locales;
+ }
+}
diff --git a/app/Filament/Pages/PageCache.php b/app/Filament/Pages/PageCache.php
new file mode 100644
index 0000000..0d04358
--- /dev/null
+++ b/app/Filament/Pages/PageCache.php
@@ -0,0 +1,93 @@
+ (int) config('blog.page_cache_ttl', 300),
+ ];
+ }
+
+ /**
+ * @return list
+ */
+ public function getEntriesProperty(): array
+ {
+ return PageCacheMiddleware::entries();
+ }
+
+ public function flushPageCache(): void
+ {
+ PageCacheMiddleware::flush();
+ Notification::make()->title('前台页面缓存已清空')->success()->send();
+ }
+
+ public function flushAllCache(): void
+ {
+ PageCacheMiddleware::flush();
+ Setting::flushCache();
+ Cache::flush();
+ Notification::make()->title('全部缓存已清空')->success()->send();
+ }
+
+ public function resetStats(): void
+ {
+ PageCacheMiddleware::resetStats();
+ Notification::make()->title('命中统计已重置')->success()->send();
+ }
+
+ public function forgetEntry(string $key): void
+ {
+ PageCacheMiddleware::forget($key);
+ Notification::make()->title('已删除这条缓存')->success()->send();
+ }
+
+ protected function getHeaderActions(): array
+ {
+ return [
+ Action::make('resetStats')
+ ->label('重置统计')
+ ->color('gray')
+ ->action(fn () => $this->resetStats()),
+ Action::make('flushPageCache')
+ ->label('清空前台缓存')
+ ->color('warning')
+ ->action(fn () => $this->flushPageCache()),
+ Action::make('flushAllCache')
+ ->label('清空全部缓存')
+ ->color('danger')
+ ->requiresConfirmation()
+ ->action(fn () => $this->flushAllCache()),
+ ];
+ }
+}
diff --git a/app/Filament/Resources/Categories/Schemas/CategoryForm.php b/app/Filament/Resources/Categories/Schemas/CategoryForm.php
index 6cc29fe..c30f40d 100644
--- a/app/Filament/Resources/Categories/Schemas/CategoryForm.php
+++ b/app/Filament/Resources/Categories/Schemas/CategoryForm.php
@@ -46,7 +46,8 @@ class CategoryForm
->label('封面图')
->module('articles')
->mediaType('image')
- ->relationship('coverMedia'),
+ ->relationship('coverMedia')
+ ->extraFieldWrapperAttributes(['class' => 'll-media-picker-preview']),
]),
]);
}
diff --git a/app/Filament/Resources/Posts/Pages/EditPost.php b/app/Filament/Resources/Posts/Pages/EditPost.php
index 36e3984..d46bc8a 100644
--- a/app/Filament/Resources/Posts/Pages/EditPost.php
+++ b/app/Filament/Resources/Posts/Pages/EditPost.php
@@ -2,10 +2,10 @@
declare(strict_types=1);
-
namespace App\Filament\Resources\Posts\Pages;
use App\Filament\Resources\Posts\PostResource;
+use Filament\Actions\Action;
use Filament\Actions\DeleteAction;
use Filament\Resources\Pages\EditRecord;
@@ -16,6 +16,12 @@ class EditPost extends EditRecord
protected function getHeaderActions(): array
{
return [
+ Action::make('preview')
+ ->label('预览')
+ ->icon('heroicon-o-eye')
+ ->color('gray')
+ ->url(fn (): string => $this->record->url)
+ ->openUrlInNewTab(),
DeleteAction::make(),
];
}
diff --git a/app/Filament/Resources/Posts/RelationManagers/CommentsRelationManager.php b/app/Filament/Resources/Posts/RelationManagers/CommentsRelationManager.php
index c435a89..378ae4e 100644
--- a/app/Filament/Resources/Posts/RelationManagers/CommentsRelationManager.php
+++ b/app/Filament/Resources/Posts/RelationManagers/CommentsRelationManager.php
@@ -5,10 +5,11 @@ declare(strict_types=1);
namespace App\Filament\Resources\Posts\RelationManagers;
use App\Models\Comment;
-use Filament\Forms\Components\TextInput;
+use Filament\Actions\Action;
use Filament\Forms\Components\Textarea;
+use Filament\Forms\Components\TextInput;
use Filament\Resources\RelationManagers\RelationManager;
-use Filament\Tables\Actions\Action;
+use Filament\Schemas\Schema;
use Filament\Tables\Columns\TextColumn;
use Filament\Tables\Table;
@@ -22,7 +23,7 @@ class CommentsRelationManager extends RelationManager
protected static ?string $pluralModelLabel = '评论';
- public function form(\Filament\Schemas\Schema $schema): \Filament\Schemas\Schema
+ public function form(Schema $schema): Schema
{
return $schema
->components([
diff --git a/app/Filament/Resources/Posts/Schemas/PostForm.php b/app/Filament/Resources/Posts/Schemas/PostForm.php
index 47521f8..c04ebce 100644
--- a/app/Filament/Resources/Posts/Schemas/PostForm.php
+++ b/app/Filament/Resources/Posts/Schemas/PostForm.php
@@ -5,129 +5,295 @@ declare(strict_types=1);
namespace App\Filament\Resources\Posts\Schemas;
use App\Blog\Support\MarkdownMediaEmbed;
+use App\Models\Post;
use Filament\Actions\Action;
use Filament\Forms\Components\DateTimePicker;
use Filament\Forms\Components\MarkdownEditor;
use Filament\Forms\Components\Select;
-use Filament\Forms\Components\TextInput;
use Filament\Forms\Components\Textarea;
-use Filament\Forms\Components\Toggle;
+use Filament\Forms\Components\TextInput;
+use Filament\Forms\Components\ToggleButtons;
use Filament\Notifications\Notification;
use Filament\Schemas\Components\Actions;
+use Filament\Schemas\Components\Component;
+use Filament\Schemas\Components\Group;
+use Filament\Schemas\Components\Section;
use Filament\Schemas\Components\Utilities\Get;
use Filament\Schemas\Components\Utilities\Set;
use Filament\Schemas\Schema;
+use ReflectionClass;
use Xpier\FilamentMediaLibrary\Components\MediaPicker;
use Xpier\FilamentMediaLibrary\Models\MediaLibrary;
class PostForm
{
+ public const SIDEBAR_KEY = 'post_form_sidebar';
+
+ public const ACCESS_KEY = 'post_form_access';
+
public static function configure(Schema $schema): Schema
{
return $schema
+ ->columns(self::pcColumns(3))
->components([
- \Filament\Schemas\Components\Section::make('内容')
- ->columns(2)
- ->schema([
- TextInput::make('title')
- ->label('标题')
- ->required()
- ->columnSpanFull(),
- TextInput::make('slug')
- ->label('Slug(留空自动生成)'),
- Select::make('category_id')
- ->label('分类')
- ->relationship('category', 'name'),
- Select::make('content_format')
- ->label('内容格式')
- ->options([
- 'markdown' => 'Markdown',
- 'html' => 'HTML',
- ])
- ->default('markdown')
- ->live(),
- Select::make('status')
- ->label('状态')
- ->options([
- 'published' => '已发布',
- 'draft' => '草稿',
- 'private' => '私密',
- ])
- ->default('published'),
- MarkdownEditor::make('content')
- ->label('正文')
- ->required()
- ->helperText('Markdown 插入图片用 ,文件用 [文件名](media:ID)。不要把 CDN / 对象存储地址写进正文。')
- ->fileAttachments(false)
- ->columnSpanFull(),
- MediaPicker::make('insert_media_id')
- ->label('从媒体库插入正文')
- ->module('articles')
- ->mediaType(null)
- ->dehydrated(false)
- ->helperText('选好媒体后点「插入到正文」。Markdown 写入 media:ID 引用,渲染时再换成真实地址。')
- ->columnSpanFull(),
- Actions::make([
- Action::make('insertSelectedMedia')
- ->label('插入到正文')
- ->action(function (Get $get, Set $set): void {
- $mediaId = $get('insert_media_id');
- $media = is_numeric($mediaId)
- ? MediaLibrary::query()->find((int) $mediaId)
- : null;
-
- if (! $media instanceof MediaLibrary) {
- Notification::make()->title('请先从媒体库选择文件')->warning()->send();
-
- return;
- }
-
- $snippet = app(MarkdownMediaEmbed::class)->snippet(
- $media,
- (string) ($get('content_format') ?: 'markdown')
- );
- $set('content', rtrim((string) $get('content'))."\n\n".$snippet."\n");
- $set('insert_media_id', null);
- Notification::make()->title('已写入正文引用')->success()->send();
- }),
- ])->columnSpanFull(),
- ]),
- \Filament\Schemas\Components\Section::make('封面与摘要')
- ->columns(2)
- ->schema([
- MediaPicker::make('cover_media_id')
- ->label('封面图')
- ->module('articles')
- ->mediaType('image')
- ->relationship('coverMedia')
- ->columnSpanFull(),
- Textarea::make('excerpt')
- ->label('摘要(留空自动截取)')
- ->rows(3),
- TextInput::make('keywords')
- ->label('关键词(逗号分隔)'),
- ]),
- \Filament\Schemas\Components\Section::make('发布设置')
- ->columns(3)
- ->schema([
- DateTimePicker::make('published_at')
- ->label('发布时间')
- ->default(now()),
- Toggle::make('is_sticky')
- ->label('置顶'),
- Toggle::make('close_comment')
- ->label('关闭评论'),
- TextInput::make('read_password')
- ->label('阅读密码(留空公开)'),
- TextInput::make('views')
- ->label('浏览量')
- ->numeric()
- ->disabled(),
- TextInput::make('comment_count')
- ->label('评论数')
- ->numeric()
- ->disabled(),
- ]),
+ self::contentSection(),
+ self::sidebarGroup(),
]);
}
+
+ /**
+ * Append sections or fields into the right-hand sidebar (2:1 layout).
+ *
+ * @param array $components
+ */
+ public static function appendToSidebar(Schema $schema, array $components): void
+ {
+ $sidebar = self::findKeyedComponent($schema->getComponents(), self::SIDEBAR_KEY);
+
+ if (! $sidebar instanceof Component) {
+ $schema->components([
+ ...$schema->getComponents(),
+ ...$components,
+ ]);
+
+ return;
+ }
+
+ $existing = $sidebar->getDefaultChildComponents();
+
+ if ($existing instanceof Schema) {
+ $existing->components([
+ ...$existing->getComponents(),
+ ...$components,
+ ]);
+
+ return;
+ }
+
+ $sidebar->schema([
+ ...(is_array($existing) ? $existing : []),
+ ...$components,
+ ]);
+ }
+
+ /**
+ * Append fields into the access group (password / members / price).
+ *
+ * @param array $fields
+ */
+ public static function appendToAccessSection(Schema $schema, array $fields): void
+ {
+ $sidebar = self::findKeyedComponent($schema->getComponents(), self::SIDEBAR_KEY);
+ $access = $sidebar instanceof Component
+ ? self::findKeyedComponent(self::childList($sidebar), self::ACCESS_KEY)
+ : null;
+
+ if (! $access instanceof Component) {
+ self::appendToSidebar($schema, [
+ Section::make('访问设置')
+ ->key(self::ACCESS_KEY)
+ ->schema($fields),
+ ]);
+
+ return;
+ }
+
+ $existing = $access->getDefaultChildComponents();
+
+ if ($existing instanceof Schema) {
+ $existing->components([
+ ...$existing->getComponents(),
+ ...$fields,
+ ]);
+
+ return;
+ }
+
+ $access->schema([
+ ...(is_array($existing) ? $existing : []),
+ ...$fields,
+ ]);
+ }
+
+ private static function contentSection(): Section
+ {
+ return Section::make('正文')
+ ->columnSpan(self::pcColumns(2))
+ ->columns(self::pcColumns(3))
+ ->schema([
+ TextInput::make('title')
+ ->label('标题')
+ ->required()
+ ->columnSpanFull(),
+ TextInput::make('slug')
+ ->label('Slug(留空自动生成)')
+ ->columnSpanFull(),
+ Select::make('category_id')
+ ->label('分类')
+ ->relationship('category', 'name'),
+ ToggleButtons::make('content_format')
+ ->label('内容格式')
+ ->options([
+ 'markdown' => 'Markdown',
+ 'html' => 'HTML',
+ ])
+ ->default('markdown')
+ ->inline()
+ ->grouped()
+ ->live(),
+ ToggleButtons::make('status')
+ ->label('状态')
+ ->options([
+ 'published' => '已发布',
+ 'draft' => '草稿',
+ 'private' => '私密',
+ ])
+ ->default('published')
+ ->inline()
+ ->grouped(),
+ MediaPicker::make('cover_media_id')
+ ->label('封面图')
+ ->module('articles')
+ ->mediaType('image')
+ ->relationship('coverMedia')
+ ->extraFieldWrapperAttributes(['class' => 'll-media-picker-preview'])
+ ->columnSpanFull(),
+ MarkdownEditor::make('content')
+ ->label('正文')
+ ->required()
+ ->minHeight('32rem')
+ ->helperText('Markdown 插入图片用 ,文件用 [文件名](media:ID)。不要把 CDN / 对象存储地址写进正文。')
+ ->fileAttachments(false)
+ ->columnSpanFull(),
+ MediaPicker::make('insert_media_id')
+ ->label('从媒体库插入正文')
+ ->module('articles')
+ ->mediaType(null)
+ ->dehydrated(false)
+ ->extraFieldWrapperAttributes(['class' => 'll-media-picker-preview'])
+ ->helperText('选好媒体后点「插入到正文」。Markdown 写入 media:ID 引用,渲染时再换成真实地址。')
+ ->columnSpanFull(),
+ Actions::make([
+ Action::make('insertSelectedMedia')
+ ->label('插入到正文')
+ ->action(function (Get $get, Set $set): void {
+ $mediaId = $get('insert_media_id');
+ $media = is_numeric($mediaId)
+ ? MediaLibrary::query()->find((int) $mediaId)
+ : null;
+
+ if (! $media instanceof MediaLibrary) {
+ Notification::make()->title('请先从媒体库选择文件')->warning()->send();
+
+ return;
+ }
+
+ $snippet = app(MarkdownMediaEmbed::class)->snippet(
+ $media,
+ (string) ($get('content_format') ?: 'markdown')
+ );
+ $set('content', rtrim((string) $get('content'))."\n\n".$snippet."\n");
+ $set('insert_media_id', null);
+ Notification::make()->title('已写入正文引用')->success()->send();
+ }),
+ ])->columnSpanFull(),
+ ]);
+ }
+
+ private static function sidebarGroup(): Group
+ {
+ return Group::make([
+ Section::make('发布设置')
+ ->schema([
+ DateTimePicker::make('published_at')
+ ->label('发布时间')
+ ->default(now()),
+ ToggleButtons::make('sticky_level')
+ ->label('置顶等级')
+ ->options(Post::stickyLevelOptions())
+ ->default(Post::STICKY_NONE)
+ ->inline()
+ ->grouped()
+ ->helperText('轻置顶压过普通文章,强置顶压过轻置顶,置顶头条永远排在最上面。'),
+ ToggleButtons::make('close_comment')
+ ->label('评论状态')
+ ->boolean('关闭评论', '开放评论')
+ ->default(false)
+ ->inline()
+ ->grouped()
+ ->dehydrateStateUsing(fn (mixed $state): bool => (bool) $state),
+ Textarea::make('excerpt')
+ ->label('摘要(留空自动截取)')
+ ->rows(4),
+ TextInput::make('keywords')
+ ->label('关键词(逗号分隔)'),
+ ]),
+ Section::make('访问设置')
+ ->key(self::ACCESS_KEY)
+ ->schema([
+ TextInput::make('read_password')
+ ->label('阅读密码(留空公开)'),
+ ]),
+ ])
+ ->key(self::SIDEBAR_KEY)
+ ->columnSpan(self::pcColumns(1));
+ }
+
+ /**
+ * Admin writing is almost always on a PC. Filament's columns(3) only
+ * applies from the lg breakpoint, so we pin every breakpoint.
+ *
+ * @return array
+ */
+ private static function pcColumns(int $count): array
+ {
+ return [
+ 'default' => $count,
+ 'sm' => $count,
+ 'md' => $count,
+ 'lg' => $count,
+ 'xl' => $count,
+ '2xl' => $count,
+ ];
+ }
+
+ /**
+ * @param array $components
+ */
+ private static function findKeyedComponent(array $components, string $key): ?Component
+ {
+ foreach ($components as $component) {
+ if (! $component instanceof Component) {
+ continue;
+ }
+
+ if (self::rawKey($component) === $key) {
+ return $component;
+ }
+ }
+
+ return null;
+ }
+
+ private static function rawKey(Component $component): ?string
+ {
+ $property = (new ReflectionClass($component))->getProperty('key');
+ $key = $property->getValue($component);
+
+ return is_string($key) ? $key : null;
+ }
+
+ /**
+ * @return array
+ */
+ private static function childList(Component $component): array
+ {
+ $children = $component->getDefaultChildComponents();
+
+ if ($children instanceof Schema) {
+ return $children->getComponents();
+ }
+
+ return is_array($children) ? $children : [];
+ }
}
diff --git a/app/Filament/Resources/Posts/Tables/PostsTable.php b/app/Filament/Resources/Posts/Tables/PostsTable.php
index 881e3e6..28158ac 100644
--- a/app/Filament/Resources/Posts/Tables/PostsTable.php
+++ b/app/Filament/Resources/Posts/Tables/PostsTable.php
@@ -2,18 +2,18 @@
declare(strict_types=1);
-
namespace App\Filament\Resources\Posts\Tables;
+use App\Models\Post;
+use Filament\Actions\Action;
use Filament\Actions\BulkActionGroup;
use Filament\Actions\DeleteBulkAction;
use Filament\Actions\EditAction;
use Filament\Actions\ViewAction;
-use Filament\Forms\Components\Select;
-use Filament\Tables\Columns\IconColumn;
+use Filament\Support\Enums\Width;
use Filament\Tables\Columns\TextColumn;
+use Filament\Tables\Enums\FiltersLayout;
use Filament\Tables\Filters\SelectFilter;
-use Filament\Tables\Filters\TernaryFilter;
use Filament\Tables\Table;
class PostsTable
@@ -46,9 +46,17 @@ class PostsTable
'draft' => 'warning',
default => 'gray',
}),
- IconColumn::make('is_sticky')
+ TextColumn::make('sticky_level')
->label('置顶')
- ->boolean(),
+ ->badge()
+ ->sortable()
+ ->formatStateUsing(fn ($state) => Post::stickyLevelOptions()[(int) $state] ?? '不置顶')
+ ->color(fn ($state) => match ((int) $state) {
+ Post::STICKY_HEADLINE => 'danger',
+ Post::STICKY_STRONG => 'warning',
+ Post::STICKY_LIGHT => 'success',
+ default => 'gray',
+ }),
TextColumn::make('views')
->label('阅读')
->numeric()
@@ -79,12 +87,17 @@ class PostsTable
'markdown' => 'Markdown',
'html' => 'HTML',
]),
- TernaryFilter::make('is_sticky')
- ->label('置顶'),
- ], \Filament\Tables\Enums\FiltersLayout::AboveContent)
+ SelectFilter::make('sticky_level')
+ ->label('置顶')
+ ->options(Post::stickyLevelOptions()),
+ ], FiltersLayout::AboveContent)
->filtersFormColumns(3)
- ->filtersFormWidth(\Filament\Support\Enums\Width::ThreeExtraLarge)
+ ->filtersFormWidth(Width::ThreeExtraLarge)
->recordActions([
+ Action::make('preview')
+ ->label('预览')
+ ->url(fn (Post $record): string => $record->url)
+ ->openUrlInNewTab(),
ViewAction::make(),
EditAction::make(),
])
diff --git a/app/Filament/Widgets/BlogStats.php b/app/Filament/Widgets/BlogStats.php
index 69482e1..07d651d 100644
--- a/app/Filament/Widgets/BlogStats.php
+++ b/app/Filament/Widgets/BlogStats.php
@@ -2,9 +2,10 @@
declare(strict_types=1);
-
namespace App\Filament\Widgets;
+use App\Blog\Support\PageCacheMiddleware;
+use App\Filament\Pages\PageCache;
use App\Models\Comment;
use App\Models\Post;
use App\Models\User;
@@ -17,7 +18,7 @@ class BlogStats extends BaseWidget
protected function getStats(): array
{
- $cache = \App\Blog\Support\PageCacheMiddleware::stats();
+ $cache = PageCacheMiddleware::stats();
return [
Stat::make('文章', Post::published()->count())
@@ -30,6 +31,7 @@ class BlogStats extends BaseWidget
->icon('heroicon-o-users'),
Stat::make('页面缓存', $cache['count'].' 条')
->description('命中率 '.$cache['rate'].'%(命中 '.$cache['hits'].' / 未命中 '.$cache['misses'].',按日滚动)')
+ ->url(PageCache::getUrl())
->icon('heroicon-o-bolt'),
];
}
diff --git a/app/Models/Post.php b/app/Models/Post.php
index f972295..850577e 100644
--- a/app/Models/Post.php
+++ b/app/Models/Post.php
@@ -2,25 +2,25 @@
declare(strict_types=1);
-
namespace App\Models;
+use App\Support\MediaDisk;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Spatie\MediaLibrary\HasMedia;
-use Xpier\FilamentMediaLibrary\Models\MediaLibrary;
use Spatie\MediaLibrary\InteractsWithMedia;
use Spatie\MediaLibrary\MediaCollections\Models\Media;
use Spatie\Tags\HasTags;
+use Xpier\FilamentMediaLibrary\Models\MediaLibrary;
class Post extends Model implements HasMedia
{
use HasFactory;
- use InteractsWithMedia;
use HasTags;
+ use InteractsWithMedia;
protected $fillable = [
'category_id',
@@ -34,6 +34,7 @@ class Post extends Model implements HasMedia
'cover_media_id',
'status',
'is_sticky',
+ 'sticky_level',
'close_comment',
'read_password',
'views',
@@ -45,10 +46,63 @@ class Post extends Model implements HasMedia
protected $casts = [
'meta' => 'array',
'is_sticky' => 'boolean',
+ 'sticky_level' => 'integer',
'close_comment' => 'boolean',
'published_at' => 'datetime',
];
+ public const STICKY_NONE = 0;
+
+ public const STICKY_LIGHT = 1;
+
+ public const STICKY_STRONG = 2;
+
+ public const STICKY_HEADLINE = 3;
+
+ /**
+ * @return array
+ */
+ public static function stickyLevelOptions(): array
+ {
+ return [
+ self::STICKY_NONE => '不置顶',
+ self::STICKY_LIGHT => '轻置顶',
+ self::STICKY_STRONG => '强置顶',
+ self::STICKY_HEADLINE => '置顶头条',
+ ];
+ }
+
+ public function stickyLabel(): ?string
+ {
+ $level = (int) $this->sticky_level;
+
+ return $level > 0 ? (self::stickyLevelOptions()[$level] ?? '置顶') : null;
+ }
+
+ public function isPinned(): bool
+ {
+ return (int) $this->sticky_level > 0 || $this->is_sticky;
+ }
+
+ protected static function booted(): void
+ {
+ static::saving(function (Post $post): void {
+ $level = (int) ($post->sticky_level ?? 0);
+ $post->sticky_level = max(self::STICKY_NONE, min(self::STICKY_HEADLINE, $level));
+
+ if ($post->is_sticky && $post->sticky_level < self::STICKY_LIGHT) {
+ $post->sticky_level = self::STICKY_LIGHT;
+ }
+
+ $post->is_sticky = $post->sticky_level > self::STICKY_NONE;
+ });
+ }
+
+ public function scopeOrderedForList(Builder $query): Builder
+ {
+ return $query->orderByDesc('sticky_level')->latest('published_at');
+ }
+
public function category(): BelongsTo
{
return $this->belongsTo(Category::class);
@@ -107,8 +161,8 @@ class Post extends Model implements HasMedia
public function registerMediaCollections(): void
{
- $this->addMediaCollection('attachments')->useDisk(\App\Support\MediaDisk::name());
- $this->addMediaCollection('cover')->useDisk(\App\Support\MediaDisk::name())
+ $this->addMediaCollection('attachments')->useDisk(MediaDisk::name());
+ $this->addMediaCollection('cover')->useDisk(MediaDisk::name())
->singleFile()
->registerMediaConversions(function () {
$this->addMediaConversion('card')->width(1200)->height(630)->sharpen(1);
@@ -153,13 +207,13 @@ class Post extends Model implements HasMedia
return '';
}
- $text = strip_tags($this->content);
+ $text = (new \App\Blog\Support\SablogHtmlNormalizer)->plainText((string) $this->content);
// 付费块不进入摘要,避免付费内容泄漏到列表页/SEO meta
$text = preg_replace('/\[paid\].*?\[\/paid\]/s', '', $text) ?? $text;
$text = preg_replace('/!\[([^\]]*)\]\((?:media|attach):\d+\)/i', '$1', $text) ?? $text;
$text = preg_replace('/\[([^\]]*)\]\((?:media|attach):\d+\)/i', '$1', $text) ?? $text;
$text = preg_replace('/\[[^\]]*\]/', '', $text) ?? $text;
- return mb_substr($text, 0, 200);
+ return mb_substr(trim($text), 0, 200);
}
}
diff --git a/app/Providers/AppServiceProvider.php b/app/Providers/AppServiceProvider.php
index 046a8ae..aeb2539 100644
--- a/app/Providers/AppServiceProvider.php
+++ b/app/Providers/AppServiceProvider.php
@@ -4,10 +4,16 @@ declare(strict_types=1);
namespace App\Providers;
+use App\Blog\Support\HomeViewMode;
+use App\Blog\Support\PageCacheMiddleware;
+use App\Blog\Support\PluginManager;
+use App\Models\MarketPackage;
use Filament\Facades\Filament;
use Filament\View\PanelsRenderHook;
use Illuminate\Support\Facades\Blade;
+use Illuminate\Support\Facades\View;
use Illuminate\Support\ServiceProvider;
+use Plugins\Neatstudio\Payment\Models\Payment;
class AppServiceProvider extends ServiceProvider
{
@@ -21,16 +27,16 @@ class AppServiceProvider extends ServiceProvider
private function registerMarketPurchaseListener(): void
{
- $manager = app(\App\Blog\Support\PluginManager::class);
+ $manager = app(PluginManager::class);
if (! $manager->isEnabled('neatstudio.payment')) {
return;
}
- $manager->addAction('payment.paid', function (\Plugins\Neatstudio\Payment\Models\Payment $payment) {
+ $manager->addAction('payment.paid', function (Payment $payment) {
$payable = $payment->payable;
- if ($payable instanceof \App\Models\MarketPackage) {
+ if ($payable instanceof MarketPackage) {
$payable->markPaid($payment);
}
}, 10);
@@ -54,7 +60,20 @@ class AppServiceProvider extends ServiceProvider
$this->registerMarketPurchaseListener();
// 前台页面缓存(匿名 GET,缓存含 SEO 注入后的最终 HTML)
- app('router')->pushMiddlewareToGroup('web', \App\Blog\Support\PageCacheMiddleware::class);
+ app('router')->pushMiddlewareToGroup('web', PageCacheMiddleware::class);
+
+ View::composer([
+ 'index',
+ 'list',
+ 'tag',
+ 'search',
+ 'partials.post-card',
+ 'partials.view-mode-switcher',
+ ], function ($view): void {
+ if (! $view->offsetExists('homeViewMode')) {
+ $view->with('homeViewMode', HomeViewMode::current());
+ }
+ });
// 后台紧凑化:侧边栏 + 表格行 + 筛选区(Filament 无官方全局紧凑开关,用 CSS 收紧)
Filament::serving(function () {
@@ -96,6 +115,22 @@ class AppServiceProvider extends ServiceProvider
/* 文章编辑框加大(MarkdownEditor 基于 tiptap) */
.fi-fo-markdown-editor .tiptap, .fi-fo-markdown-editor .ProseMirror { min-height: 26rem !important; }
+ /* 封面/媒体预览:禁止按栏宽 100% 拉伸,否则大图会把编辑页撑烂 */
+ .ll-media-picker-preview img,
+ img.h-32.max-w-full.rounded-md.object-contain {
+ max-width: 220px !important;
+ max-height: 124px !important;
+ width: auto !important;
+ height: auto !important;
+ object-fit: contain !important;
+ display: block !important;
+ }
+ .ll-media-picker-preview .relative,
+ .relative.rounded-lg:has(> img.h-32.max-w-full) {
+ width: fit-content;
+ max-width: 15rem;
+ }
+
/* 筛选条件区:整体一行横排,重置紧跟应用按钮,控件紧凑 */
.fi-ta-filters-above-content-ctn { padding: 0.4rem 0.9rem !important; }
.fi-ta-filters-heading { display: none !important; }
diff --git a/app/Support/helpers.php b/app/Support/helpers.php
index a0be646..d785d29 100644
--- a/app/Support/helpers.php
+++ b/app/Support/helpers.php
@@ -33,6 +33,18 @@ if (! function_exists('theme')) {
}
}
+if (! function_exists('blog_paginate')) {
+ /**
+ * Front-end pager: fewer page chips so the sidebar column does not wrap under.
+ */
+ function blog_paginate(\Illuminate\Database\Eloquent\Builder|\Illuminate\Database\Eloquent\Relations\Relation $query, ?int $perPage = null): \Illuminate\Contracts\Pagination\LengthAwarePaginator
+ {
+ $perPage = max(1, (int) ($perPage ?? blog_setting('per_page', config('blog.per_page'))));
+
+ return $query->paginate($perPage)->onEachSide(1)->withQueryString();
+ }
+}
+
if (! function_exists('blog_setting')) {
/**
* 读取博客设置(Setting 表,带缓存)。
diff --git a/config/blog.php b/config/blog.php
index 5159021..a4cc03e 100644
--- a/config/blog.php
+++ b/config/blog.php
@@ -25,6 +25,8 @@ return [
'per_page' => 10,
+ 'home_view_mode' => 'list',
+
'rss_num' => 20,
'comment_min_len' => 2,
diff --git a/database/migrations/2026_09_18_210000_add_sticky_level_to_posts_table.php b/database/migrations/2026_09_18_210000_add_sticky_level_to_posts_table.php
new file mode 100644
index 0000000..efd58d6
--- /dev/null
+++ b/database/migrations/2026_09_18_210000_add_sticky_level_to_posts_table.php
@@ -0,0 +1,31 @@
+unsignedTinyInteger('sticky_level')->default(0)->after('is_sticky');
+ });
+ }
+
+ DB::table('posts')->where('is_sticky', true)->where('sticky_level', 0)->update(['sticky_level' => 1]);
+ }
+
+ public function down(): void
+ {
+ if (Schema::hasColumn('posts', 'sticky_level')) {
+ Schema::table('posts', function (Blueprint $table) {
+ $table->dropColumn('sticky_level');
+ });
+ }
+ }
+};
diff --git a/database/schema/mysql-schema.sql b/database/schema/mysql-schema.sql
index fa76276..3af0b28 100644
--- a/database/schema/mysql-schema.sql
+++ b/database/schema/mysql-schema.sql
@@ -392,6 +392,7 @@ CREATE TABLE `posts` (
`cover_media_id` bigint unsigned DEFAULT NULL,
`status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
`is_sticky` tinyint(1) NOT NULL DEFAULT '0',
+ `sticky_level` tinyint unsigned NOT NULL DEFAULT '0',
`close_comment` tinyint(1) NOT NULL DEFAULT '0',
`read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
`views` bigint unsigned NOT NULL DEFAULT '0',
@@ -580,3 +581,4 @@ INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_09_18_210000_add_sticky_level_to_posts_table',3);
diff --git a/database/seeders/DatabaseSeeder.php b/database/seeders/DatabaseSeeder.php
index e2a816f..ef70a9e 100644
--- a/database/seeders/DatabaseSeeder.php
+++ b/database/seeders/DatabaseSeeder.php
@@ -33,6 +33,7 @@ class DatabaseSeeder extends Seeder
'site_description' => config('blog.description'),
'site_icp' => config('blog.icp'),
'active_theme' => 'sablog',
+ 'home_view_mode' => 'list',
'comment_audit' => '0',
'comment_min_len' => (string) config('blog.comment_min_len'),
'comment_max_len' => (string) config('blog.comment_max_len'),
diff --git a/docs/architecture.md b/docs/architecture.md
index f12a4b0..ee5e729 100644
--- a/docs/architecture.md
+++ b/docs/architecture.md
@@ -27,7 +27,7 @@ app/
Providers/ # ThemeServiceProvider、PluginManagerServiceProvider
View/Composers/ # 侧边栏数据共享
Filament/ # 后台(Resources/Pages/Widgets)
- Console/Commands/ # sablog:import / attachments:sync-s3 / workerman:serve / content:convert / theme:publish
+ Console/Commands/ # sablog:import / sablog:fix-content / attachments:sync-s3 / workerman:serve / content:convert / theme:publish
Models/ # Post/Category/Comment/Link/Setting/User...
themes/ # 皮肤(theme.json + views + assets)
plugins/ # 插件(plugin.json + ServiceProvider + 迁移/路由/视图)
diff --git a/docs/database/mysql-schema-20260918-203827.sql b/docs/database/mysql-schema-20260918-203827.sql
new file mode 100644
index 0000000..fa76276
--- /dev/null
+++ b/docs/database/mysql-schema-20260918-203827.sql
@@ -0,0 +1,582 @@
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+DROP TABLE IF EXISTS `cache`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `cache` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `expiration` int NOT NULL,
+ PRIMARY KEY (`key`),
+ KEY `cache_expiration_index` (`expiration`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `cache_locks`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `cache_locks` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `expiration` int NOT NULL,
+ PRIMARY KEY (`key`),
+ KEY `cache_locks_expiration_index` (`expiration`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `categories`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `categories` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `display_order` tinyint NOT NULL DEFAULT '0',
+ `description` text COLLATE utf8mb4_unicode_ci,
+ `cover_media_id` bigint unsigned DEFAULT NULL,
+ `post_count` int unsigned NOT NULL DEFAULT '0',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `categories_slug_unique` (`slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `comments`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `comments` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `post_id` bigint unsigned NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `author_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `author_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `author_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `ip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `ai_review` json DEFAULT NULL,
+ `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `comments_user_id_foreign` (`user_id`),
+ KEY `comments_post_id_status_index` (`post_id`,`status`),
+ KEY `comments_status_index` (`status`),
+ CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `comments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `failed_jobs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `failed_jobs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `hello_world_logs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `hello_world_logs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `comment_id` bigint unsigned DEFAULT NULL,
+ `note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `hello_world_logs_comment_id_foreign` (`comment_id`),
+ CONSTRAINT `hello_world_logs_comment_id_foreign` FOREIGN KEY (`comment_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `job_batches`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `job_batches` (
+ `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `total_jobs` int NOT NULL,
+ `pending_jobs` int NOT NULL,
+ `failed_jobs` int NOT NULL,
+ `failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `options` mediumtext COLLATE utf8mb4_unicode_ci,
+ `cancelled_at` int DEFAULT NULL,
+ `created_at` int NOT NULL,
+ `finished_at` int DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `jobs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `jobs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `attempts` tinyint unsigned NOT NULL,
+ `reserved_at` int unsigned DEFAULT NULL,
+ `available_at` int unsigned NOT NULL,
+ `created_at` int unsigned NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `jobs_queue_index` (`queue`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `links`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `links` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `note` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `display_order` tinyint NOT NULL DEFAULT '0',
+ `visible` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `market_packages`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `market_packages` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `item_key` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `item_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `price` bigint unsigned NOT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `payment_id` bigint unsigned DEFAULT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `market_packages_item_key_item_type_unique` (`item_key`,`item_type`),
+ KEY `market_packages_payment_id_foreign` (`payment_id`),
+ KEY `market_packages_user_id_foreign` (`user_id`),
+ CONSTRAINT `market_packages_payment_id_foreign` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `market_packages_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ `uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `collection_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `conversions_disk` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `size` bigint unsigned NOT NULL,
+ `manipulations` json NOT NULL,
+ `custom_properties` json NOT NULL,
+ `generated_conversions` json NOT NULL,
+ `responsive_images` json NOT NULL,
+ `order_column` int unsigned DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_uuid_unique` (`uuid`),
+ KEY `media_model_type_model_id_index` (`model_type`,`model_id`),
+ KEY `media_order_column_index` (`order_column`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media_folders`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media_folders` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `parent_id` bigint unsigned DEFAULT NULL,
+ `code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `sort` int NOT NULL DEFAULT '0',
+ `is_active` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_folders_code_unique` (`code`),
+ KEY `media_folders_parent_id_foreign` (`parent_id`),
+ CONSTRAINT `media_folders_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `media_folders` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media_library`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media_library` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `disk` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public',
+ `path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `original_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `extension` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `size` bigint unsigned NOT NULL DEFAULT '0',
+ `type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'image',
+ `source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
+ `folder` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `alt_text` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `custom_properties` json DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `deleted_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_library_path_unique` (`path`),
+ KEY `media_library_user_id_foreign` (`user_id`),
+ KEY `media_library_type_index` (`type`),
+ KEY `media_library_folder_index` (`folder`),
+ KEY `media_library_source_index` (`source`),
+ CONSTRAINT `media_library_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `membership_plans`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `membership_plans` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `description` text COLLATE utf8mb4_unicode_ci,
+ `price` bigint unsigned NOT NULL,
+ `duration_days` int unsigned NOT NULL DEFAULT '30',
+ `permissions` json DEFAULT NULL,
+ `active` tinyint(1) NOT NULL DEFAULT '1',
+ `sort` int unsigned NOT NULL DEFAULT '0',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `membership_plans_slug_unique` (`slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `migrations`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `migrations` (
+ `id` int unsigned NOT NULL AUTO_INCREMENT,
+ `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `batch` int NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `model_has_permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `model_has_permissions` (
+ `permission_id` bigint unsigned NOT NULL,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
+ KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
+ CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `model_has_roles`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `model_has_roles` (
+ `role_id` bigint unsigned NOT NULL,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`role_id`,`model_id`,`model_type`),
+ KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
+ CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `password_reset_tokens`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `password_reset_tokens` (
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `payments`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `payments` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `order_no` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `amount` bigint unsigned NOT NULL,
+ `channel` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'alipay',
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `gateway_trade_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `payload` json DEFAULT NULL,
+ `paid_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `payable_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `payable_id` bigint unsigned DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `payments_order_no_unique` (`order_no`),
+ KEY `payments_user_id_status_index` (`user_id`,`status`),
+ KEY `payments_payable_type_payable_id_index` (`payable_type`,`payable_id`),
+ CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `permissions` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `personal_access_tokens`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `personal_access_tokens` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `tokenable_id` bigint unsigned NOT NULL,
+ `name` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `abilities` text COLLATE utf8mb4_unicode_ci,
+ `last_used_at` timestamp NULL DEFAULT NULL,
+ `expires_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
+ KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
+ KEY `personal_access_tokens_expires_at_index` (`expires_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `plugin_records`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `plugin_records` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `vendor` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `version` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `enabled` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `plugin_records_vendor_name_unique` (`vendor`,`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `posts`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `posts` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `category_id` bigint unsigned DEFAULT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `excerpt` text COLLATE utf8mb4_unicode_ci,
+ `content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `content_format` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'markdown',
+ `keywords` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `cover_media_id` bigint unsigned DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
+ `is_sticky` tinyint(1) NOT NULL DEFAULT '0',
+ `close_comment` tinyint(1) NOT NULL DEFAULT '0',
+ `read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `views` bigint unsigned NOT NULL DEFAULT '0',
+ `comment_count` int unsigned NOT NULL DEFAULT '0',
+ `meta` json DEFAULT NULL,
+ `published_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `posts_slug_unique` (`slug`),
+ KEY `posts_category_id_foreign` (`category_id`),
+ KEY `posts_user_id_foreign` (`user_id`),
+ KEY `posts_status_published_at_index` (`status`,`published_at`),
+ KEY `posts_status_index` (`status`),
+ KEY `posts_published_at_index` (`published_at`),
+ CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `redirects`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `redirects` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `hash` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `target_url` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `clicks` bigint unsigned NOT NULL DEFAULT '0',
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
+ `last_clicked_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `redirects_hash_unique` (`hash`),
+ KEY `redirects_target_url_index` (`target_url`(191))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `role_has_permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `role_has_permissions` (
+ `permission_id` bigint unsigned NOT NULL,
+ `role_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`permission_id`,`role_id`),
+ KEY `role_has_permissions_role_id_foreign` (`role_id`),
+ CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `roles`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `roles` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `sessions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `sessions` (
+ `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `user_agent` text COLLATE utf8mb4_unicode_ci,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `last_activity` int NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `sessions_user_id_index` (`user_id`),
+ KEY `sessions_last_activity_index` (`last_activity`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `settings`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `settings` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `value` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`key`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `subscriptions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `subscriptions` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` bigint unsigned NOT NULL,
+ `membership_plan_id` bigint unsigned DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
+ `starts_at` timestamp NULL DEFAULT NULL,
+ `ends_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `subscriptions_membership_plan_id_foreign` (`membership_plan_id`),
+ KEY `subscriptions_user_id_status_index` (`user_id`,`status`),
+ CONSTRAINT `subscriptions_membership_plan_id_foreign` FOREIGN KEY (`membership_plan_id`) REFERENCES `membership_plans` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `subscriptions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `taggables`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `taggables` (
+ `tag_id` bigint unsigned NOT NULL,
+ `taggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `taggable_id` bigint unsigned NOT NULL,
+ UNIQUE KEY `taggables_tag_id_taggable_id_taggable_type_unique` (`tag_id`,`taggable_id`,`taggable_type`),
+ KEY `taggables_taggable_type_taggable_id_index` (`taggable_type`,`taggable_id`),
+ CONSTRAINT `taggables_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `tags`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `tags` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` json NOT NULL,
+ `slug` json NOT NULL,
+ `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `order_column` int DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `users`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `users` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email_verified_at` timestamp NULL DEFAULT NULL,
+ `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `legacy_md5` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `logincount` int unsigned NOT NULL DEFAULT '0',
+ `loginip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `regip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `logintime` timestamp NULL DEFAULT NULL,
+ `lastpost_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `users_email_unique` (`email`),
+ KEY `users_legacy_md5_index` (`legacy_md5`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_08_11_092446_create_permission_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_08_11_092448_create_media_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_08_11_092449_create_tag_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_08_11_100000_create_membership_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_08_11_100000_create_payments_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_08_11_100001_create_categories_and_posts_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_08_11_100002_create_comments_and_trackbacks_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_08_11_100003_create_links_and_settings_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_08_11_100004_add_blog_columns_to_users_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_08_11_100005_add_content_format_to_posts_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_08_11_113537_create_personal_access_tokens_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_08_12_000000_create_hello_world_logs_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_08_12_100000_add_payable_to_payments_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2026_08_12_200000_create_market_packages_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2026_08_12_300000_add_description_to_categories_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_08_12_400000_create_redirects_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_08_15_000001_create_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_08_15_000002_create_media_folders_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000003_add_soft_deletes_to_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
diff --git a/docs/database/mysql-schema-20260918-205818.sql b/docs/database/mysql-schema-20260918-205818.sql
new file mode 100644
index 0000000..3af0b28
--- /dev/null
+++ b/docs/database/mysql-schema-20260918-205818.sql
@@ -0,0 +1,584 @@
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+DROP TABLE IF EXISTS `cache`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `cache` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `expiration` int NOT NULL,
+ PRIMARY KEY (`key`),
+ KEY `cache_expiration_index` (`expiration`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `cache_locks`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `cache_locks` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `expiration` int NOT NULL,
+ PRIMARY KEY (`key`),
+ KEY `cache_locks_expiration_index` (`expiration`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `categories`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `categories` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `display_order` tinyint NOT NULL DEFAULT '0',
+ `description` text COLLATE utf8mb4_unicode_ci,
+ `cover_media_id` bigint unsigned DEFAULT NULL,
+ `post_count` int unsigned NOT NULL DEFAULT '0',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `categories_slug_unique` (`slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `comments`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `comments` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `post_id` bigint unsigned NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `author_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `author_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `author_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `ip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `ai_review` json DEFAULT NULL,
+ `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `comments_user_id_foreign` (`user_id`),
+ KEY `comments_post_id_status_index` (`post_id`,`status`),
+ KEY `comments_status_index` (`status`),
+ CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `comments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `failed_jobs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `failed_jobs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `hello_world_logs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `hello_world_logs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `comment_id` bigint unsigned DEFAULT NULL,
+ `note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `hello_world_logs_comment_id_foreign` (`comment_id`),
+ CONSTRAINT `hello_world_logs_comment_id_foreign` FOREIGN KEY (`comment_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `job_batches`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `job_batches` (
+ `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `total_jobs` int NOT NULL,
+ `pending_jobs` int NOT NULL,
+ `failed_jobs` int NOT NULL,
+ `failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `options` mediumtext COLLATE utf8mb4_unicode_ci,
+ `cancelled_at` int DEFAULT NULL,
+ `created_at` int NOT NULL,
+ `finished_at` int DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `jobs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `jobs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `attempts` tinyint unsigned NOT NULL,
+ `reserved_at` int unsigned DEFAULT NULL,
+ `available_at` int unsigned NOT NULL,
+ `created_at` int unsigned NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `jobs_queue_index` (`queue`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `links`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `links` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `note` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `display_order` tinyint NOT NULL DEFAULT '0',
+ `visible` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `market_packages`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `market_packages` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `item_key` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `item_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `price` bigint unsigned NOT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `payment_id` bigint unsigned DEFAULT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `market_packages_item_key_item_type_unique` (`item_key`,`item_type`),
+ KEY `market_packages_payment_id_foreign` (`payment_id`),
+ KEY `market_packages_user_id_foreign` (`user_id`),
+ CONSTRAINT `market_packages_payment_id_foreign` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `market_packages_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ `uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `collection_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `conversions_disk` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `size` bigint unsigned NOT NULL,
+ `manipulations` json NOT NULL,
+ `custom_properties` json NOT NULL,
+ `generated_conversions` json NOT NULL,
+ `responsive_images` json NOT NULL,
+ `order_column` int unsigned DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_uuid_unique` (`uuid`),
+ KEY `media_model_type_model_id_index` (`model_type`,`model_id`),
+ KEY `media_order_column_index` (`order_column`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media_folders`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media_folders` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `parent_id` bigint unsigned DEFAULT NULL,
+ `code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `sort` int NOT NULL DEFAULT '0',
+ `is_active` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_folders_code_unique` (`code`),
+ KEY `media_folders_parent_id_foreign` (`parent_id`),
+ CONSTRAINT `media_folders_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `media_folders` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media_library`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media_library` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `disk` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public',
+ `path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `original_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `extension` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `size` bigint unsigned NOT NULL DEFAULT '0',
+ `type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'image',
+ `source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
+ `folder` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `alt_text` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `custom_properties` json DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `deleted_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_library_path_unique` (`path`),
+ KEY `media_library_user_id_foreign` (`user_id`),
+ KEY `media_library_type_index` (`type`),
+ KEY `media_library_folder_index` (`folder`),
+ KEY `media_library_source_index` (`source`),
+ CONSTRAINT `media_library_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `membership_plans`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `membership_plans` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `description` text COLLATE utf8mb4_unicode_ci,
+ `price` bigint unsigned NOT NULL,
+ `duration_days` int unsigned NOT NULL DEFAULT '30',
+ `permissions` json DEFAULT NULL,
+ `active` tinyint(1) NOT NULL DEFAULT '1',
+ `sort` int unsigned NOT NULL DEFAULT '0',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `membership_plans_slug_unique` (`slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `migrations`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `migrations` (
+ `id` int unsigned NOT NULL AUTO_INCREMENT,
+ `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `batch` int NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `model_has_permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `model_has_permissions` (
+ `permission_id` bigint unsigned NOT NULL,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
+ KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
+ CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `model_has_roles`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `model_has_roles` (
+ `role_id` bigint unsigned NOT NULL,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`role_id`,`model_id`,`model_type`),
+ KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
+ CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `password_reset_tokens`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `password_reset_tokens` (
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `payments`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `payments` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `order_no` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `amount` bigint unsigned NOT NULL,
+ `channel` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'alipay',
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `gateway_trade_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `payload` json DEFAULT NULL,
+ `paid_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `payable_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `payable_id` bigint unsigned DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `payments_order_no_unique` (`order_no`),
+ KEY `payments_user_id_status_index` (`user_id`,`status`),
+ KEY `payments_payable_type_payable_id_index` (`payable_type`,`payable_id`),
+ CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `permissions` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `personal_access_tokens`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `personal_access_tokens` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `tokenable_id` bigint unsigned NOT NULL,
+ `name` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `abilities` text COLLATE utf8mb4_unicode_ci,
+ `last_used_at` timestamp NULL DEFAULT NULL,
+ `expires_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
+ KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
+ KEY `personal_access_tokens_expires_at_index` (`expires_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `plugin_records`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `plugin_records` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `vendor` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `version` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `enabled` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `plugin_records_vendor_name_unique` (`vendor`,`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `posts`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `posts` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `category_id` bigint unsigned DEFAULT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `excerpt` text COLLATE utf8mb4_unicode_ci,
+ `content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `content_format` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'markdown',
+ `keywords` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `cover_media_id` bigint unsigned DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
+ `is_sticky` tinyint(1) NOT NULL DEFAULT '0',
+ `sticky_level` tinyint unsigned NOT NULL DEFAULT '0',
+ `close_comment` tinyint(1) NOT NULL DEFAULT '0',
+ `read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `views` bigint unsigned NOT NULL DEFAULT '0',
+ `comment_count` int unsigned NOT NULL DEFAULT '0',
+ `meta` json DEFAULT NULL,
+ `published_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `posts_slug_unique` (`slug`),
+ KEY `posts_category_id_foreign` (`category_id`),
+ KEY `posts_user_id_foreign` (`user_id`),
+ KEY `posts_status_published_at_index` (`status`,`published_at`),
+ KEY `posts_status_index` (`status`),
+ KEY `posts_published_at_index` (`published_at`),
+ CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `redirects`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `redirects` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `hash` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `target_url` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `clicks` bigint unsigned NOT NULL DEFAULT '0',
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
+ `last_clicked_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `redirects_hash_unique` (`hash`),
+ KEY `redirects_target_url_index` (`target_url`(191))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `role_has_permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `role_has_permissions` (
+ `permission_id` bigint unsigned NOT NULL,
+ `role_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`permission_id`,`role_id`),
+ KEY `role_has_permissions_role_id_foreign` (`role_id`),
+ CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `roles`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `roles` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `sessions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `sessions` (
+ `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `user_agent` text COLLATE utf8mb4_unicode_ci,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `last_activity` int NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `sessions_user_id_index` (`user_id`),
+ KEY `sessions_last_activity_index` (`last_activity`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `settings`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `settings` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `value` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`key`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `subscriptions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `subscriptions` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` bigint unsigned NOT NULL,
+ `membership_plan_id` bigint unsigned DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
+ `starts_at` timestamp NULL DEFAULT NULL,
+ `ends_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `subscriptions_membership_plan_id_foreign` (`membership_plan_id`),
+ KEY `subscriptions_user_id_status_index` (`user_id`,`status`),
+ CONSTRAINT `subscriptions_membership_plan_id_foreign` FOREIGN KEY (`membership_plan_id`) REFERENCES `membership_plans` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `subscriptions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `taggables`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `taggables` (
+ `tag_id` bigint unsigned NOT NULL,
+ `taggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `taggable_id` bigint unsigned NOT NULL,
+ UNIQUE KEY `taggables_tag_id_taggable_id_taggable_type_unique` (`tag_id`,`taggable_id`,`taggable_type`),
+ KEY `taggables_taggable_type_taggable_id_index` (`taggable_type`,`taggable_id`),
+ CONSTRAINT `taggables_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `tags`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `tags` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` json NOT NULL,
+ `slug` json NOT NULL,
+ `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `order_column` int DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `users`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `users` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email_verified_at` timestamp NULL DEFAULT NULL,
+ `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `legacy_md5` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `logincount` int unsigned NOT NULL DEFAULT '0',
+ `loginip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `regip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `logintime` timestamp NULL DEFAULT NULL,
+ `lastpost_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `users_email_unique` (`email`),
+ KEY `users_legacy_md5_index` (`legacy_md5`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_08_11_092446_create_permission_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_08_11_092448_create_media_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_08_11_092449_create_tag_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_08_11_100000_create_membership_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_08_11_100000_create_payments_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_08_11_100001_create_categories_and_posts_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_08_11_100002_create_comments_and_trackbacks_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_08_11_100003_create_links_and_settings_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_08_11_100004_add_blog_columns_to_users_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_08_11_100005_add_content_format_to_posts_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_08_11_113537_create_personal_access_tokens_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_08_12_000000_create_hello_world_logs_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_08_12_100000_add_payable_to_payments_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2026_08_12_200000_create_market_packages_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2026_08_12_300000_add_description_to_categories_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_08_12_400000_create_redirects_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_08_15_000001_create_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_08_15_000002_create_media_folders_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000003_add_soft_deletes_to_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_09_18_210000_add_sticky_level_to_posts_table',3);
diff --git a/docs/database/mysql-schema-20260918-210305.sql b/docs/database/mysql-schema-20260918-210305.sql
new file mode 100644
index 0000000..3af0b28
--- /dev/null
+++ b/docs/database/mysql-schema-20260918-210305.sql
@@ -0,0 +1,584 @@
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+DROP TABLE IF EXISTS `cache`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `cache` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `expiration` int NOT NULL,
+ PRIMARY KEY (`key`),
+ KEY `cache_expiration_index` (`expiration`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `cache_locks`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `cache_locks` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `expiration` int NOT NULL,
+ PRIMARY KEY (`key`),
+ KEY `cache_locks_expiration_index` (`expiration`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `categories`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `categories` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `display_order` tinyint NOT NULL DEFAULT '0',
+ `description` text COLLATE utf8mb4_unicode_ci,
+ `cover_media_id` bigint unsigned DEFAULT NULL,
+ `post_count` int unsigned NOT NULL DEFAULT '0',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `categories_slug_unique` (`slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `comments`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `comments` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `post_id` bigint unsigned NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `author_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `author_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `author_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `ip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `ai_review` json DEFAULT NULL,
+ `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `comments_user_id_foreign` (`user_id`),
+ KEY `comments_post_id_status_index` (`post_id`,`status`),
+ KEY `comments_status_index` (`status`),
+ CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `comments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `failed_jobs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `failed_jobs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `hello_world_logs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `hello_world_logs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `comment_id` bigint unsigned DEFAULT NULL,
+ `note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `hello_world_logs_comment_id_foreign` (`comment_id`),
+ CONSTRAINT `hello_world_logs_comment_id_foreign` FOREIGN KEY (`comment_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `job_batches`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `job_batches` (
+ `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `total_jobs` int NOT NULL,
+ `pending_jobs` int NOT NULL,
+ `failed_jobs` int NOT NULL,
+ `failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `options` mediumtext COLLATE utf8mb4_unicode_ci,
+ `cancelled_at` int DEFAULT NULL,
+ `created_at` int NOT NULL,
+ `finished_at` int DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `jobs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `jobs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `attempts` tinyint unsigned NOT NULL,
+ `reserved_at` int unsigned DEFAULT NULL,
+ `available_at` int unsigned NOT NULL,
+ `created_at` int unsigned NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `jobs_queue_index` (`queue`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `links`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `links` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `note` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `display_order` tinyint NOT NULL DEFAULT '0',
+ `visible` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `market_packages`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `market_packages` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `item_key` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `item_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `price` bigint unsigned NOT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `payment_id` bigint unsigned DEFAULT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `market_packages_item_key_item_type_unique` (`item_key`,`item_type`),
+ KEY `market_packages_payment_id_foreign` (`payment_id`),
+ KEY `market_packages_user_id_foreign` (`user_id`),
+ CONSTRAINT `market_packages_payment_id_foreign` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `market_packages_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ `uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `collection_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `conversions_disk` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `size` bigint unsigned NOT NULL,
+ `manipulations` json NOT NULL,
+ `custom_properties` json NOT NULL,
+ `generated_conversions` json NOT NULL,
+ `responsive_images` json NOT NULL,
+ `order_column` int unsigned DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_uuid_unique` (`uuid`),
+ KEY `media_model_type_model_id_index` (`model_type`,`model_id`),
+ KEY `media_order_column_index` (`order_column`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media_folders`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media_folders` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `parent_id` bigint unsigned DEFAULT NULL,
+ `code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `sort` int NOT NULL DEFAULT '0',
+ `is_active` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_folders_code_unique` (`code`),
+ KEY `media_folders_parent_id_foreign` (`parent_id`),
+ CONSTRAINT `media_folders_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `media_folders` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media_library`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media_library` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `disk` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public',
+ `path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `original_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `extension` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `size` bigint unsigned NOT NULL DEFAULT '0',
+ `type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'image',
+ `source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
+ `folder` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `alt_text` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `custom_properties` json DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `deleted_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_library_path_unique` (`path`),
+ KEY `media_library_user_id_foreign` (`user_id`),
+ KEY `media_library_type_index` (`type`),
+ KEY `media_library_folder_index` (`folder`),
+ KEY `media_library_source_index` (`source`),
+ CONSTRAINT `media_library_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `membership_plans`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `membership_plans` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `description` text COLLATE utf8mb4_unicode_ci,
+ `price` bigint unsigned NOT NULL,
+ `duration_days` int unsigned NOT NULL DEFAULT '30',
+ `permissions` json DEFAULT NULL,
+ `active` tinyint(1) NOT NULL DEFAULT '1',
+ `sort` int unsigned NOT NULL DEFAULT '0',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `membership_plans_slug_unique` (`slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `migrations`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `migrations` (
+ `id` int unsigned NOT NULL AUTO_INCREMENT,
+ `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `batch` int NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `model_has_permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `model_has_permissions` (
+ `permission_id` bigint unsigned NOT NULL,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
+ KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
+ CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `model_has_roles`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `model_has_roles` (
+ `role_id` bigint unsigned NOT NULL,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`role_id`,`model_id`,`model_type`),
+ KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
+ CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `password_reset_tokens`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `password_reset_tokens` (
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `payments`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `payments` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `order_no` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `amount` bigint unsigned NOT NULL,
+ `channel` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'alipay',
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `gateway_trade_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `payload` json DEFAULT NULL,
+ `paid_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `payable_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `payable_id` bigint unsigned DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `payments_order_no_unique` (`order_no`),
+ KEY `payments_user_id_status_index` (`user_id`,`status`),
+ KEY `payments_payable_type_payable_id_index` (`payable_type`,`payable_id`),
+ CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `permissions` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `personal_access_tokens`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `personal_access_tokens` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `tokenable_id` bigint unsigned NOT NULL,
+ `name` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `abilities` text COLLATE utf8mb4_unicode_ci,
+ `last_used_at` timestamp NULL DEFAULT NULL,
+ `expires_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
+ KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
+ KEY `personal_access_tokens_expires_at_index` (`expires_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `plugin_records`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `plugin_records` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `vendor` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `version` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `enabled` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `plugin_records_vendor_name_unique` (`vendor`,`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `posts`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `posts` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `category_id` bigint unsigned DEFAULT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `excerpt` text COLLATE utf8mb4_unicode_ci,
+ `content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `content_format` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'markdown',
+ `keywords` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `cover_media_id` bigint unsigned DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
+ `is_sticky` tinyint(1) NOT NULL DEFAULT '0',
+ `sticky_level` tinyint unsigned NOT NULL DEFAULT '0',
+ `close_comment` tinyint(1) NOT NULL DEFAULT '0',
+ `read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `views` bigint unsigned NOT NULL DEFAULT '0',
+ `comment_count` int unsigned NOT NULL DEFAULT '0',
+ `meta` json DEFAULT NULL,
+ `published_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `posts_slug_unique` (`slug`),
+ KEY `posts_category_id_foreign` (`category_id`),
+ KEY `posts_user_id_foreign` (`user_id`),
+ KEY `posts_status_published_at_index` (`status`,`published_at`),
+ KEY `posts_status_index` (`status`),
+ KEY `posts_published_at_index` (`published_at`),
+ CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `redirects`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `redirects` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `hash` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `target_url` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `clicks` bigint unsigned NOT NULL DEFAULT '0',
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
+ `last_clicked_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `redirects_hash_unique` (`hash`),
+ KEY `redirects_target_url_index` (`target_url`(191))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `role_has_permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `role_has_permissions` (
+ `permission_id` bigint unsigned NOT NULL,
+ `role_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`permission_id`,`role_id`),
+ KEY `role_has_permissions_role_id_foreign` (`role_id`),
+ CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `roles`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `roles` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `sessions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `sessions` (
+ `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `user_agent` text COLLATE utf8mb4_unicode_ci,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `last_activity` int NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `sessions_user_id_index` (`user_id`),
+ KEY `sessions_last_activity_index` (`last_activity`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `settings`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `settings` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `value` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`key`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `subscriptions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `subscriptions` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` bigint unsigned NOT NULL,
+ `membership_plan_id` bigint unsigned DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
+ `starts_at` timestamp NULL DEFAULT NULL,
+ `ends_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `subscriptions_membership_plan_id_foreign` (`membership_plan_id`),
+ KEY `subscriptions_user_id_status_index` (`user_id`,`status`),
+ CONSTRAINT `subscriptions_membership_plan_id_foreign` FOREIGN KEY (`membership_plan_id`) REFERENCES `membership_plans` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `subscriptions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `taggables`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `taggables` (
+ `tag_id` bigint unsigned NOT NULL,
+ `taggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `taggable_id` bigint unsigned NOT NULL,
+ UNIQUE KEY `taggables_tag_id_taggable_id_taggable_type_unique` (`tag_id`,`taggable_id`,`taggable_type`),
+ KEY `taggables_taggable_type_taggable_id_index` (`taggable_type`,`taggable_id`),
+ CONSTRAINT `taggables_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `tags`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `tags` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` json NOT NULL,
+ `slug` json NOT NULL,
+ `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `order_column` int DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `users`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `users` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email_verified_at` timestamp NULL DEFAULT NULL,
+ `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `legacy_md5` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `logincount` int unsigned NOT NULL DEFAULT '0',
+ `loginip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `regip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `logintime` timestamp NULL DEFAULT NULL,
+ `lastpost_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `users_email_unique` (`email`),
+ KEY `users_legacy_md5_index` (`legacy_md5`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_08_11_092446_create_permission_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_08_11_092448_create_media_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_08_11_092449_create_tag_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_08_11_100000_create_membership_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_08_11_100000_create_payments_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_08_11_100001_create_categories_and_posts_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_08_11_100002_create_comments_and_trackbacks_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_08_11_100003_create_links_and_settings_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_08_11_100004_add_blog_columns_to_users_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_08_11_100005_add_content_format_to_posts_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_08_11_113537_create_personal_access_tokens_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_08_12_000000_create_hello_world_logs_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_08_12_100000_add_payable_to_payments_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2026_08_12_200000_create_market_packages_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2026_08_12_300000_add_description_to_categories_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_08_12_400000_create_redirects_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_08_15_000001_create_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_08_15_000002_create_media_folders_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000003_add_soft_deletes_to_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_09_18_210000_add_sticky_level_to_posts_table',3);
diff --git a/docs/database/mysql-schema-20260918-210847.sql b/docs/database/mysql-schema-20260918-210847.sql
new file mode 100644
index 0000000..3af0b28
--- /dev/null
+++ b/docs/database/mysql-schema-20260918-210847.sql
@@ -0,0 +1,584 @@
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+DROP TABLE IF EXISTS `cache`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `cache` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `expiration` int NOT NULL,
+ PRIMARY KEY (`key`),
+ KEY `cache_expiration_index` (`expiration`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `cache_locks`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `cache_locks` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `expiration` int NOT NULL,
+ PRIMARY KEY (`key`),
+ KEY `cache_locks_expiration_index` (`expiration`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `categories`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `categories` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `display_order` tinyint NOT NULL DEFAULT '0',
+ `description` text COLLATE utf8mb4_unicode_ci,
+ `cover_media_id` bigint unsigned DEFAULT NULL,
+ `post_count` int unsigned NOT NULL DEFAULT '0',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `categories_slug_unique` (`slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `comments`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `comments` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `post_id` bigint unsigned NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `author_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `author_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `author_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `ip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `ai_review` json DEFAULT NULL,
+ `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `comments_user_id_foreign` (`user_id`),
+ KEY `comments_post_id_status_index` (`post_id`,`status`),
+ KEY `comments_status_index` (`status`),
+ CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `comments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `failed_jobs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `failed_jobs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `hello_world_logs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `hello_world_logs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `comment_id` bigint unsigned DEFAULT NULL,
+ `note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `hello_world_logs_comment_id_foreign` (`comment_id`),
+ CONSTRAINT `hello_world_logs_comment_id_foreign` FOREIGN KEY (`comment_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `job_batches`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `job_batches` (
+ `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `total_jobs` int NOT NULL,
+ `pending_jobs` int NOT NULL,
+ `failed_jobs` int NOT NULL,
+ `failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `options` mediumtext COLLATE utf8mb4_unicode_ci,
+ `cancelled_at` int DEFAULT NULL,
+ `created_at` int NOT NULL,
+ `finished_at` int DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `jobs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `jobs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `attempts` tinyint unsigned NOT NULL,
+ `reserved_at` int unsigned DEFAULT NULL,
+ `available_at` int unsigned NOT NULL,
+ `created_at` int unsigned NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `jobs_queue_index` (`queue`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `links`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `links` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `note` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `display_order` tinyint NOT NULL DEFAULT '0',
+ `visible` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `market_packages`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `market_packages` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `item_key` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `item_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `price` bigint unsigned NOT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `payment_id` bigint unsigned DEFAULT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `market_packages_item_key_item_type_unique` (`item_key`,`item_type`),
+ KEY `market_packages_payment_id_foreign` (`payment_id`),
+ KEY `market_packages_user_id_foreign` (`user_id`),
+ CONSTRAINT `market_packages_payment_id_foreign` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `market_packages_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ `uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `collection_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `conversions_disk` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `size` bigint unsigned NOT NULL,
+ `manipulations` json NOT NULL,
+ `custom_properties` json NOT NULL,
+ `generated_conversions` json NOT NULL,
+ `responsive_images` json NOT NULL,
+ `order_column` int unsigned DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_uuid_unique` (`uuid`),
+ KEY `media_model_type_model_id_index` (`model_type`,`model_id`),
+ KEY `media_order_column_index` (`order_column`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media_folders`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media_folders` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `parent_id` bigint unsigned DEFAULT NULL,
+ `code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `sort` int NOT NULL DEFAULT '0',
+ `is_active` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_folders_code_unique` (`code`),
+ KEY `media_folders_parent_id_foreign` (`parent_id`),
+ CONSTRAINT `media_folders_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `media_folders` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media_library`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media_library` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `disk` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public',
+ `path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `original_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `extension` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `size` bigint unsigned NOT NULL DEFAULT '0',
+ `type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'image',
+ `source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
+ `folder` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `alt_text` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `custom_properties` json DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `deleted_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_library_path_unique` (`path`),
+ KEY `media_library_user_id_foreign` (`user_id`),
+ KEY `media_library_type_index` (`type`),
+ KEY `media_library_folder_index` (`folder`),
+ KEY `media_library_source_index` (`source`),
+ CONSTRAINT `media_library_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `membership_plans`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `membership_plans` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `description` text COLLATE utf8mb4_unicode_ci,
+ `price` bigint unsigned NOT NULL,
+ `duration_days` int unsigned NOT NULL DEFAULT '30',
+ `permissions` json DEFAULT NULL,
+ `active` tinyint(1) NOT NULL DEFAULT '1',
+ `sort` int unsigned NOT NULL DEFAULT '0',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `membership_plans_slug_unique` (`slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `migrations`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `migrations` (
+ `id` int unsigned NOT NULL AUTO_INCREMENT,
+ `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `batch` int NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `model_has_permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `model_has_permissions` (
+ `permission_id` bigint unsigned NOT NULL,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
+ KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
+ CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `model_has_roles`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `model_has_roles` (
+ `role_id` bigint unsigned NOT NULL,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`role_id`,`model_id`,`model_type`),
+ KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
+ CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `password_reset_tokens`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `password_reset_tokens` (
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `payments`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `payments` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `order_no` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `amount` bigint unsigned NOT NULL,
+ `channel` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'alipay',
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `gateway_trade_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `payload` json DEFAULT NULL,
+ `paid_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `payable_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `payable_id` bigint unsigned DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `payments_order_no_unique` (`order_no`),
+ KEY `payments_user_id_status_index` (`user_id`,`status`),
+ KEY `payments_payable_type_payable_id_index` (`payable_type`,`payable_id`),
+ CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `permissions` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `personal_access_tokens`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `personal_access_tokens` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `tokenable_id` bigint unsigned NOT NULL,
+ `name` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `abilities` text COLLATE utf8mb4_unicode_ci,
+ `last_used_at` timestamp NULL DEFAULT NULL,
+ `expires_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
+ KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
+ KEY `personal_access_tokens_expires_at_index` (`expires_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `plugin_records`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `plugin_records` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `vendor` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `version` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `enabled` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `plugin_records_vendor_name_unique` (`vendor`,`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `posts`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `posts` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `category_id` bigint unsigned DEFAULT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `excerpt` text COLLATE utf8mb4_unicode_ci,
+ `content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `content_format` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'markdown',
+ `keywords` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `cover_media_id` bigint unsigned DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
+ `is_sticky` tinyint(1) NOT NULL DEFAULT '0',
+ `sticky_level` tinyint unsigned NOT NULL DEFAULT '0',
+ `close_comment` tinyint(1) NOT NULL DEFAULT '0',
+ `read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `views` bigint unsigned NOT NULL DEFAULT '0',
+ `comment_count` int unsigned NOT NULL DEFAULT '0',
+ `meta` json DEFAULT NULL,
+ `published_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `posts_slug_unique` (`slug`),
+ KEY `posts_category_id_foreign` (`category_id`),
+ KEY `posts_user_id_foreign` (`user_id`),
+ KEY `posts_status_published_at_index` (`status`,`published_at`),
+ KEY `posts_status_index` (`status`),
+ KEY `posts_published_at_index` (`published_at`),
+ CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `redirects`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `redirects` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `hash` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `target_url` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `clicks` bigint unsigned NOT NULL DEFAULT '0',
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
+ `last_clicked_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `redirects_hash_unique` (`hash`),
+ KEY `redirects_target_url_index` (`target_url`(191))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `role_has_permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `role_has_permissions` (
+ `permission_id` bigint unsigned NOT NULL,
+ `role_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`permission_id`,`role_id`),
+ KEY `role_has_permissions_role_id_foreign` (`role_id`),
+ CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `roles`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `roles` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `sessions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `sessions` (
+ `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `user_agent` text COLLATE utf8mb4_unicode_ci,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `last_activity` int NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `sessions_user_id_index` (`user_id`),
+ KEY `sessions_last_activity_index` (`last_activity`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `settings`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `settings` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `value` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`key`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `subscriptions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `subscriptions` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` bigint unsigned NOT NULL,
+ `membership_plan_id` bigint unsigned DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
+ `starts_at` timestamp NULL DEFAULT NULL,
+ `ends_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `subscriptions_membership_plan_id_foreign` (`membership_plan_id`),
+ KEY `subscriptions_user_id_status_index` (`user_id`,`status`),
+ CONSTRAINT `subscriptions_membership_plan_id_foreign` FOREIGN KEY (`membership_plan_id`) REFERENCES `membership_plans` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `subscriptions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `taggables`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `taggables` (
+ `tag_id` bigint unsigned NOT NULL,
+ `taggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `taggable_id` bigint unsigned NOT NULL,
+ UNIQUE KEY `taggables_tag_id_taggable_id_taggable_type_unique` (`tag_id`,`taggable_id`,`taggable_type`),
+ KEY `taggables_taggable_type_taggable_id_index` (`taggable_type`,`taggable_id`),
+ CONSTRAINT `taggables_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `tags`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `tags` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` json NOT NULL,
+ `slug` json NOT NULL,
+ `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `order_column` int DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `users`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `users` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email_verified_at` timestamp NULL DEFAULT NULL,
+ `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `legacy_md5` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `logincount` int unsigned NOT NULL DEFAULT '0',
+ `loginip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `regip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `logintime` timestamp NULL DEFAULT NULL,
+ `lastpost_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `users_email_unique` (`email`),
+ KEY `users_legacy_md5_index` (`legacy_md5`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_08_11_092446_create_permission_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_08_11_092448_create_media_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_08_11_092449_create_tag_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_08_11_100000_create_membership_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_08_11_100000_create_payments_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_08_11_100001_create_categories_and_posts_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_08_11_100002_create_comments_and_trackbacks_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_08_11_100003_create_links_and_settings_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_08_11_100004_add_blog_columns_to_users_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_08_11_100005_add_content_format_to_posts_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_08_11_113537_create_personal_access_tokens_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_08_12_000000_create_hello_world_logs_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_08_12_100000_add_payable_to_payments_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2026_08_12_200000_create_market_packages_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2026_08_12_300000_add_description_to_categories_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_08_12_400000_create_redirects_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_08_15_000001_create_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_08_15_000002_create_media_folders_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000003_add_soft_deletes_to_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_09_18_210000_add_sticky_level_to_posts_table',3);
diff --git a/docs/database/mysql-schema-20260918-211824.sql b/docs/database/mysql-schema-20260918-211824.sql
new file mode 100644
index 0000000..3af0b28
--- /dev/null
+++ b/docs/database/mysql-schema-20260918-211824.sql
@@ -0,0 +1,584 @@
+/*!40103 SET @OLD_TIME_ZONE=@@TIME_ZONE */;
+/*!40103 SET TIME_ZONE='+00:00' */;
+/*!40014 SET @OLD_UNIQUE_CHECKS=@@UNIQUE_CHECKS, UNIQUE_CHECKS=0 */;
+/*!40014 SET @OLD_FOREIGN_KEY_CHECKS=@@FOREIGN_KEY_CHECKS, FOREIGN_KEY_CHECKS=0 */;
+/*!40101 SET @OLD_SQL_MODE=@@SQL_MODE, SQL_MODE='NO_AUTO_VALUE_ON_ZERO' */;
+/*!40111 SET @OLD_SQL_NOTES=@@SQL_NOTES, SQL_NOTES=0 */;
+DROP TABLE IF EXISTS `cache`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `cache` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `value` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `expiration` int NOT NULL,
+ PRIMARY KEY (`key`),
+ KEY `cache_expiration_index` (`expiration`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `cache_locks`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `cache_locks` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `owner` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `expiration` int NOT NULL,
+ PRIMARY KEY (`key`),
+ KEY `cache_locks_expiration_index` (`expiration`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `categories`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `categories` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(120) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `display_order` tinyint NOT NULL DEFAULT '0',
+ `description` text COLLATE utf8mb4_unicode_ci,
+ `cover_media_id` bigint unsigned DEFAULT NULL,
+ `post_count` int unsigned NOT NULL DEFAULT '0',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `categories_slug_unique` (`slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `comments`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `comments` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `post_id` bigint unsigned NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `author_name` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `author_email` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `author_url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `content` mediumtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `ip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `ai_review` json DEFAULT NULL,
+ `created_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ KEY `comments_user_id_foreign` (`user_id`),
+ KEY `comments_post_id_status_index` (`post_id`,`status`),
+ KEY `comments_status_index` (`status`),
+ CONSTRAINT `comments_post_id_foreign` FOREIGN KEY (`post_id`) REFERENCES `posts` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `comments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `failed_jobs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `failed_jobs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `uuid` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `connection` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `queue` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `exception` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `failed_at` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `failed_jobs_uuid_unique` (`uuid`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `hello_world_logs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `hello_world_logs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `comment_id` bigint unsigned DEFAULT NULL,
+ `note` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `hello_world_logs_comment_id_foreign` (`comment_id`),
+ CONSTRAINT `hello_world_logs_comment_id_foreign` FOREIGN KEY (`comment_id`) REFERENCES `comments` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `job_batches`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `job_batches` (
+ `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `total_jobs` int NOT NULL,
+ `pending_jobs` int NOT NULL,
+ `failed_jobs` int NOT NULL,
+ `failed_job_ids` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `options` mediumtext COLLATE utf8mb4_unicode_ci,
+ `cancelled_at` int DEFAULT NULL,
+ `created_at` int NOT NULL,
+ `finished_at` int DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `jobs`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `jobs` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `queue` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `attempts` tinyint unsigned NOT NULL,
+ `reserved_at` int unsigned DEFAULT NULL,
+ `available_at` int unsigned NOT NULL,
+ `created_at` int unsigned NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `jobs_queue_index` (`queue`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `links`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `links` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(100) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `url` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `note` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `display_order` tinyint NOT NULL DEFAULT '0',
+ `visible` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `market_packages`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `market_packages` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `item_key` varchar(190) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `item_type` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `price` bigint unsigned NOT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `payment_id` bigint unsigned DEFAULT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `market_packages_item_key_item_type_unique` (`item_key`,`item_type`),
+ KEY `market_packages_payment_id_foreign` (`payment_id`),
+ KEY `market_packages_user_id_foreign` (`user_id`),
+ CONSTRAINT `market_packages_payment_id_foreign` FOREIGN KEY (`payment_id`) REFERENCES `payments` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `market_packages_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ `uuid` char(36) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `collection_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `file_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `disk` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `conversions_disk` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `size` bigint unsigned NOT NULL,
+ `manipulations` json NOT NULL,
+ `custom_properties` json NOT NULL,
+ `generated_conversions` json NOT NULL,
+ `responsive_images` json NOT NULL,
+ `order_column` int unsigned DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_uuid_unique` (`uuid`),
+ KEY `media_model_type_model_id_index` (`model_type`,`model_id`),
+ KEY `media_order_column_index` (`order_column`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media_folders`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media_folders` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `parent_id` bigint unsigned DEFAULT NULL,
+ `code` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `sort` int NOT NULL DEFAULT '0',
+ `is_active` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_folders_code_unique` (`code`),
+ KEY `media_folders_parent_id_foreign` (`parent_id`),
+ CONSTRAINT `media_folders_parent_id_foreign` FOREIGN KEY (`parent_id`) REFERENCES `media_folders` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `media_library`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `media_library` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `disk` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'public',
+ `path` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `original_name` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `extension` varchar(20) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `mime_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `size` bigint unsigned NOT NULL DEFAULT '0',
+ `type` varchar(30) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'image',
+ `source` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'admin',
+ `folder` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `alt_text` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `custom_properties` json DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `deleted_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `media_library_path_unique` (`path`),
+ KEY `media_library_user_id_foreign` (`user_id`),
+ KEY `media_library_type_index` (`type`),
+ KEY `media_library_folder_index` (`folder`),
+ KEY `media_library_source_index` (`source`),
+ CONSTRAINT `media_library_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `membership_plans`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `membership_plans` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `description` text COLLATE utf8mb4_unicode_ci,
+ `price` bigint unsigned NOT NULL,
+ `duration_days` int unsigned NOT NULL DEFAULT '30',
+ `permissions` json DEFAULT NULL,
+ `active` tinyint(1) NOT NULL DEFAULT '1',
+ `sort` int unsigned NOT NULL DEFAULT '0',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `membership_plans_slug_unique` (`slug`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `migrations`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `migrations` (
+ `id` int unsigned NOT NULL AUTO_INCREMENT,
+ `migration` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `batch` int NOT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `model_has_permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `model_has_permissions` (
+ `permission_id` bigint unsigned NOT NULL,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`permission_id`,`model_id`,`model_type`),
+ KEY `model_has_permissions_model_id_model_type_index` (`model_id`,`model_type`),
+ CONSTRAINT `model_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `model_has_roles`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `model_has_roles` (
+ `role_id` bigint unsigned NOT NULL,
+ `model_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `model_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`role_id`,`model_id`,`model_type`),
+ KEY `model_has_roles_model_id_model_type_index` (`model_id`,`model_type`),
+ CONSTRAINT `model_has_roles_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `password_reset_tokens`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `password_reset_tokens` (
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `token` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`email`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `payments`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `payments` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `order_no` varchar(40) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `subject` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `description` varchar(500) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `amount` bigint unsigned NOT NULL,
+ `channel` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'alipay',
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'pending',
+ `gateway_trade_no` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `payload` json DEFAULT NULL,
+ `paid_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `payable_type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `payable_id` bigint unsigned DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `payments_order_no_unique` (`order_no`),
+ KEY `payments_user_id_status_index` (`user_id`,`status`),
+ KEY `payments_payable_type_payable_id_index` (`payable_type`,`payable_id`),
+ CONSTRAINT `payments_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `permissions` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `permissions_name_guard_name_unique` (`name`,`guard_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `personal_access_tokens`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `personal_access_tokens` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `tokenable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `tokenable_id` bigint unsigned NOT NULL,
+ `name` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `token` varchar(64) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `abilities` text COLLATE utf8mb4_unicode_ci,
+ `last_used_at` timestamp NULL DEFAULT NULL,
+ `expires_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `personal_access_tokens_token_unique` (`token`),
+ KEY `personal_access_tokens_tokenable_type_tokenable_id_index` (`tokenable_type`,`tokenable_id`),
+ KEY `personal_access_tokens_expires_at_index` (`expires_at`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `plugin_records`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `plugin_records` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `vendor` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `name` varchar(60) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `version` varchar(40) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `enabled` tinyint(1) NOT NULL DEFAULT '1',
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `plugin_records_vendor_name_unique` (`vendor`,`name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `posts`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `posts` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `category_id` bigint unsigned DEFAULT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `title` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `slug` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `excerpt` text COLLATE utf8mb4_unicode_ci,
+ `content` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `content_format` varchar(10) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'markdown',
+ `keywords` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `cover_media_id` bigint unsigned DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'published',
+ `is_sticky` tinyint(1) NOT NULL DEFAULT '0',
+ `sticky_level` tinyint unsigned NOT NULL DEFAULT '0',
+ `close_comment` tinyint(1) NOT NULL DEFAULT '0',
+ `read_password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `views` bigint unsigned NOT NULL DEFAULT '0',
+ `comment_count` int unsigned NOT NULL DEFAULT '0',
+ `meta` json DEFAULT NULL,
+ `published_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `posts_slug_unique` (`slug`),
+ KEY `posts_category_id_foreign` (`category_id`),
+ KEY `posts_user_id_foreign` (`user_id`),
+ KEY `posts_status_published_at_index` (`status`,`published_at`),
+ KEY `posts_status_index` (`status`),
+ KEY `posts_published_at_index` (`published_at`),
+ CONSTRAINT `posts_category_id_foreign` FOREIGN KEY (`category_id`) REFERENCES `categories` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `posts_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE SET NULL
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `redirects`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `redirects` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `hash` varchar(16) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `target_url` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `clicks` bigint unsigned NOT NULL DEFAULT '0',
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
+ `last_clicked_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `redirects_hash_unique` (`hash`),
+ KEY `redirects_target_url_index` (`target_url`(191))
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `role_has_permissions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `role_has_permissions` (
+ `permission_id` bigint unsigned NOT NULL,
+ `role_id` bigint unsigned NOT NULL,
+ PRIMARY KEY (`permission_id`,`role_id`),
+ KEY `role_has_permissions_role_id_foreign` (`role_id`),
+ CONSTRAINT `role_has_permissions_permission_id_foreign` FOREIGN KEY (`permission_id`) REFERENCES `permissions` (`id`) ON DELETE CASCADE,
+ CONSTRAINT `role_has_permissions_role_id_foreign` FOREIGN KEY (`role_id`) REFERENCES `roles` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `roles`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `roles` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `guard_name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `roles_name_guard_name_unique` (`name`,`guard_name`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `sessions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `sessions` (
+ `id` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `user_id` bigint unsigned DEFAULT NULL,
+ `ip_address` varchar(45) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `user_agent` text COLLATE utf8mb4_unicode_ci,
+ `payload` longtext COLLATE utf8mb4_unicode_ci NOT NULL,
+ `last_activity` int NOT NULL,
+ PRIMARY KEY (`id`),
+ KEY `sessions_user_id_index` (`user_id`),
+ KEY `sessions_last_activity_index` (`last_activity`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `settings`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `settings` (
+ `key` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `value` text COLLATE utf8mb4_unicode_ci NOT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`key`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `subscriptions`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `subscriptions` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `user_id` bigint unsigned NOT NULL,
+ `membership_plan_id` bigint unsigned DEFAULT NULL,
+ `status` varchar(20) COLLATE utf8mb4_unicode_ci NOT NULL DEFAULT 'active',
+ `starts_at` timestamp NULL DEFAULT NULL,
+ `ends_at` timestamp NULL DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ KEY `subscriptions_membership_plan_id_foreign` (`membership_plan_id`),
+ KEY `subscriptions_user_id_status_index` (`user_id`,`status`),
+ CONSTRAINT `subscriptions_membership_plan_id_foreign` FOREIGN KEY (`membership_plan_id`) REFERENCES `membership_plans` (`id`) ON DELETE SET NULL,
+ CONSTRAINT `subscriptions_user_id_foreign` FOREIGN KEY (`user_id`) REFERENCES `users` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `taggables`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `taggables` (
+ `tag_id` bigint unsigned NOT NULL,
+ `taggable_type` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `taggable_id` bigint unsigned NOT NULL,
+ UNIQUE KEY `taggables_tag_id_taggable_id_taggable_type_unique` (`tag_id`,`taggable_id`,`taggable_type`),
+ KEY `taggables_taggable_type_taggable_id_index` (`taggable_type`,`taggable_id`),
+ CONSTRAINT `taggables_tag_id_foreign` FOREIGN KEY (`tag_id`) REFERENCES `tags` (`id`) ON DELETE CASCADE
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `tags`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `tags` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` json NOT NULL,
+ `slug` json NOT NULL,
+ `type` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `order_column` int DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+DROP TABLE IF EXISTS `users`;
+/*!40101 SET @saved_cs_client = @@character_set_client */;
+/*!50503 SET character_set_client = utf8mb4 */;
+CREATE TABLE `users` (
+ `id` bigint unsigned NOT NULL AUTO_INCREMENT,
+ `name` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email` varchar(255) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `email_verified_at` timestamp NULL DEFAULT NULL,
+ `password` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `remember_token` varchar(100) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `created_at` timestamp NULL DEFAULT NULL,
+ `updated_at` timestamp NULL DEFAULT NULL,
+ `legacy_md5` varchar(32) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `url` varchar(255) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `logincount` int unsigned NOT NULL DEFAULT '0',
+ `loginip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `regip` varchar(64) COLLATE utf8mb4_unicode_ci DEFAULT NULL,
+ `logintime` timestamp NULL DEFAULT NULL,
+ `lastpost_at` timestamp NULL DEFAULT NULL,
+ PRIMARY KEY (`id`),
+ UNIQUE KEY `users_email_unique` (`email`),
+ KEY `users_legacy_md5_index` (`legacy_md5`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
+/*!40101 SET character_set_client = @saved_cs_client */;
+/*!40103 SET TIME_ZONE=@OLD_TIME_ZONE */;
+
+/*!40101 SET SQL_MODE=@OLD_SQL_MODE */;
+/*!40014 SET FOREIGN_KEY_CHECKS=@OLD_FOREIGN_KEY_CHECKS */;
+/*!40014 SET UNIQUE_CHECKS=@OLD_UNIQUE_CHECKS */;
+/*!40111 SET SQL_NOTES=@OLD_SQL_NOTES */;
+
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (1,'0001_01_01_000000_create_users_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (2,'0001_01_01_000001_create_cache_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (3,'0001_01_01_000002_create_jobs_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (4,'2026_08_11_092446_create_permission_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (5,'2026_08_11_092448_create_media_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (6,'2026_08_11_092449_create_tag_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (7,'2026_08_11_100000_create_membership_tables',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (8,'2026_08_11_100000_create_payments_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (9,'2026_08_11_100001_create_categories_and_posts_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (10,'2026_08_11_100002_create_comments_and_trackbacks_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (11,'2026_08_11_100003_create_links_and_settings_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (12,'2026_08_11_100004_add_blog_columns_to_users_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (13,'2026_08_11_100005_add_content_format_to_posts_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (14,'2026_08_11_113537_create_personal_access_tokens_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (15,'2026_08_12_000000_create_hello_world_logs_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (16,'2026_08_12_100000_add_payable_to_payments_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (17,'2026_08_12_200000_create_market_packages_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (18,'2026_08_12_300000_add_description_to_categories_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (19,'2026_08_12_400000_create_redirects_table',1);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (20,'2026_08_15_000001_create_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (21,'2026_08_15_000002_create_media_folders_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (22,'2026_08_15_000003_add_soft_deletes_to_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (23,'2026_08_15_000004_add_custom_properties_to_media_library_table',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (24,'2026_09_16_000001_add_cover_media_id_to_posts_and_categories',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (25,'2026_09_16_000002_seed_default_media_folders',2);
+INSERT INTO `migrations` (`id`, `migration`, `batch`) VALUES (26,'2026_09_18_210000_add_sticky_level_to_posts_table',3);
diff --git a/docs/development.md b/docs/development.md
index df750de..f75084b 100644
--- a/docs/development.md
+++ b/docs/development.md
@@ -35,17 +35,17 @@ routes/web.php # 前台规范路由;routes/legacy.php 老 URL 301(新增
### 钩子清单
-| Hook | 类型 | 参数 | 用途 |
-|------|------|------|------|
-| `post.rendered` | filter | `(string $html, Post): string` | 文章渲染后处理 |
-| `comment.created` | action | `Comment` | 新评论创建 |
-| `payment.paid` | action | `Payment` | 支付成功分发 |
-| `filament.post_form` | action | `Schema` | 文章表单注入 |
-| `filament.post_table` | action | `Table` | 文章表格注入 |
-| `seo.structured_data` | filter | `(array): array` | JSON-LD 扩展 |
-| `theme.inject.{point}` | filter | `(string): string` | 注入点追加 HTML |
+| Hook | 类型 | 参数 | 用途 |
+| ---------------------- | ------ | ------------------------------ | --------------- |
+| `post.rendered` | filter | `(string $html, Post): string` | 文章渲染后处理 |
+| `comment.created` | action | `Comment` | 新评论创建 |
+| `payment.paid` | action | `Payment` | 支付成功分发 |
+| `filament.post_form` | action | `Schema` | 文章表单注入 |
+| `filament.post_table` | action | `Table` | 文章表格注入 |
+| `seo.structured_data` | filter | `(array): array` | JSON-LD 扩展 |
+| `theme.inject.{point}` | filter | `(string): string` | 注入点追加 HTML |
-> `Schema::components()` / `Table::columns()` 是**替换**语义:表单用 `[...$schema->getComponents(), Section::make(...)]` 合并,表格用 `pushColumns()`。
+> 文章表单是左 2 右 1:插件用 `PostForm::appendToSidebar()` / `PostForm::appendToAccessSection()` 注入,不要往根节点追加整行 Section。表格仍用 `pushColumns()`。
## 4. 主题规范(详见 docs/themes.md)
@@ -74,7 +74,17 @@ routes/web.php # 前台规范路由;routes/legacy.php 老 URL 301(新增
- 不提交:`.env`、密钥/证书、`storage/media-library/temp/`、构建产物
- 完成后更新对应 `docs/` 文档
-## 8. 常用命令速查
+## 8. 后台导航约定
+
+- 顶栏 **网站** 打开前台新窗口,**写文章** 进新建文章
+- **管理 → 页面缓存**:查看/清理前台整页缓存,不要只藏在用户菜单里
+- **管理 → 语言**:只展示当前 `APP_LOCALE` 和已有语言包。文章还没有多语言版本,不要假装已经做了
+- 文章置顶用 `sticky_level`(0/1/2/3),列表排序 `orderedForList()`。3 = 置顶头条,永远最前
+- 前台 RSS / Sitemap / llms.txt 开新窗口。`/llms.txt` 和 `/posts/{slug}.md` 是核心路由,不要做成静态文件,新文章要马上出现
+- 首页浏览模式:`home_view_mode` + `?view=list|full` + cookie `ll_home_view`。分页用 `blog_paginate()`(`onEachSide(1)`)
+- 老 sablog 正文清洗放 `SablogHtmlNormalizer`,导入 / 渲染 / `sablog:fix-content` 共用,不要各写一套 strip_tags
+
+## 9. 常用命令速查
```bash
php artisan test # 全量测试
diff --git a/docs/diary.md b/docs/diary.md
new file mode 100644
index 0000000..d3215dc
--- /dev/null
+++ b/docs/diary.md
@@ -0,0 +1,7 @@
+# Diary
+
+## 2026-09-18
+
+- Edit post crashed with `Class "Filament\Tables\Actions\Action" not found`.
+- Cause: `CommentsRelationManager` still imported the Filament v3 class. HTTP GET on the edit page can pass because the relation table only instantiates when its Livewire component boots.
+- Fix: `use Filament\Actions\Action`. Regression: `FilamentV5CompatTest`.
diff --git a/docs/import.md b/docs/import.md
index e22f6a5..228a2be 100644
--- a/docs/import.md
+++ b/docs/import.md
@@ -65,7 +65,7 @@ php artisan sablog:import \
- 伪静态:`/show-12-1.html` → 新文章页;`/category-3-1.html` → 新分类页;`/archives-202008-1.html` → 新归档
- 查询串:`/?action=show&id=12`、`/?action=search&keyword=x`、`/?action=tags&id=1` …全部映射
-- 稳定路径:`/rss.xml`、`/sitemap.xml`、`/robots.txt` 保持原样
+- 稳定路径:`/rss.xml`、`/sitemap.xml`、`/robots.txt`、`/llms.txt` 保持原样
- 老入口:`attachment.php?id=` → 301 到 S3/本地媒体地址;`tburl.php`/`trackback.php` → 410
## 附件上 S3
@@ -86,8 +86,15 @@ php artisan attachments:sync-s3 --legacy-dir=... --force
php artisan content:convert --all # 全部 HTML 文章转 Markdown
php artisan content:convert 12 # 单篇
php artisan content:convert --all --dry-run # 先预览
+
+# 清洗已导入正文:字面 、SyntaxHighlighter、docker 分行 div
+php artisan sablog:fix-content --all
+php artisan sablog:fix-content 3156
+php artisan sablog:fix-content --all --dry-run
```
+渲染时也会做同样的清洗,所以不跑修复命令,前台也能先看起来正常。`content:convert` 和 `--convert-markdown` 会先清洗再转 Markdown。
+
## 幂等与重跑
- 导入按**原始 ID** `updateOrInsert`:重复执行不会产生重复数据,只会覆盖更新
diff --git a/docs/plugins.md b/docs/plugins.md
index bb80132..80d5f40 100644
--- a/docs/plugins.md
+++ b/docs/plugins.md
@@ -103,21 +103,20 @@ class ServiceProvider extends PluginServiceProvider
核心资源的表单/表格会触发 `filament.post_form` / `filament.post_table` 两个钩子,插件可追加自己的字段与列,停用插件后字段自动消失,核心零侵入。
-> 注意:`Schema::components()` 与 `Table::columns()` 都是**整体替换**语义,追加要这样写:
+> 文章编辑页是 **左正文 2 : 右功能 1**。插件不要往根节点再追加整行 Section,否则会把 2:1 挤掉。
>
-> - 表单:`$schema->components([...$schema->getComponents(), Section::make(...)])`
+> - 表单:用 `PostForm::appendToSidebar()` / `PostForm::appendToAccessSection()`
> - 表格:`$table->pushColumns([...])`
-示例(给文章加「付费设置」):
+示例(给右侧「访问设置」加会员字段):
```php
+use App\Filament\Resources\Posts\Schemas\PostForm;
+
$manager->addAction('filament.post_form', function (Schema $schema) {
- $schema->components([
- ...$schema->getComponents(),
- Section::make('付费设置')->columns(3)->collapsible()->schema([
- TextInput::make('meta.price')->label('单篇价格(分)')->numeric()->default(0),
- Toggle::make('meta.members_only')->label('会员专享'),
- ]),
+ PostForm::appendToAccessSection($schema, [
+ TextInput::make('meta.price')->label('价格')->numeric()->default(0),
+ Toggle::make('meta.members_only')->label('会员专享'),
]);
});
diff --git a/docs/tutorial-plugin.md b/docs/tutorial-plugin.md
index 7288949..282b6b7 100644
--- a/docs/tutorial-plugin.md
+++ b/docs/tutorial-plugin.md
@@ -76,14 +76,14 @@ class ServiceProvider extends PluginServiceProvider
### 常用钩子
-| Hook | 类型 | 参数 | 用途 |
-| --------------------- | ------ | ------------------------------- | --------------------------------------- |
-| `post.rendered` | filter | `(string $html, Post) : string` | 文章渲染后处理(短代码/付费过滤) |
-| `comment.created` | action | `Comment` | 新评论创建 |
-| `payment.paid` | action | `Payment` | 支付成功(按 `$payment->payable` 分发) |
-| `filament.post_form` | action | `Schema` | 文章编辑表单追加字段 |
-| `filament.post_table` | action | `Table` | 文章列表追加列 |
-| `seo.structured_data` | filter | `(array $data) : array` | 扩展 JSON-LD |
+| Hook | 类型 | 参数 | 用途 |
+| --------------------- | ------ | ------------------------------- | ------------------------------------------------ |
+| `post.rendered` | filter | `(string $html, Post) : string` | 文章渲染后处理(短代码/付费过滤) |
+| `comment.created` | action | `Comment` | 新评论创建 |
+| `payment.paid` | action | `Payment` | 支付成功(按 `$payment->payable` 分发) |
+| `filament.post_form` | action | `Schema` | 文章表单注入(用 `PostForm::appendToSidebar()`) |
+| `filament.post_table` | action | `Table` | 文章列表追加列 |
+| `seo.structured_data` | filter | `(array $data) : array` | 扩展 JSON-LD |
完整列表见 `docs/plugins.md`。
diff --git a/plugins/demo.hello-world/src/ServiceProvider.php b/plugins/demo.hello-world/src/ServiceProvider.php
index 71b34c9..66bbd64 100644
--- a/plugins/demo.hello-world/src/ServiceProvider.php
+++ b/plugins/demo.hello-world/src/ServiceProvider.php
@@ -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([
diff --git a/plugins/neatstudio.blog-seo/plugin.json b/plugins/neatstudio.blog-seo/plugin.json
index 83726d3..6710125 100644
--- a/plugins/neatstudio.blog-seo/plugin.json
+++ b/plugins/neatstudio.blog-seo/plugin.json
@@ -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站点名/描述等在「博客设置 → 站点」配置,社交账号在「博客设置 → 社交账号」配置"
}
diff --git a/plugins/neatstudio.blog-seo/routes/web.php b/plugins/neatstudio.blog-seo/routes/web.php
deleted file mode 100644
index 93e760c..0000000
--- a/plugins/neatstudio.blog-seo/routes/web.php
+++ /dev/null
@@ -1,11 +0,0 @@
-name('geo.llms');
-Route::get('/posts/{slug}.md', [SeoController::class, 'postMarkdown'])->where('slug', '[A-Za-z0-9\-]+')->name('geo.post.markdown');
diff --git a/plugins/neatstudio.blog-seo/src/Http/SeoController.php b/plugins/neatstudio.blog-seo/src/Http/SeoController.php
deleted file mode 100644
index 27c90af..0000000
--- a/plugins/neatstudio.blog-seo/src/Http/SeoController.php
+++ /dev/null
@@ -1,71 +0,0 @@
-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);
- }
-}
diff --git a/plugins/neatstudio.blog-seo/src/ServiceProvider.php b/plugins/neatstudio.blog-seo/src/ServiceProvider.php
index f746c7d..6b59d2b 100644
--- a/plugins/neatstudio.blog-seo/src/ServiceProvider.php
+++ b/plugins/neatstudio.blog-seo/src/ServiceProvider.php
@@ -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) {
diff --git a/plugins/neatstudio.membership/src/ServiceProvider.php b/plugins/neatstudio.membership/src/ServiceProvider.php
index 59a5932..394bf1f 100644
--- a/plugins/neatstudio.membership/src/ServiceProvider.php
+++ b/plugins/neatstudio.membership/src/ServiceProvider.php
@@ -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 表示单篇免费'),
]);
});
diff --git a/public/css/blog.css b/public/css/blog.css
index f7b3795..435442a 100644
--- a/public/css/blog.css
+++ b/public/css/blog.css
@@ -1,40 +1,112 @@
/* ============ LaraLog 默认主题 ============ */
-* { box-sizing: border-box; margin: 0; padding: 0; }
+* {
+ box-sizing: border-box;
+ margin: 0;
+ padding: 0;
+}
body {
- font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
+ "Hiragino Sans GB", "Microsoft YaHei", sans-serif;
font-size: 16px;
line-height: 1.7;
color: #333;
background: #f5f6f8;
}
-a { color: #2d6cdf; text-decoration: none; }
-a:hover { color: #1a4fa8; }
+a {
+ color: #2d6cdf;
+ text-decoration: none;
+}
+a:hover {
+ color: #1a4fa8;
+}
-.container { max-width: 1100px; margin: 0 auto; padding: 0 16px; }
+.container {
+ max-width: 1100px;
+ margin: 0 auto;
+ padding: 0 16px;
+}
/* Header */
-.site-header { background: #fff; border-bottom: 1px solid #e5e7eb; padding: 18px 0; }
-.site-header .container { display: flex; align-items: center; justify-content: space-between; flex-wrap: wrap; gap: 12px; }
-.site-title { font-size: 26px; font-weight: 800; color: #111; }
-.site-desc { font-size: 13px; color: #888; margin-top: 2px; }
-.site-nav { display: flex; align-items: center; gap: 16px; flex-wrap: wrap; font-size: 15px; }
-.site-nav a.active { color: #2d6cdf; font-weight: 600; }
-.site-search input { padding: 6px 10px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; width: 150px; }
-.inline-form { display: inline; }
-.link-btn { background: none; border: none; color: #2d6cdf; cursor: pointer; font-size: 15px; padding: 0; }
+.site-header {
+ background: #fff;
+ border-bottom: 1px solid #e5e7eb;
+ padding: 18px 0;
+}
+.site-header .container {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ flex-wrap: wrap;
+ gap: 12px;
+}
+.site-title {
+ font-size: 26px;
+ font-weight: 800;
+ color: #111;
+}
+.site-desc {
+ font-size: 13px;
+ color: #888;
+ margin-top: 2px;
+}
+.site-nav {
+ display: flex;
+ align-items: center;
+ gap: 16px;
+ flex-wrap: wrap;
+ font-size: 15px;
+}
+.site-nav a.active {
+ color: #2d6cdf;
+ font-weight: 600;
+}
+.site-search input {
+ padding: 6px 10px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 14px;
+ width: 150px;
+}
+.inline-form {
+ display: inline;
+}
+.link-btn {
+ background: none;
+ border: none;
+ color: #2d6cdf;
+ cursor: pointer;
+ font-size: 15px;
+ padding: 0;
+}
/* Layout */
-.main-layout { display: grid; grid-template-columns: 1fr 320px; gap: 24px; padding: 24px 16px; align-items: start; }
+.main-layout {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 320px;
+ gap: 24px;
+ padding: 24px 16px;
+ align-items: start;
+}
@media (max-width: 860px) {
- .main-layout { grid-template-columns: 1fr; }
+ .main-layout {
+ grid-template-columns: 1fr;
+ }
}
/* Content */
-.content { min-width: 0; }
+.content {
+ min-width: 0;
+}
-.post-card, .post-full, .widget, .auth-box, .profile-box, .links-list {
+.post-card,
+.post-full,
+.widget,
+.auth-box,
+.profile-box,
+.links-list {
background: #fff;
border: 1px solid #e5e7eb;
border-radius: 10px;
@@ -42,114 +114,446 @@ a:hover { color: #1a4fa8; }
margin-bottom: 20px;
}
-.post-card.sticky { border-left: 4px solid #f59e0b; }
+.post-card.sticky {
+ border-left: 4px solid #f59e0b;
+}
-.post-title { font-size: 20px; line-height: 1.4; margin-bottom: 8px; }
-.post-card .post-title a { color: #111; }
-.post-card .post-title a:hover { color: #2d6cdf; }
-.badge { background: #f59e0b; color: #fff; font-size: 12px; padding: 2px 6px; border-radius: 4px; margin-right: 6px; vertical-align: middle; }
+.post-title {
+ font-size: 20px;
+ line-height: 1.4;
+ margin-bottom: 8px;
+}
+.post-card .post-title a {
+ color: #111;
+}
+.post-card .post-title a:hover {
+ color: #2d6cdf;
+}
+.badge {
+ background: #f59e0b;
+ color: #fff;
+ font-size: 12px;
+ padding: 2px 6px;
+ border-radius: 4px;
+ margin-right: 6px;
+ vertical-align: middle;
+}
-.post-meta { font-size: 13px; color: #888; margin-bottom: 10px; }
-.post-excerpt { color: #555; margin-bottom: 10px; }
-.post-footer { display: flex; align-items: center; gap: 10px; flex-wrap: wrap; font-size: 14px; }
-.read-more { font-weight: 600; }
+.post-meta {
+ font-size: 13px;
+ color: #888;
+ margin-bottom: 10px;
+}
+.post-excerpt {
+ color: #555;
+ margin-bottom: 10px;
+}
+.post-footer {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ flex-wrap: wrap;
+ font-size: 14px;
+}
+.read-more {
+ font-weight: 600;
+}
-.post-full .post-title { font-size: 28px; }
-.post-cover { margin: -20px -24px 16px; border-radius: 10px 10px 0 0; overflow: hidden; }
-.post-cover img { width: 100%; height: auto; display: block; max-height: 420px; object-fit: cover; }
-.post-card .post-cover { margin: -20px -24px 12px; border-radius: 10px 10px 0 0; }
-.post-card .post-cover img { max-height: 260px; }
-.post-content { margin-top: 16px; font-size: 16.5px; }
-.post-content img { max-width: 100%; height: auto; border-radius: 6px; }
-.post-content pre { background: #1e1e2e; color: #e6e6e6; padding: 14px; border-radius: 8px; overflow-x: auto; margin: 12px 0; }
-.post-content code { background: #f0f1f3; padding: 2px 5px; border-radius: 4px; font-size: 14px; }
-.post-content pre code { background: none; padding: 0; }
-.post-content blockquote { border-left: 4px solid #2d6cdf; padding-left: 14px; color: #666; margin: 12px 0; }
-.post-content h1, .post-content h2, .post-content h3 { margin: 18px 0 8px; line-height: 1.3; }
-.post-content ul, .post-content ol { padding-left: 24px; margin: 8px 0; }
-.post-content table { border-collapse: collapse; width: 100%; margin: 12px 0; }
-.post-content th, .post-content td { border: 1px solid #ddd; padding: 6px 10px; }
+.post-full .post-title {
+ font-size: 28px;
+}
+.post-cover {
+ margin: -20px -24px 16px;
+ border-radius: 10px 10px 0 0;
+ overflow: hidden;
+}
+.post-cover img {
+ width: 100%;
+ height: auto;
+ display: block;
+ max-height: 420px;
+ object-fit: cover;
+}
+.post-card .post-cover {
+ margin: -20px -24px 12px;
+ border-radius: 10px 10px 0 0;
+}
+.post-card .post-cover img {
+ max-height: 260px;
+}
+.post-content {
+ margin-top: 16px;
+ font-size: 16.5px;
+ max-width: 100%;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+.post-content img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 6px;
+}
+.post-content pre,
+.post-content .codeText,
+.post-content ol[class^="dp-"] {
+ background: #1e1e2e;
+ color: #e6e6e6;
+ padding: 14px;
+ border-radius: 8px;
+ overflow-x: auto;
+ max-width: 100%;
+ margin: 12px 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.post-content code {
+ background: #f0f1f3;
+ padding: 2px 5px;
+ border-radius: 4px;
+ font-size: 14px;
+ word-break: break-all;
+ overflow-wrap: anywhere;
+ white-space: break-spaces;
+}
+.post-content pre code {
+ display: block;
+ background: none;
+ padding: 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.post-content blockquote {
+ border-left: 4px solid #2d6cdf;
+ padding-left: 14px;
+ color: #666;
+ margin: 12px 0;
+}
+.post-content h1,
+.post-content h2,
+.post-content h3 {
+ margin: 18px 0 8px;
+ line-height: 1.3;
+}
+.post-content ul,
+.post-content ol {
+ padding-left: 24px;
+ margin: 8px 0;
+}
+.post-content table {
+ border-collapse: collapse;
+ width: 100%;
+ margin: 12px 0;
+}
+.post-content th,
+.post-content td {
+ border: 1px solid #ddd;
+ padding: 6px 10px;
+}
-.post-tags { margin-top: 16px; }
-.tag-link { display: inline-block; background: #eef2ff; color: #3b5bdb; border-radius: 4px; padding: 2px 8px; font-size: 13px; margin: 2px 4px 2px 0; }
-.tag-link:hover { background: #dbe4ff; }
+.post-tags {
+ margin-top: 16px;
+}
+.tag-link {
+ display: inline-block;
+ background: #eef2ff;
+ color: #3b5bdb;
+ border-radius: 4px;
+ padding: 2px 8px;
+ font-size: 13px;
+ margin: 2px 4px 2px 0;
+}
+.tag-link:hover {
+ background: #dbe4ff;
+}
/* Comments */
-.comments-section { background: #fff; border: 1px solid #e5e7eb; border-radius: 10px; padding: 20px 24px; margin-bottom: 20px; }
-.section-title { font-size: 18px; margin-bottom: 14px; }
-.comment { padding: 12px 0; border-bottom: 1px solid #f0f1f3; }
-.comment:last-of-type { border-bottom: none; }
-.comment-head { font-size: 14px; margin-bottom: 4px; }
-.comment-time { color: #999; }
-.comment-body { font-size: 15px; color: #444; }
+.comments-section {
+ background: #fff;
+ border: 1px solid #e5e7eb;
+ border-radius: 10px;
+ padding: 20px 24px;
+ margin-bottom: 20px;
+}
+.section-title {
+ font-size: 18px;
+ margin-bottom: 14px;
+}
+.comment {
+ padding: 12px 0;
+ border-bottom: 1px solid #f0f1f3;
+}
+.comment:last-of-type {
+ border-bottom: none;
+}
+.comment-head {
+ font-size: 14px;
+ margin-bottom: 4px;
+}
+.comment-time {
+ color: #999;
+}
+.comment-body {
+ font-size: 15px;
+ color: #444;
+}
-.comment-form { margin-top: 16px; }
-.form-row { margin-bottom: 12px; }
-.form-row label { display: block; font-size: 14px; margin-bottom: 4px; }
-.form-row input, .form-row textarea {
- width: 100%; padding: 8px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 15px; font-family: inherit;
+.comment-form {
+ margin-top: 16px;
+}
+.form-row {
+ margin-bottom: 12px;
+}
+.form-row label {
+ display: block;
+ font-size: 14px;
+ margin-bottom: 4px;
+}
+.form-row input,
+.form-row textarea {
+ width: 100%;
+ padding: 8px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 15px;
+ font-family: inherit;
+}
+.honeypot {
+ position: absolute;
+ left: -9999px;
+ opacity: 0;
+}
+.error {
+ color: #e03131;
+ font-size: 13px;
+ margin-top: 4px;
}
-.honeypot { position: absolute; left: -9999px; opacity: 0; }
-.error { color: #e03131; font-size: 13px; margin-top: 4px; }
.btn {
- background: #2d6cdf; color: #fff; border: none; padding: 9px 20px; border-radius: 6px;
- font-size: 15px; cursor: pointer;
+ background: #2d6cdf;
+ color: #fff;
+ border: none;
+ padding: 9px 20px;
+ border-radius: 6px;
+ font-size: 15px;
+ cursor: pointer;
+}
+.btn:hover {
+ background: #1a4fa8;
+}
+.btn-primary {
+ background: #2d6cdf;
}
-.btn:hover { background: #1a4fa8; }
-.btn-primary { background: #2d6cdf; }
-.alert { padding: 10px 16px; border-radius: 8px; margin-bottom: 14px; font-size: 14px; }
-.alert-success { background: #d3f9d8; color: #2b8a3e; }
-.alert-error { background: #ffe3e3; color: #c92a2a; }
+.alert {
+ padding: 10px 16px;
+ border-radius: 8px;
+ margin-bottom: 14px;
+ font-size: 14px;
+}
+.alert-success {
+ background: #d3f9d8;
+ color: #2b8a3e;
+}
+.alert-error {
+ background: #ffe3e3;
+ color: #c92a2a;
+}
+.alert-warning {
+ background: #fff3bf;
+ color: #8a6d00;
+}
/* Sidebar */
-.sidebar { position: sticky; top: 20px; }
-.widget { padding: 16px 20px; }
-.widget-title { font-size: 15px; margin-bottom: 10px; padding-bottom: 8px; border-bottom: 2px solid #2d6cdf; }
-.widget-list { list-style: none; }
-.widget-list li { font-size: 14px; padding: 4px 0; }
-.count { color: #aaa; font-size: 13px; }
-.tag-cloud { line-height: 2; }
-.tag-cloud.big { line-height: 2.4; }
+.sidebar {
+ position: sticky;
+ top: 20px;
+}
+.widget {
+ padding: 16px 20px;
+}
+.widget-title {
+ font-size: 15px;
+ margin-bottom: 10px;
+ padding-bottom: 8px;
+ border-bottom: 2px solid #2d6cdf;
+}
+.widget-list {
+ list-style: none;
+}
+.widget-list li {
+ font-size: 14px;
+ padding: 4px 0;
+}
+.count {
+ color: #aaa;
+ font-size: 13px;
+}
+.tag-cloud {
+ line-height: 2;
+}
+.tag-cloud.big {
+ line-height: 2.4;
+}
/* Misc */
-.empty { color: #999; text-align: center; padding: 30px 0; }
-.list-title { font-size: 22px; margin-bottom: 16px; }
-.archive-year { font-size: 17px; margin: 14px 0 6px; }
-.archive-list { list-style: none; }
-.archive-posts { margin: 4px 0 10px 22px; }
-.archive-posts time { color: #aaa; font-size: 13px; }
-
-.pagination { margin: 16px 0; }
-.pagination-links { display: flex; gap: 8px; flex-wrap: wrap; align-items: center; }
-.pagination-links a, .pagination-links .current, .pagination-links .disabled {
- padding: 6px 12px; border: 1px solid #ddd; border-radius: 6px; font-size: 14px; background: #fff;
+.empty {
+ color: #999;
+ text-align: center;
+ padding: 30px 0;
+}
+.list-title {
+ font-size: 22px;
+ margin-bottom: 16px;
+}
+.archive-year {
+ font-size: 17px;
+ margin: 14px 0 6px;
+}
+.archive-list {
+ list-style: none;
+}
+.archive-posts {
+ margin: 4px 0 10px 22px;
+}
+.archive-posts time {
+ color: #aaa;
+ font-size: 13px;
}
-.pagination-links .current { background: #2d6cdf; color: #fff; border-color: #2d6cdf; }
-.pagination-links .disabled { color: #bbb; }
-.auth-layout { display: flex; justify-content: center; padding: 40px 16px; }
-.auth-box { width: 100%; max-width: 420px; }
-.auth-alt { margin-top: 14px; font-size: 14px; color: #666; text-align: center; }
+.view-mode-switcher {
+ font-size: 13px;
+ color: #888;
+ margin-bottom: 14px;
+}
+.view-mode-switcher a.active {
+ font-weight: 700;
+ color: #2d6cdf;
+}
-.links-list { list-style: none; }
-.links-list li { padding: 6px 0; }
-.link-note { color: #999; font-size: 14px; }
+.pagination {
+ width: 100%;
+ max-width: 100%;
+ min-width: 0;
+ margin: 16px 0;
+}
+.pagination-links {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+ align-items: center;
+ max-width: 100%;
+}
+.pagination-info {
+ color: #888;
+ font-size: 13px;
+ margin-right: 6px;
+ white-space: nowrap;
+}
+.pagination-links a,
+.pagination-links .current,
+.pagination-links .disabled {
+ padding: 6px 12px;
+ border: 1px solid #ddd;
+ border-radius: 6px;
+ font-size: 14px;
+ background: #fff;
+}
+.pagination-links .current {
+ background: #2d6cdf;
+ color: #fff;
+ border-color: #2d6cdf;
+}
+.pagination-links .disabled {
+ color: #bbb;
+}
-.site-footer { background: #fff; border-top: 1px solid #e5e7eb; padding: 20px 0; margin-top: 24px; text-align: center; font-size: 13px; color: #888; }
-.powered { margin-top: 4px; }
+.auth-layout {
+ display: flex;
+ justify-content: center;
+ padding: 40px 16px;
+}
+.auth-box {
+ width: 100%;
+ max-width: 420px;
+}
+.auth-alt {
+ margin-top: 14px;
+ font-size: 14px;
+ color: #666;
+ text-align: center;
+}
+
+.links-list {
+ list-style: none;
+}
+.links-list li {
+ padding: 6px 0;
+}
+.link-note {
+ color: #999;
+ font-size: 14px;
+}
+
+.site-footer {
+ background: #fff;
+ border-top: 1px solid #e5e7eb;
+ padding: 20px 0;
+ margin-top: 24px;
+ text-align: center;
+ font-size: 13px;
+ color: #888;
+}
+.powered {
+ margin-top: 4px;
+}
/* TOC */
-.toc { background: #f8f9fa; border: 1px solid #e5e7eb; border-radius: 8px; padding: 12px 16px; margin: 0 0 18px; font-size: 14px; }
-.toc summary { cursor: pointer; font-weight: 600; margin-bottom: 6px; }
-.toc-list { list-style: none; }
-.toc-list li { padding: 2px 0; }
-.toc-list a { color: #555; }
-.toc-list a:hover { color: #2d6cdf; }
-.toc-level-2 { padding-left: 0; }
-.toc-level-3 { padding-left: 14px; }
-.toc-level-4, .toc-level-5, .toc-level-6 { padding-left: 28px; }
+.toc {
+ background: #f8f9fa;
+ border: 1px solid #e5e7eb;
+ border-radius: 8px;
+ padding: 12px 16px;
+ margin: 0 0 18px;
+ font-size: 14px;
+}
+.toc summary {
+ cursor: pointer;
+ font-weight: 600;
+ margin-bottom: 6px;
+}
+.toc-list {
+ list-style: none;
+}
+.toc-list li {
+ padding: 2px 0;
+}
+.toc-list a {
+ color: #555;
+}
+.toc-list a:hover {
+ color: #2d6cdf;
+}
+.toc-level-2 {
+ padding-left: 0;
+}
+.toc-level-3 {
+ padding-left: 14px;
+}
+.toc-level-4,
+.toc-level-5,
+.toc-level-6 {
+ padding-left: 28px;
+}
/* 屏幕阅读器专用(SEO h1 隐藏) */
-.sr-only { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip: rect(0 0 0 0); white-space: nowrap; border: 0; }
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ white-space: nowrap;
+ border: 0;
+}
diff --git a/public/themes/modern/style.css b/public/themes/modern/style.css
index 9e364c7..ce484ce 100644
--- a/public/themes/modern/style.css
+++ b/public/themes/modern/style.css
@@ -17,88 +17,776 @@
--border: #2c2c2c;
}
}
-* { margin: 0; padding: 0; box-sizing: border-box; }
-body { background: var(--bg); color: var(--text); font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif; font-size: 16px; line-height: 1.8; }
-a { color: var(--accent); text-decoration: none; }
-a:hover { opacity: 0.8; }
-.m-container { max-width: 720px; margin: 0 auto; padding: 0 20px; }
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
+body {
+ background: var(--bg);
+ color: var(--text);
+ font-family:
+ -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC",
+ "Microsoft YaHei", sans-serif;
+ font-size: 16px;
+ line-height: 1.8;
+}
+a {
+ color: var(--accent);
+ text-decoration: none;
+}
+a:hover {
+ opacity: 0.8;
+}
+/* 页面统一宽度:页头/页脚与主体对齐(默认视图的 .container 也落在这里) */
+.container,
+.m-container {
+ max-width: 1080px;
+ margin: 0 auto;
+ padding: 0 20px;
+}
-.m-header { border-bottom: 1px solid var(--border); background: var(--card); }
-.m-header .m-container { display: flex; justify-content: space-between; align-items: center; padding: 18px 20px; }
-.m-logo { font-size: 22px; font-weight: 800; letter-spacing: 1px; color: var(--text); }
-.m-nav { display: flex; gap: 18px; font-size: 14px; align-items: center; }
-.m-nav a.active { font-weight: 700; }
-.m-link-btn { background: none; border: none; color: var(--accent); font-size: 14px; cursor: pointer; padding: 0; }
-.inline-form { display: inline; }
+.m-header {
+ border-bottom: 1px solid var(--border);
+ background: var(--card);
+}
+.m-header .m-container {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 18px 20px;
+}
+.m-logo {
+ font-size: 22px;
+ font-weight: 800;
+ letter-spacing: 1px;
+ color: var(--text);
+}
+.m-nav {
+ display: flex;
+ gap: 18px;
+ font-size: 14px;
+ align-items: center;
+}
+.m-nav a.active {
+ font-weight: 700;
+}
+.m-link-btn {
+ background: none;
+ border: none;
+ color: var(--accent);
+ font-size: 14px;
+ cursor: pointer;
+ padding: 0;
+}
+.m-search input {
+ padding: 7px 12px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg);
+ color: var(--text);
+ font-size: 14px;
+ width: 140px;
+}
+.inline-form {
+ display: inline;
+}
-.main-layout { padding: 36px 20px; }
-.main-layout .content { max-width: 720px; margin: 0 auto; }
+.main-layout {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 300px;
+ gap: 32px;
+ align-items: start;
+ max-width: 1080px;
+ margin: 0 auto;
+ padding: 36px 20px;
+}
+.main-layout .content {
+ min-width: 0;
+ max-width: 100%;
+}
+.main-layout .sidebar {
+ width: auto;
+ min-width: 0;
+}
-.post-card, .post-full, .comments-section { background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 26px 30px; margin-bottom: 22px; }
-.post-card.sticky { box-shadow: inset 3px 0 0 var(--accent); }
-.post-title { font-size: 20px; margin-bottom: 8px; line-height: 1.45; }
-.post-card .post-title a { color: var(--text); }
-.badge { background: var(--accent); color: #fff; font-size: 11px; padding: 2px 7px; border-radius: 3px; margin-right: 6px; vertical-align: 2px; }
-.post-meta { color: var(--muted); font-size: 13px; margin-bottom: 12px; }
-.post-excerpt { color: var(--muted); }
-.post-footer { margin-top: 12px; display: flex; gap: 12px; flex-wrap: wrap; font-size: 14px; }
-.read-more { font-weight: 600; }
+.post-card,
+.post-full,
+.comments-section {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 26px 30px;
+ margin-bottom: 22px;
+}
+.post-card.sticky {
+ box-shadow: inset 3px 0 0 var(--accent);
+}
+.post-title {
+ font-size: 20px;
+ margin-bottom: 8px;
+ line-height: 1.45;
+}
+.post-card .post-title a {
+ color: var(--text);
+}
+.badge {
+ background: var(--accent);
+ color: #fff;
+ font-size: 11px;
+ padding: 2px 7px;
+ border-radius: 3px;
+ margin-right: 6px;
+ vertical-align: 2px;
+}
+.post-meta {
+ color: var(--muted);
+ font-size: 13px;
+ margin-bottom: 12px;
+}
+.post-excerpt {
+ color: var(--muted);
+}
+.post-footer {
+ margin-top: 12px;
+ display: flex;
+ gap: 12px;
+ flex-wrap: wrap;
+ font-size: 14px;
+}
+.read-more {
+ font-weight: 600;
+}
-.post-full .post-title { font-size: 28px; }
-.post-content { margin-top: 14px; }
-.post-content img { max-width: 100%; border-radius: 8px; }
-.post-content pre { background: #0d1117; color: #c9d1d9; padding: 16px; border-radius: 8px; overflow-x: auto; margin: 14px 0; }
-.post-content code { background: rgba(128,128,128,.15); padding: 2px 6px; border-radius: 4px; font-size: 14px; }
-.post-content pre code { background: none; padding: 0; }
-.post-content blockquote { border-left: 3px solid var(--accent); padding-left: 16px; color: var(--muted); margin: 14px 0; }
-.post-content h1, .post-content h2, .post-content h3 { margin: 20px 0 8px; }
+.post-full .post-title {
+ font-size: 28px;
+}
+.post-content {
+ margin-top: 14px;
+ max-width: 100%;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+.post-content img {
+ max-width: 100%;
+ height: auto;
+ border-radius: 8px;
+}
+.post-content pre,
+.post-content .codeText,
+.post-content ol[class^="dp-"] {
+ background: #0d1117;
+ color: #c9d1d9;
+ padding: 16px;
+ border-radius: 8px;
+ overflow-x: auto;
+ max-width: 100%;
+ margin: 14px 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.post-content code {
+ background: rgba(128, 128, 128, 0.15);
+ padding: 2px 6px;
+ border-radius: 4px;
+ font-size: 14px;
+ word-break: break-all;
+ overflow-wrap: anywhere;
+ white-space: break-spaces;
+}
+.post-content pre code {
+ display: block;
+ background: none;
+ padding: 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.post-content blockquote {
+ border-left: 3px solid var(--accent);
+ padding-left: 16px;
+ color: var(--muted);
+ margin: 14px 0;
+}
+.post-content h1,
+.post-content h2,
+.post-content h3 {
+ margin: 20px 0 8px;
+}
-.post-cover { margin: -26px -30px 18px; border-radius: 12px 12px 0 0; overflow: hidden; }
-.post-cover img { width: 100%; display: block; max-height: 380px; object-fit: cover; }
+.post-cover {
+ margin: -26px -30px 18px;
+ border-radius: 12px 12px 0 0;
+ overflow: hidden;
+}
+.post-cover img {
+ width: 100%;
+ display: block;
+ max-height: 380px;
+ object-fit: cover;
+}
-.tag-link { color: var(--accent); font-size: 14px; margin-right: 8px; }
+.tag-link {
+ color: var(--accent);
+ font-size: 14px;
+ margin-right: 8px;
+}
-.comments-section { margin-top: 8px; }
-.section-title { font-size: 17px; margin-bottom: 14px; }
-.comment { padding: 12px 0; border-bottom: 1px solid var(--border); }
-.comment:last-of-type { border-bottom: none; }
-.comment-head { font-size: 14px; margin-bottom: 4px; }
-.comment-time { color: var(--muted); font-size: 13px; }
-.comment-body { color: var(--text); font-size: 15px; }
-.comment-form { margin-top: 14px; }
-.form-row { margin-bottom: 12px; }
-.form-row label { display: block; font-size: 14px; margin-bottom: 4px; color: var(--muted); }
-.form-row input, .form-row textarea { width: 100%; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text); font-size: 15px; font-family: inherit; }
-.honeypot { position: absolute; left: -9999px; opacity: 0; }
-.error { color: var(--accent); font-size: 13px; }
-.btn { background: var(--accent); color: #fff; border: none; padding: 10px 24px; border-radius: 8px; font-size: 15px; cursor: pointer; }
-.alert { padding: 12px 18px; border-radius: 8px; margin-bottom: 16px; font-size: 14px; }
-.alert-success { background: rgba(46,160,67,.12); color: #2ea043; }
-.alert-error { background: rgba(248,81,73,.12); color: #f85149; }
-.empty { color: var(--muted); text-align: center; padding: 40px 0; }
+.comments-section {
+ margin-top: 8px;
+}
+.section-title {
+ font-size: 17px;
+ margin-bottom: 14px;
+}
+.comment {
+ padding: 12px 0;
+ border-bottom: 1px solid var(--border);
+}
+.comment:last-of-type {
+ border-bottom: none;
+}
+.comment-head {
+ font-size: 14px;
+ margin-bottom: 4px;
+}
+.comment-time {
+ color: var(--muted);
+ font-size: 13px;
+}
+.comment-body {
+ color: var(--text);
+ font-size: 15px;
+}
+.comment-form {
+ margin-top: 14px;
+}
+.form-row {
+ margin-bottom: 12px;
+}
+.form-row label {
+ display: block;
+ font-size: 14px;
+ margin-bottom: 4px;
+ color: var(--muted);
+}
+.form-row input,
+.form-row textarea {
+ width: 100%;
+ padding: 10px 14px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg);
+ color: var(--text);
+ font-size: 15px;
+ font-family: inherit;
+}
+.honeypot {
+ position: absolute;
+ left: -9999px;
+ opacity: 0;
+}
+.error {
+ color: var(--accent);
+ font-size: 13px;
+}
+.btn {
+ background: var(--accent);
+ color: #fff;
+ border: none;
+ padding: 10px 24px;
+ border-radius: 8px;
+ font-size: 15px;
+ cursor: pointer;
+}
+.alert {
+ padding: 12px 18px;
+ border-radius: 8px;
+ margin-bottom: 16px;
+ font-size: 14px;
+}
+.alert-success {
+ background: rgba(46, 160, 67, 0.12);
+ color: #2ea043;
+}
+.alert-error {
+ background: rgba(248, 81, 73, 0.12);
+ color: #f85149;
+}
+.alert-warning {
+ background: rgba(210, 153, 34, 0.12);
+ color: #9a6700;
+}
+.empty {
+ color: var(--muted);
+ text-align: center;
+ padding: 40px 0;
+}
-.list-title { font-size: 24px; margin-bottom: 20px; }
+.list-title {
+ font-size: 24px;
+ margin-bottom: 20px;
+}
-.archive-year { font-size: 16px; margin: 16px 0 6px; }
-.archive-list { list-style: none; }
-.archive-posts { margin: 4px 0 12px 22px; }
-.archive-posts time { color: var(--muted); font-size: 13px; }
+.archive-year {
+ font-size: 16px;
+ margin: 16px 0 6px;
+}
+.archive-list {
+ list-style: none;
+}
+.archive-posts {
+ margin: 4px 0 12px 22px;
+}
+.archive-posts time {
+ color: var(--muted);
+ font-size: 13px;
+}
-.pagination { margin: 16px 0; }
-.pagination-links { display: flex; gap: 8px; flex-wrap: wrap; }
-.pagination-links a, .pagination-links .current, .pagination-links .disabled { padding: 6px 14px; border: 1px solid var(--border); border-radius: 6px; font-size: 14px; background: var(--card); color: var(--text); }
-.pagination-links .current { background: var(--accent); color: #fff; border-color: var(--accent); }
-.pagination-links .disabled { color: var(--muted); }
+.view-mode-switcher {
+ font-size: 13px;
+ color: var(--muted);
+ margin-bottom: 16px;
+}
+.view-mode-switcher a.active {
+ font-weight: 700;
+ color: var(--text);
+}
-.m-footer { border-top: 1px solid var(--border); padding: 26px 0 40px; margin-top: 30px; text-align: center; color: var(--muted); font-size: 13px; }
+.pagination {
+ width: 100%;
+ max-width: 100%;
+ min-width: 0;
+ margin: 16px 0;
+}
+.pagination-links {
+ display: flex;
+ gap: 6px;
+ flex-wrap: wrap;
+ align-items: center;
+ max-width: 100%;
+}
+.pagination-info {
+ color: var(--muted);
+ font-size: 13px;
+ margin-right: 6px;
+ white-space: nowrap;
+}
+.pagination-links a,
+.pagination-links .current,
+.pagination-links .disabled {
+ padding: 6px 14px;
+ border: 1px solid var(--border);
+ border-radius: 6px;
+ font-size: 14px;
+ background: var(--card);
+ color: var(--text);
+}
+.pagination-links .current {
+ background: var(--accent);
+ color: #fff;
+ border-color: var(--accent);
+}
+.pagination-links .disabled {
+ color: var(--muted);
+}
-.tag-cloud.big { line-height: 2.4; }
-.links-list { list-style: none; }
-.links-list li { padding: 6px 0; }
-.link-note { color: var(--muted); font-size: 14px; }
+.m-footer {
+ border-top: 1px solid var(--border);
+ padding: 26px 0 40px;
+ margin-top: 30px;
+ text-align: center;
+ color: var(--muted);
+ font-size: 13px;
+}
-.auth-layout { display: flex; justify-content: center; padding: 50px 20px; }
-.auth-box { width: 100%; max-width: 420px; background: var(--card); border: 1px solid var(--border); border-radius: 12px; padding: 30px; }
-.auth-alt { margin-top: 14px; font-size: 14px; color: var(--muted); text-align: center; }
-.search-form { margin-bottom: 16px; display: flex; gap: 8px; }
-.search-form input { flex: 1; padding: 10px 14px; border: 1px solid var(--border); border-radius: 8px; background: var(--bg); color: var(--text); font-size: 15px; }
+.tag-cloud.big {
+ line-height: 2.4;
+}
+.links-list {
+ list-style: none;
+}
+.links-list li {
+ padding: 6px 0;
+}
+.link-note {
+ color: var(--muted);
+ font-size: 14px;
+}
+
+.auth-layout {
+ display: flex;
+ justify-content: center;
+ padding: 50px 20px;
+}
+.auth-box {
+ width: 100%;
+ max-width: 420px;
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 30px;
+}
+.auth-alt {
+ margin-top: 14px;
+ font-size: 14px;
+ color: var(--muted);
+ text-align: center;
+}
+.search-form {
+ margin-bottom: 16px;
+ display: flex;
+ gap: 8px;
+}
+.search-form input {
+ flex: 1;
+ padding: 10px 14px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg);
+ color: var(--text);
+ font-size: 15px;
+}
+
+/* ============ 侧边栏 ============ */
+.sidebar {
+ position: sticky;
+ top: 20px;
+}
+.widget {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 18px 20px;
+ margin-bottom: 18px;
+}
+.widget-title {
+ font-size: 15px;
+ margin-bottom: 10px;
+ padding-bottom: 8px;
+ border-bottom: 2px solid var(--accent);
+}
+.widget-list {
+ list-style: none;
+}
+.widget-list li {
+ font-size: 14px;
+ padding: 5px 0;
+ border-bottom: 1px solid var(--border);
+}
+
+/* 社交账号:横向胶囊列表 */
+.social-links {
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+.social-links li {
+ font-size: 12px;
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 999px;
+ padding: 4px 10px;
+}
+.widget-list li:last-child {
+ border-bottom: none;
+}
+.count {
+ color: var(--muted);
+ font-size: 13px;
+}
+.tag-cloud {
+ line-height: 2;
+}
+.tag-cloud a {
+ display: inline-block;
+}
+.tag-cloud.big {
+ line-height: 2.4;
+}
+
+/* ============ 顶栏搜索(默认 header partial) ============ */
+.site-search input {
+ padding: 7px 12px;
+ border: 1px solid var(--border);
+ border-radius: 8px;
+ background: var(--bg);
+ color: var(--text);
+ font-size: 14px;
+ width: 160px;
+}
+.link-btn {
+ background: none;
+ border: none;
+ color: var(--accent);
+ font-size: 15px;
+ cursor: pointer;
+ padding: 0;
+}
+
+/* ============ 文章正文补充 ============ */
+.post-tags {
+ margin-top: 16px;
+}
+.post-content ul,
+.post-content ol {
+ padding-left: 24px;
+ margin: 8px 0;
+}
+.post-content h4,
+.post-content h5,
+.post-content h6 {
+ margin: 16px 0 6px;
+}
+.post-content table {
+ border-collapse: collapse;
+ width: 100%;
+ margin: 12px 0;
+}
+.post-content th,
+.post-content td {
+ border: 1px solid var(--border);
+ padding: 6px 10px;
+}
+.post-content hr {
+ border: none;
+ border-top: 1px solid var(--border);
+ margin: 18px 0;
+}
+
+/* ============ 个人中心 ============ */
+.profile-box {
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 24px 28px;
+ margin-bottom: 20px;
+}
+.profile-box p {
+ margin-bottom: 8px;
+}
+
+/* ============ 按钮变体 ============ */
+.btn-primary {
+ background: var(--accent);
+}
+.btn:hover {
+ opacity: 0.9;
+}
+
+/* ============ 会员/支付 ============ */
+.plans-grid {
+ display: grid;
+ grid-template-columns: repeat(auto-fill, minmax(240px, 1fr));
+ gap: 16px;
+}
+.plan-card .plan-price {
+ font-size: 26px;
+ font-weight: 800;
+ color: var(--accent);
+}
+.paid-teaser {
+ background: rgba(248, 81, 73, 0.08);
+ border: 1px dashed var(--accent);
+ border-radius: 8px;
+ padding: 12px 16px;
+ color: var(--muted);
+ font-size: 14px;
+ margin: 12px 0;
+}
+
+/* ============ 移动端适配 ============ */
+@media (max-width: 768px) {
+ .m-header .m-container {
+ flex-direction: column;
+ align-items: flex-start;
+ gap: 12px;
+ padding: 16px 20px;
+ }
+ .m-nav {
+ flex-wrap: wrap;
+ gap: 12px;
+ width: 100%;
+ }
+ .m-search {
+ flex: 1;
+ min-width: 100%;
+ }
+ .m-search input {
+ width: 100%;
+ }
+ .main-layout {
+ grid-template-columns: minmax(0, 1fr);
+ padding: 24px 16px;
+ }
+ .main-layout .content {
+ max-width: 100%;
+ }
+ .main-layout .sidebar {
+ width: 100%;
+ }
+ .post-card,
+ .post-full,
+ .comments-section {
+ padding: 20px;
+ }
+ .post-cover {
+ margin: -20px -20px 16px;
+ }
+ .post-full .post-title {
+ font-size: 24px;
+ }
+ .auth-box {
+ padding: 24px 20px;
+ }
+ .pagination-links {
+ gap: 6px;
+ }
+ .pagination-links a,
+ .pagination-links .current,
+ .pagination-links .disabled {
+ padding: 5px 10px;
+ }
+}
+
+/* TOC */
+.toc {
+ background: var(--bg);
+ border: 1px solid var(--border);
+ border-radius: 10px;
+ padding: 14px 18px;
+ margin: 0 0 18px;
+ font-size: 14px;
+}
+.toc summary {
+ cursor: pointer;
+ font-weight: 600;
+ margin-bottom: 6px;
+}
+.toc-list {
+ list-style: none;
+}
+.toc-list li {
+ padding: 3px 0;
+}
+.toc-list a {
+ color: var(--muted);
+}
+.toc-list a:hover {
+ color: var(--accent);
+}
+.toc-level-3 {
+ padding-left: 14px;
+}
+.toc-level-4,
+.toc-level-5,
+.toc-level-6 {
+ padding-left: 28px;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* Paywall */
+.paywall {
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 28px 22px;
+ background: var(--bg);
+ text-align: center;
+}
+.paywall-cover img {
+ max-width: 100%;
+ border-radius: 10px;
+ margin-bottom: 16px;
+}
+.paywall-title {
+ font-size: 22px;
+ margin: 0 0 10px;
+}
+.paywall-excerpt {
+ color: var(--muted);
+ font-size: 14px;
+ margin: 0 0 16px;
+}
+.paywall-price {
+ font-size: 24px;
+ font-weight: 800;
+ color: var(--accent);
+ margin: 0 0 8px;
+}
+.paywall-hint {
+ color: var(--muted);
+ font-size: 13px;
+ margin: 0 0 18px;
+}
+.paywall-actions .btn {
+ margin: 0 6px;
+}
+.paid-teaser {
+ border: 1px dashed var(--border);
+ background: var(--bg-muted, #f6f6f6);
+ color: var(--muted);
+ border-radius: 10px;
+ padding: 14px 16px;
+ margin: 14px 0;
+ font-size: 14px;
+}
+.paid-teaser .btn {
+ margin-left: 8px;
+}
+
+/* 已解锁的付费内容块 */
+.paid-content {
+ border: 1px solid var(--border);
+ border-left: 4px solid var(--accent);
+ background: var(--bg);
+ border-radius: 10px;
+ padding: 14px 16px;
+ margin: 14px 0;
+}
+.paid-content-label {
+ font-size: 12px;
+ color: var(--accent);
+ font-weight: 600;
+ margin-bottom: 6px;
+}
+
+/* 分类页介绍区 */
+.category-intro {
+ display: flex;
+ gap: 14px;
+ align-items: flex-start;
+ background: var(--card);
+ border: 1px solid var(--border);
+ border-radius: 12px;
+ padding: 16px 18px;
+ margin-bottom: 18px;
+}
+.category-cover {
+ width: 128px;
+ height: 84px;
+ object-fit: cover;
+ border-radius: 8px;
+ flex-shrink: 0;
+}
+.category-description {
+ margin: 0 0 6px;
+ font-size: 14px;
+ color: var(--muted);
+ line-height: 1.7;
+}
+.category-meta {
+ margin: 0;
+ font-size: 12px;
+ color: var(--muted);
+}
diff --git a/public/themes/sablog/style.css b/public/themes/sablog/style.css
index 4eb8a3c..ed1f3d9 100644
--- a/public/themes/sablog/style.css
+++ b/public/themes/sablog/style.css
@@ -1,115 +1,691 @@
/* ============ Sablog 经典主题 ============ */
-* { margin: 0; padding: 0; box-sizing: border-box; }
+* {
+ margin: 0;
+ padding: 0;
+ box-sizing: border-box;
+}
body {
- background: #eef2f5 url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==) repeat-x top;
+ background: #eef2f5
+ url(data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mNk+M9QDwADhgGAWjR9awAAAABJRU5ErkJggg==)
+ repeat-x top;
font-family: Verdana, "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 13px;
color: #333;
line-height: 1.7;
}
-a { color: #1c62b0; text-decoration: none; }
-a:hover { color: #f60; text-decoration: underline; }
+a {
+ color: #1c62b0;
+ text-decoration: none;
+}
+a:hover {
+ color: #f60;
+ text-decoration: underline;
+}
-#outmain { width: 960px; margin: 0 auto; }
+#outmain {
+ width: 960px;
+ max-width: 100%;
+ margin: 0 auto;
+}
-#header { padding: 22px 10px 8px; }
-.logo a { font-size: 30px; font-weight: bold; color: #1c62b0; font-family: Georgia, serif; }
-.logo .description { color: #888; font-size: 12px; }
+#header {
+ padding: 22px 10px 8px;
+}
+.logo a {
+ font-size: 30px;
+ font-weight: bold;
+ color: #1c62b0;
+ font-family: Georgia, serif;
+}
+.logo .description {
+ color: #888;
+ font-size: 12px;
+}
-#nav { margin-top: 14px; border-top: 1px solid #d8dfe6; border-bottom: 1px solid #d8dfe6; background: #f7f9fb; }
-#nav ul { list-style: none; display: flex; flex-wrap: wrap; }
-#nav li { border-right: 1px solid #d8dfe6; }
-#nav a { display: block; padding: 7px 18px; color: #333; font-weight: bold; font-size: 13px; }
-#nav a:hover { background: #1c62b0; color: #fff; text-decoration: none; }
-#nav .current_page_item a { background: #1c62b0; color: #fff; }
+#nav {
+ margin-top: 14px;
+ border-top: 1px solid #d8dfe6;
+ border-bottom: 1px solid #d8dfe6;
+ background: #f7f9fb;
+}
+#nav ul {
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+}
+#nav li {
+ border-right: 1px solid #d8dfe6;
+}
+#nav a {
+ display: block;
+ padding: 7px 18px;
+ color: #333;
+ font-weight: bold;
+ font-size: 13px;
+}
+#nav a:hover {
+ background: #1c62b0;
+ color: #fff;
+ text-decoration: none;
+}
+#nav .current_page_item a {
+ background: #1c62b0;
+ color: #fff;
+}
-#page { padding: 14px 10px; display: flex; gap: 18px; align-items: flex-start; }
+#page {
+ padding: 14px 10px;
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 280px;
+ gap: 18px;
+ align-items: start;
+ width: 100%;
+ max-width: 100%;
+}
-#content { flex: 1; min-width: 0; }
+#content {
+ min-width: 0;
+ max-width: 100%;
+ overflow-wrap: anywhere;
+}
-.post-card, .post-full, .comments-section, .auth-box, .profile-box, .links-list {
+.post-card,
+.post-full,
+.comments-section,
+.auth-box,
+.profile-box,
+.links-list {
background: #fff;
border: 1px solid #d8dfe6;
padding: 18px 20px;
margin-bottom: 16px;
}
-.post-card.sticky { border-left: 4px solid #f60; }
-.post-title { font-size: 17px; margin-bottom: 6px; line-height: 1.4; }
-.post-card .post-title a { color: #1c62b0; }
-.badge { background: #f60; color: #fff; font-size: 11px; padding: 1px 5px; margin-right: 5px; }
-.post-meta { color: #999; font-size: 12px; margin-bottom: 8px; }
-.post-excerpt { color: #555; margin-bottom: 8px; }
-.post-footer { font-size: 12px; }
-.tag-link { color: #1c62b0; margin-right: 8px; }
+.post-card.sticky {
+ border-left: 4px solid #f60;
+}
+.post-title {
+ font-size: 17px;
+ margin-bottom: 6px;
+ line-height: 1.4;
+}
+.post-card .post-title a {
+ color: #1c62b0;
+}
+.badge {
+ background: #f60;
+ color: #fff;
+ font-size: 11px;
+ padding: 1px 5px;
+ margin-right: 5px;
+}
+.post-meta {
+ color: #999;
+ font-size: 12px;
+ margin-bottom: 8px;
+}
+.post-excerpt {
+ color: #555;
+ margin-bottom: 8px;
+}
+.post-footer {
+ font-size: 12px;
+}
+.tag-link {
+ color: #1c62b0;
+ margin-right: 8px;
+}
-.post-full .post-title { font-size: 22px; color: #1c62b0; }
-.post-content { margin-top: 10px; font-size: 14px; }
-.post-content img { max-width: 100%; }
-.post-content pre { background: #f4f4f4; border: 1px solid #ddd; padding: 10px; overflow: auto; margin: 10px 0; }
-.post-content blockquote { border-left: 4px solid #1c62b0; padding-left: 12px; color: #666; margin: 10px 0; }
-.post-content h1, .post-content h2, .post-content h3 { margin: 14px 0 6px; }
+.post-full .post-title {
+ font-size: 22px;
+ color: #1c62b0;
+}
+.post-content {
+ margin-top: 10px;
+ font-size: 14px;
+ max-width: 100%;
+ overflow-wrap: anywhere;
+ word-break: break-word;
+}
+.post-content img {
+ max-width: 100%;
+ height: auto;
+}
+.post-content pre,
+.post-content .codeText,
+.post-content ol[class^="dp-"] {
+ background: #f4f4f4;
+ border: 1px solid #ddd;
+ border-left: 3px solid #b3cbe1;
+ padding: 10px;
+ overflow-x: auto;
+ max-width: 100%;
+ margin: 10px 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.post-content code {
+ font-family: "Courier New", Courier, monospace;
+ word-break: break-all;
+ overflow-wrap: anywhere;
+ white-space: break-spaces;
+}
+.post-content pre code {
+ display: block;
+ background: none;
+ padding: 0;
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.post-content .codeText li {
+ white-space: pre-wrap;
+ word-break: break-word;
+}
+.post-content blockquote {
+ border-left: 4px solid #1c62b0;
+ padding-left: 12px;
+ color: #666;
+ margin: 10px 0;
+}
+.post-content h1,
+.post-content h2,
+.post-content h3 {
+ margin: 14px 0 6px;
+}
-.post-cover { margin: -18px -20px 12px; }
-.post-cover img { width: 100%; display: block; max-height: 320px; object-fit: cover; }
+.post-cover {
+ margin: -18px -20px 12px;
+}
+.post-cover img {
+ width: 100%;
+ display: block;
+ max-height: 320px;
+ object-fit: cover;
+}
-.comment { padding: 10px 0; border-bottom: 1px dashed #ddd; }
-.comment-head { font-size: 12px; margin-bottom: 3px; }
-.comment-time { color: #999; }
-.comment-body { font-size: 13px; }
-.comment-form { margin-top: 12px; }
-.form-row { margin-bottom: 8px; }
-.form-row label { display: block; font-size: 12px; margin-bottom: 3px; }
-.form-row input, .form-row textarea { width: 100%; padding: 6px 8px; border: 1px solid #ccc; font-size: 13px; font-family: inherit; }
-.honeypot { position: absolute; left: -9999px; opacity: 0; }
-.btn { background: #1c62b0; color: #fff; border: none; padding: 7px 18px; font-size: 13px; cursor: pointer; }
-.btn:hover { background: #f60; }
-.error { color: #c00; font-size: 12px; }
-.alert { padding: 8px 12px; margin-bottom: 12px; border: 1px solid; }
-.alert-success { background: #e8f5e9; border-color: #a5d6a7; color: #2e7d32; }
-.alert-error { background: #fdecea; border-color: #f5c6cb; color: #c62828; }
-.empty { color: #999; text-align: center; padding: 20px 0; }
-.list-title { font-size: 18px; margin-bottom: 12px; color: #1c62b0; }
+.comment {
+ padding: 10px 0;
+ border-bottom: 1px dashed #ddd;
+}
+.comment-head {
+ font-size: 12px;
+ margin-bottom: 3px;
+}
+.comment-time {
+ color: #999;
+}
+.comment-body {
+ font-size: 13px;
+}
+.comment-form {
+ margin-top: 12px;
+}
+.form-row {
+ margin-bottom: 8px;
+}
+.form-row label {
+ display: block;
+ font-size: 12px;
+ margin-bottom: 3px;
+}
+.form-row input,
+.form-row textarea {
+ width: 100%;
+ padding: 6px 8px;
+ border: 1px solid #ccc;
+ font-size: 13px;
+ font-family: inherit;
+}
+.honeypot {
+ position: absolute;
+ left: -9999px;
+ opacity: 0;
+}
+.btn {
+ background: #1c62b0;
+ color: #fff;
+ border: none;
+ padding: 7px 18px;
+ font-size: 13px;
+ cursor: pointer;
+}
+.btn:hover {
+ background: #f60;
+}
+.error {
+ color: #c00;
+ font-size: 12px;
+}
+.alert {
+ padding: 8px 12px;
+ margin-bottom: 12px;
+ border: 1px solid;
+}
+.alert-success {
+ background: #e8f5e9;
+ border-color: #a5d6a7;
+ color: #2e7d32;
+}
+.alert-error {
+ background: #fdecea;
+ border-color: #f5c6cb;
+ color: #c62828;
+}
+.alert-warning {
+ background: #fff8e1;
+ border-color: #ffe082;
+ color: #8a6d00;
+}
+.empty {
+ color: #999;
+ text-align: center;
+ padding: 20px 0;
+}
+.list-title {
+ font-size: 18px;
+ margin-bottom: 12px;
+ color: #1c62b0;
+}
+
+.view-mode-switcher {
+ font-size: 12px;
+ color: #888;
+ margin-bottom: 12px;
+ padding-bottom: 10px;
+ border-bottom: 1px solid #d8dfe6;
+}
+.view-mode-switcher a.active {
+ font-weight: 700;
+ color: #1c62b0;
+}
/* Sidebar */
-.sidebar { width: 280px; flex-shrink: 0; }
-.sidebar .searchbox { background: #fff; border: 1px solid #d8dfe6; padding: 10px; margin-bottom: 14px; }
-.sidebar .searchbox input[type=search] { width: 180px; padding: 4px; border: 1px solid #ccc; }
-.sidebar .widget { background: #fff; border: 1px solid #d8dfe6; margin-bottom: 14px; }
-.sidebar .widget h3 { background: #f7f9fb; border-bottom: 1px solid #d8dfe6; font-size: 13px; padding: 7px 12px; color: #1c62b0; }
-.sidebar .widget ul { list-style: none; padding: 6px 12px; }
-.sidebar .widget li { padding: 3px 0; font-size: 12px; border-bottom: 1px dotted #eee; }
-.sidebar .tagcloud { padding: 8px 12px; }
-.sidebar .tagcloud a { color: #1c62b0; margin-right: 6px; }
+.sidebar {
+ width: auto;
+ min-width: 0;
+}
+.sidebar .searchbox {
+ background: #fff;
+ border: 1px solid #d8dfe6;
+ padding: 10px;
+ margin-bottom: 14px;
+}
+.sidebar .searchbox input[type="search"] {
+ width: 180px;
+ padding: 4px;
+ border: 1px solid #ccc;
+}
+.sidebar .widget {
+ background: #fff;
+ border: 1px solid #d8dfe6;
+ margin-bottom: 14px;
+}
+.sidebar .widget h3 {
+ background: #f7f9fb;
+ border-bottom: 1px solid #d8dfe6;
+ font-size: 13px;
+ padding: 7px 12px;
+ color: #1c62b0;
+}
+.sidebar .widget ul {
+ list-style: none;
+ padding: 6px 12px;
+}
+.sidebar .widget li {
+ padding: 3px 0;
+ font-size: 12px;
+ border-bottom: 1px dotted #eee;
+}
+.sidebar .tagcloud {
+ padding: 8px 12px;
+}
+.sidebar .tagcloud a {
+ color: #1c62b0;
+ margin-right: 6px;
+}
/* 兜底页面(归档/标签云/友链/评论/登录等使用默认布局) */
-.main-layout { display: flex; gap: 18px; align-items: flex-start; padding: 14px 10px; }
-.main-layout .content { flex: 1; min-width: 0; }
-.main-layout .sidebar { width: 280px; }
+.container.main-layout {
+ max-width: 960px;
+ margin: 0 auto;
+}
+.main-layout {
+ display: grid;
+ grid-template-columns: minmax(0, 1fr) 280px;
+ gap: 18px;
+ align-items: start;
+ padding: 14px 10px;
+}
+.main-layout .content {
+ min-width: 0;
+ max-width: 100%;
+}
+.main-layout .sidebar {
+ width: auto;
+ min-width: 0;
+}
-.pagination-links { display: flex; gap: 6px; flex-wrap: wrap; margin: 12px 0; }
-.pagination-links a, .pagination-links .current, .pagination-links .disabled { padding: 4px 10px; border: 1px solid #d8dfe6; background: #fff; font-size: 12px; }
-.pagination-links .current { background: #1c62b0; color: #fff; }
+.pagination {
+ width: 100%;
+ max-width: 100%;
+ min-width: 0;
+ margin: 12px 0;
+}
+.pagination-links {
+ display: flex;
+ gap: 4px;
+ flex-wrap: wrap;
+ align-items: center;
+ max-width: 100%;
+ margin: 12px 0;
+}
+.pagination-info {
+ color: #888;
+ font-size: 12px;
+ margin-right: 6px;
+ white-space: nowrap;
+}
+.pagination-links a,
+.pagination-links .current,
+.pagination-links .disabled {
+ padding: 2px 7px;
+ border: 1px solid #d8dfe6;
+ background: #fff;
+ font-size: 12px;
+ line-height: 1.4;
+}
+.pagination-links .current {
+ background: #1c62b0;
+ color: #fff;
+}
-#footer { clear: both; text-align: center; padding: 14px 0 24px; color: #888; font-size: 12px; }
-#footer a { color: #1c62b0; }
+#footer {
+ clear: both;
+ text-align: center;
+ padding: 14px 0 24px;
+ color: #888;
+ font-size: 12px;
+}
+#footer a {
+ color: #1c62b0;
+}
-.archive-year { font-size: 15px; margin: 12px 0 4px; color: #1c62b0; }
-.archive-list { list-style: none; }
-.archive-posts { margin: 4px 0 10px 20px; }
+.archive-year {
+ font-size: 15px;
+ margin: 12px 0 4px;
+ color: #1c62b0;
+}
+.archive-list {
+ list-style: none;
+}
+.archive-posts {
+ margin: 4px 0 10px 20px;
+}
-.links-list { list-style: none; }
-.links-list li { padding: 5px 0; }
-.link-note { color: #999; }
+.links-list {
+ list-style: none;
+}
+.links-list li {
+ padding: 5px 0;
+}
+.link-note {
+ color: #999;
+}
-.tag-cloud.big { line-height: 2.4; }
+.tag-cloud.big {
+ line-height: 2.4;
+}
-.auth-layout { display: flex; justify-content: center; padding: 30px 10px; }
-.auth-box { width: 100%; max-width: 420px; }
-.auth-alt { margin-top: 12px; font-size: 12px; }
-.search-form { margin-bottom: 12px; }
-.search-form input { padding: 5px; border: 1px solid #ccc; width: 220px; }
+.auth-layout {
+ display: flex;
+ justify-content: center;
+ padding: 30px 10px;
+}
+.auth-box {
+ width: 100%;
+ max-width: 420px;
+}
+.auth-alt {
+ margin-top: 12px;
+ font-size: 12px;
+}
+.search-form {
+ margin-bottom: 12px;
+}
+.search-form input {
+ padding: 5px;
+ border: 1px solid #ccc;
+ width: 220px;
+}
-.inline-form { display: inline; }
-.link-btn { background: none; border: none; color: #1c62b0; cursor: pointer; font-size: 13px; padding: 0; font-weight: bold; }
+.inline-form {
+ display: inline;
+}
+.link-btn {
+ background: none;
+ border: none;
+ color: #1c62b0;
+ cursor: pointer;
+ font-size: 13px;
+ padding: 0;
+ font-weight: bold;
+}
+
+/* ============ 移动端适配 ============ */
+@media (max-width: 768px) {
+ #outmain {
+ width: 100%;
+ }
+ #header {
+ padding: 14px 10px;
+ }
+ .logo a {
+ font-size: 24px;
+ }
+ #nav ul {
+ flex-wrap: wrap;
+ }
+ #nav a {
+ padding: 8px 12px;
+ font-size: 13px;
+ }
+ #page {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ #content {
+ width: 100%;
+ }
+ .sidebar {
+ width: 100%;
+ }
+ .main-layout {
+ grid-template-columns: minmax(0, 1fr);
+ }
+ .main-layout .content {
+ width: 100%;
+ }
+ .main-layout .sidebar {
+ width: 100%;
+ }
+ .sidebar .searchbox input[type="search"] {
+ width: 70%;
+ }
+ .post-card,
+ .post-full,
+ .comments-section {
+ padding: 14px 16px;
+ }
+ .post-cover {
+ margin: -14px -16px 12px;
+ }
+ .post-full .post-title {
+ font-size: 22px;
+ }
+ .archive-posts {
+ margin-left: 10px;
+ }
+}
+
+/* TOC */
+.toc {
+ background: #f7f9fb;
+ border: 1px solid #d8dfe6;
+ padding: 10px 14px;
+ margin: 0 0 14px;
+ font-size: 13px;
+}
+.toc summary {
+ cursor: pointer;
+ font-weight: bold;
+ margin-bottom: 4px;
+}
+.toc-list {
+ list-style: none;
+}
+.toc-list li {
+ padding: 2px 0;
+}
+.toc-level-3 {
+ padding-left: 14px;
+}
+.toc-level-4,
+.toc-level-5,
+.toc-level-6 {
+ padding-left: 28px;
+}
+
+.sr-only {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ padding: 0;
+ margin: -1px;
+ overflow: hidden;
+ clip: rect(0 0 0 0);
+ white-space: nowrap;
+ border: 0;
+}
+
+/* Paywall */
+.paywall {
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ padding: 24px 20px;
+ background: #fafafa;
+ text-align: center;
+}
+.paywall-cover img {
+ max-width: 100%;
+ border-radius: 6px;
+ margin-bottom: 14px;
+}
+.paywall-title {
+ font-size: 20px;
+ margin: 0 0 10px;
+}
+.paywall-excerpt {
+ color: #666;
+ font-size: 14px;
+ margin: 0 0 14px;
+}
+.paywall-price {
+ font-size: 22px;
+ font-weight: 800;
+ color: #f60;
+ margin: 0 0 8px;
+}
+.paywall-hint {
+ color: #888;
+ font-size: 13px;
+ margin: 0 0 16px;
+}
+.paywall-actions .btn {
+ margin: 0 6px;
+}
+.paid-teaser {
+ border: 1px dashed #f5c6cb;
+ background: #fdecea;
+ color: #c62828;
+ border-radius: 6px;
+ padding: 12px 14px;
+ margin: 12px 0;
+ font-size: 14px;
+}
+.paid-teaser .btn {
+ margin-left: 8px;
+}
+
+/* 阅读全文链接 */
+.read-more {
+ display: inline-block;
+ margin-top: 8px;
+ font-size: 13px;
+ color: #1c62b0;
+}
+.read-more:hover {
+ color: #f60;
+}
+
+/* 社交账号:横向胶囊列表 */
+.social-links {
+ list-style: none;
+ display: flex;
+ flex-wrap: wrap;
+ gap: 6px;
+ margin: 0;
+ padding: 0;
+}
+.social-links li {
+ font-size: 12px;
+ background: #f7f9fb;
+ border: 1px solid #d8dfe6;
+ border-radius: 999px;
+ padding: 3px 10px;
+}
+
+/* 已解锁的付费内容块 */
+.paid-content {
+ border: 1px dashed #f5c6cb;
+ border-left: 4px solid #f60;
+ background: #fff8f0;
+ border-radius: 6px;
+ padding: 12px 14px;
+ margin: 12px 0;
+}
+.paid-content-label {
+ font-size: 12px;
+ color: #f60;
+ font-weight: 700;
+ margin-bottom: 6px;
+}
+
+/* 分类页介绍区 */
+.category-intro {
+ display: flex;
+ gap: 14px;
+ align-items: flex-start;
+ background: #fff;
+ border: 1px solid #e0e0e0;
+ border-radius: 8px;
+ padding: 14px 16px;
+ margin-bottom: 16px;
+}
+.category-cover {
+ width: 120px;
+ height: 80px;
+ object-fit: cover;
+ border-radius: 6px;
+ flex-shrink: 0;
+}
+.category-description {
+ margin: 0 0 6px;
+ font-size: 14px;
+ color: #555;
+ line-height: 1.7;
+}
+.category-meta {
+ margin: 0;
+ font-size: 12px;
+ color: #999;
+}
diff --git a/resources/views/filament/hooks/topbar-tools.blade.php b/resources/views/filament/hooks/topbar-tools.blade.php
index dc948cf..a74e4b6 100644
--- a/resources/views/filament/hooks/topbar-tools.blade.php
+++ b/resources/views/filament/hooks/topbar-tools.blade.php
@@ -9,6 +9,18 @@
{{ $environmentName }}
+
+ 网站
+
+
+
+
+
+ - 站点语言
+ - {{ $this->info['locale'] }}
+
+
+ - 回退语言
+ - {{ $this->info['fallback'] }}
+
+
+ - 假数据语言
+ - {{ $this->info['faker'] }}
+
+
+
+ 改语言请编辑 .env 里的 APP_LOCALE,后台菜单文案本身是中文写死的。
+
+
+
+
+ 未开通
+
+ 现在每篇文章只有一份正文,没有中英两个版本可以切换。这个页面先让你看到现状,以后如果做文章翻译,会在这里列出语言和对应文章。
+
+
+
+
+
+ @if(count($this->info['app_files']) === 0)
+ 还没有站点自己的语言文件。
+ @else
+
+ @foreach($this->info['app_files'] as $locale)
+ {{ $locale }}
+ @endforeach
+
+ @endif
+
+
+
+
+ 这些是后台组件自带的翻译,不是文章内容翻译。
+
+
+ @foreach($this->info['filament_locales'] as $locale)
+ {{ $locale }}
+ @endforeach
+
+
+
+
diff --git a/resources/views/filament/pages/page-cache.blade.php b/resources/views/filament/pages/page-cache.blade.php
new file mode 100644
index 0000000..08d22fb
--- /dev/null
+++ b/resources/views/filament/pages/page-cache.blade.php
@@ -0,0 +1,64 @@
+
+
+
+ 当前缓存页
+ {{ $this->stats['count'] }} 条
+
+
+ 命中率
+ {{ $this->stats['rate'] }}%
+
+
+ 命中 / 未命中
+ {{ $this->stats['hits'] }} / {{ $this->stats['misses'] }}
+
+
+ 过期时间
+ {{ $this->stats['ttl'] }} 秒
+
+
+
+
+
+ 只缓存未登录访客打开的页面。登录后看前台不会进缓存,所以这里可能暂时是空的。
+
+
+ @if(count($this->entries) === 0)
+ 当前没有前台页面缓存。
+ @else
+
+
+
+
+ 地址
+ 缓存时间
+ 操作
+
+
+
+ @foreach($this->entries as $entry)
+
+
+
+ {{ $entry['url'] }}
+
+
+ {{ $entry['cached_at'] ?: '—' }}
+
+
+ 删除
+
+
+
+ @endforeach
+
+
+
+ @endif
+
+
diff --git a/resources/views/index.blade.php b/resources/views/index.blade.php
index a84b267..83e6eb3 100644
--- a/resources/views/index.blade.php
+++ b/resources/views/index.blade.php
@@ -11,6 +11,8 @@
{{ session('error') }}
@endif
+ @include('partials.view-mode-switcher')
+
@forelse($posts as $post)
@include('partials.post-card', ['post' => $post])
@empty
diff --git a/resources/views/list.blade.php b/resources/views/list.blade.php
index 81e1aa4..da04505 100644
--- a/resources/views/list.blade.php
+++ b/resources/views/list.blade.php
@@ -24,6 +24,8 @@
@endif
+ @include('partials.view-mode-switcher')
+
@forelse($posts as $post)
@include('partials.post-card', ['post' => $post])
@empty
diff --git a/resources/views/partials/feed-links.blade.php b/resources/views/partials/feed-links.blade.php
new file mode 100644
index 0000000..7e31b42
--- /dev/null
+++ b/resources/views/partials/feed-links.blade.php
@@ -0,0 +1,16 @@
+@php
+ $asList = $asList ?? false;
+ $feedLinks = [
+ ['label' => 'RSS', 'url' => route('feed.rss')],
+ ['label' => 'Sitemap', 'url' => route('feed.sitemap')],
+ ['label' => 'llms.txt', 'url' => route('feed.llms')],
+ ];
+@endphp
+@foreach($feedLinks as $feedLink)
+ @if($asList)
+ {{ $feedLink['label'] }}
+ @else
+ @if(! $loop->first) · @endif
+ {{ $feedLink['label'] }}
+ @endif
+@endforeach
diff --git a/resources/views/partials/footer.blade.php b/resources/views/partials/footer.blade.php
index b4e19b4..2d5f3d5 100644
--- a/resources/views/partials/footer.blade.php
+++ b/resources/views/partials/footer.blade.php
@@ -7,6 +7,7 @@
{{ $siteIcp }}
@endif
+ @include('partials.feed-links')
diff --git a/resources/views/partials/head.blade.php b/resources/views/partials/head.blade.php
index 19dfcd3..ea77a38 100644
--- a/resources/views/partials/head.blade.php
+++ b/resources/views/partials/head.blade.php
@@ -12,6 +12,7 @@
+
@stack('head')
@stack('theme:head')
diff --git a/resources/views/partials/header.blade.php b/resources/views/partials/header.blade.php
index 082c10b..8cfc1a2 100644
--- a/resources/views/partials/header.blade.php
+++ b/resources/views/partials/header.blade.php
@@ -12,6 +12,7 @@
标签
友链
评论
+ @include('partials.feed-links')
diff --git a/resources/views/partials/pagination-links.blade.php b/resources/views/partials/pagination-links.blade.php
index 091d255..41f78db 100644
--- a/resources/views/partials/pagination-links.blade.php
+++ b/resources/views/partials/pagination-links.blade.php
@@ -1,13 +1,13 @@
@if ($paginator->hasPages())
-