@fecommunity/reactpress 4.0.0-beta.15 → 4.0.0-beta.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/admin-dist/assets/{admin-bamoE2Tn.js → admin-CF4LIXsB.js} +1 -1
  2. package/admin-dist/assets/{admin-CrL6Xc9b.js → admin-DYBZ60Ac.js} +1 -1
  3. package/admin-dist/assets/{cssMode-J2cfgFkq.js → cssMode-BJqbvgLG.js} +1 -1
  4. package/admin-dist/assets/{freemarker2-L_gr_Wq4.js → freemarker2-CYGOmtYQ.js} +1 -1
  5. package/admin-dist/assets/{handlebars-D81kzUDC.js → handlebars-qhE4jgvZ.js} +1 -1
  6. package/admin-dist/assets/{html-Cung-Yr0.js → html-U5gI8Ly8.js} +1 -1
  7. package/admin-dist/assets/{htmlMode-C6QemWl2.js → htmlMode-DQd5Eb2D.js} +1 -1
  8. package/admin-dist/assets/{index-DXTkTQ21.js → index-BKNoDtfb.js} +178 -178
  9. package/admin-dist/assets/{javascript-WUcGtCKt.js → javascript-DU3Ebk7L.js} +1 -1
  10. package/admin-dist/assets/{jsonMode-Dp0R9P1F.js → jsonMode-D1w8Vhjb.js} +1 -1
  11. package/admin-dist/assets/{liquid-OFb9c-6B.js → liquid-BPVbnl_K.js} +1 -1
  12. package/admin-dist/assets/{mdx-CUfjLxA3.js → mdx-CMSYE9vh.js} +1 -1
  13. package/admin-dist/assets/{python-zxEYY77U.js → python-Dbpms-Xp.js} +1 -1
  14. package/admin-dist/assets/{razor-DJrSr4ly.js → razor-CWQdYrqP.js} +1 -1
  15. package/admin-dist/assets/{tsMode-Dtxx1cRn.js → tsMode-DAsZhwU2.js} +1 -1
  16. package/admin-dist/assets/{typescript-DT11gl4K.js → typescript-7mrVr_47.js} +1 -1
  17. package/admin-dist/assets/{xml-BIbLvdwx.js → xml-DbPOrhNN.js} +1 -1
  18. package/admin-dist/assets/{yaml-BEeUZskE.js → yaml-6hDyKyrt.js} +1 -1
  19. package/admin-dist/index.html +1 -1
  20. package/out/bin/reactpress-theme-client.js +4 -0
  21. package/out/bin/reactpress.js +7 -2
  22. package/out/core/services/admin-static.js +79 -26
  23. package/out/core/services/local-site.js +68 -0
  24. package/out/lib/bootstrap.js +9 -0
  25. package/out/lib/i18n/strings.js +20 -8
  26. package/out/lib/lifecycle.js +9 -0
  27. package/out/lib/pm2-runtime.js +15 -0
  28. package/out/lib/project-logs.js +318 -12
  29. package/package.json +2 -1
  30. package/plugins/hello-world/README.md +104 -0
  31. package/plugins/hello-world/dist/config.d.ts +4 -0
  32. package/plugins/hello-world/dist/config.js +17 -0
  33. package/plugins/hello-world/dist/index.d.ts +10 -0
  34. package/plugins/hello-world/dist/index.js +15 -0
  35. package/plugins/hello-world/dist/summary.d.ts +3 -0
  36. package/plugins/hello-world/dist/summary.js +33 -0
  37. package/plugins/hello-world/dist/types.d.ts +16 -0
  38. package/plugins/hello-world/dist/types.js +2 -0
  39. package/plugins/hello-world/locales/en.json +24 -0
  40. package/plugins/hello-world/locales/zh.json +24 -0
  41. package/plugins/hello-world/package.json +20 -0
  42. package/plugins/hello-world/plugin.json +61 -0
  43. package/plugins/hello-world/tsconfig.json +16 -0
  44. package/plugins/image-optimizer/README.md +43 -0
  45. package/plugins/image-optimizer/admin/OptimizeDashboard.tsx +249 -0
  46. package/plugins/image-optimizer/admin/api.ts +133 -0
  47. package/plugins/image-optimizer/admin/index.ts +16 -0
  48. package/plugins/image-optimizer/admin/locale.ts +68 -0
  49. package/plugins/image-optimizer/admin/optimize-dashboard.module.css +80 -0
  50. package/plugins/image-optimizer/dist/config.d.ts +8 -0
  51. package/plugins/image-optimizer/dist/config.js +13 -0
  52. package/plugins/image-optimizer/dist/index.d.ts +10 -0
  53. package/plugins/image-optimizer/dist/index.js +13 -0
  54. package/plugins/image-optimizer/locales/en.json +51 -0
  55. package/plugins/image-optimizer/locales/zh.json +53 -0
  56. package/plugins/image-optimizer/package.json +20 -0
  57. package/plugins/image-optimizer/plugin.json +58 -0
  58. package/plugins/image-optimizer/tsconfig.json +16 -0
  59. package/plugins/package.json +14 -0
  60. package/plugins/seo/README.md +60 -0
  61. package/plugins/seo/admin/ArticleEditorSeoPanel.tsx +67 -0
  62. package/plugins/seo/admin/article-editor-seo-panel.module.css +74 -0
  63. package/plugins/seo/admin/index.ts +18 -0
  64. package/plugins/seo/dist/config.d.ts +4 -0
  65. package/plugins/seo/dist/config.js +19 -0
  66. package/plugins/seo/dist/index.d.ts +10 -0
  67. package/plugins/seo/dist/index.js +23 -0
  68. package/plugins/seo/dist/seo.d.ts +3 -0
  69. package/plugins/seo/dist/seo.js +79 -0
  70. package/plugins/seo/dist/slug.d.ts +2 -0
  71. package/plugins/seo/dist/slug.js +13 -0
  72. package/plugins/seo/dist/types.d.ts +23 -0
  73. package/plugins/seo/dist/types.js +2 -0
  74. package/plugins/seo/locales/en.json +32 -0
  75. package/plugins/seo/locales/zh.json +32 -0
  76. package/plugins/seo/package.json +20 -0
  77. package/plugins/seo/plugin.json +76 -0
  78. package/plugins/seo/tsconfig.json +16 -0
  79. package/server/bin/reactpress-server.js +17 -0
  80. package/server/package-lock.json +3 -3
  81. package/server/package.json +1 -1
  82. package/toolkit/package.json +1 -1
@@ -0,0 +1,133 @@
1
+ import { resolveApiBaseUrl } from "@fecommunity/reactpress-toolkit/plugin/react";
2
+
3
+ const DEFAULT_API_BASE = "/api";
4
+
5
+ export interface ImageOptimizeReport {
6
+ total: number;
7
+ alreadyOptimized: number;
8
+ needsOptimization: number;
9
+ skipped: {
10
+ nonImage: number;
11
+ svg: number;
12
+ gif: number;
13
+ missing: number;
14
+ };
15
+ storageBytes: {
16
+ current: number;
17
+ estimatedAfter: number;
18
+ estimatedSaved: number;
19
+ };
20
+ contentRefs: {
21
+ articles: number;
22
+ pages: number;
23
+ };
24
+ }
25
+
26
+ export interface OptimizeRunOptions {
27
+ dryRun?: boolean;
28
+ batchSize?: number;
29
+ skipGif?: boolean;
30
+ rewriteContent?: boolean;
31
+ cleanupOriginals?: boolean;
32
+ limit?: number;
33
+ }
34
+
35
+ export interface OptimizeJobItemResult {
36
+ fileId: string;
37
+ originalname: string;
38
+ status: "success" | "skipped" | "failed";
39
+ oldUrl?: string;
40
+ newUrl?: string;
41
+ savedBytes?: number;
42
+ error?: string;
43
+ }
44
+
45
+ export interface OptimizeJob {
46
+ id: string;
47
+ status: "pending" | "running" | "completed" | "failed";
48
+ dryRun: boolean;
49
+ total: number;
50
+ processed: number;
51
+ succeeded: number;
52
+ failed: number;
53
+ skipped: number;
54
+ savedBytes: number;
55
+ urlMap: Record<string, string>;
56
+ items: OptimizeJobItemResult[];
57
+ rewriteContent: boolean;
58
+ contentRewrite?: {
59
+ articles: number;
60
+ pages: number;
61
+ revisions: number;
62
+ };
63
+ error?: string;
64
+ startedAt: string;
65
+ finishedAt?: string;
66
+ }
67
+
68
+ function readAccessToken(): string | null {
69
+ if (typeof localStorage === "undefined") return null;
70
+ try {
71
+ const raw = localStorage.getItem("auth-storage");
72
+ if (!raw) return null;
73
+ const parsed = JSON.parse(raw) as { state?: { tokens?: { accessToken?: string } } };
74
+ return parsed.state?.tokens?.accessToken ?? null;
75
+ } catch {
76
+ return null;
77
+ }
78
+ }
79
+
80
+ function readApiErrorMessage(err: unknown, fallback: string): string {
81
+ if (err instanceof Error && err.message.trim()) return err.message;
82
+ return fallback;
83
+ }
84
+
85
+ async function optimizeFetch<T>(path: string, init?: RequestInit): Promise<T> {
86
+ const base = (await resolveApiBaseUrl(DEFAULT_API_BASE)).replace(/\/$/, "");
87
+ const token = readAccessToken();
88
+ const res = await fetch(`${base}${path}`, {
89
+ ...init,
90
+ headers: {
91
+ "Content-Type": "application/json",
92
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
93
+ ...init?.headers,
94
+ },
95
+ });
96
+ const body = (await res.json()) as {
97
+ success?: boolean;
98
+ code?: number;
99
+ data?: T;
100
+ msg?: string;
101
+ message?: string;
102
+ };
103
+ if (res.status === 401) {
104
+ throw new Error("SESSION_EXPIRED");
105
+ }
106
+ if (!res.ok) {
107
+ throw new Error(body.msg ?? body.message ?? `Request failed: ${res.status}`);
108
+ }
109
+ if (typeof body.code === "number" && body.code !== 0) {
110
+ throw new Error(body.message ?? "Request failed");
111
+ }
112
+ if (body.success === false) {
113
+ throw new Error(body.msg ?? "Request failed");
114
+ }
115
+ return body.data as T;
116
+ }
117
+
118
+ export function fetchOptimizeReport() {
119
+ return optimizeFetch<ImageOptimizeReport>("/file/optimize/report");
120
+ }
121
+
122
+ export function startOptimizeJob(options: OptimizeRunOptions) {
123
+ return optimizeFetch<OptimizeJob>("/file/optimize/run", {
124
+ method: "POST",
125
+ body: JSON.stringify(options),
126
+ });
127
+ }
128
+
129
+ export function fetchOptimizeJob(jobId: string) {
130
+ return optimizeFetch<OptimizeJob>(`/file/optimize/job/${encodeURIComponent(jobId)}`);
131
+ }
132
+
133
+ export { readApiErrorMessage };
@@ -0,0 +1,16 @@
1
+ import type { PluginAdminModule } from "@fecommunity/reactpress-toolkit/plugin/admin";
2
+
3
+ export { SettingsPanel } from "./OptimizeDashboard";
4
+
5
+ import { SettingsPanel } from "./OptimizeDashboard";
6
+
7
+ export function registerAdmin(): void {
8
+ // Dashboard is mounted via SettingsPanel on the plugin settings page.
9
+ }
10
+
11
+ const pluginAdmin: PluginAdminModule = {
12
+ registerAdmin,
13
+ SettingsPanel,
14
+ };
15
+
16
+ export default pluginAdmin;
@@ -0,0 +1,68 @@
1
+ import { useEffect, useMemo, useState } from "react";
2
+
3
+ import { resolveApiBaseUrl } from "@fecommunity/reactpress-toolkit/plugin/react";
4
+
5
+ type LocaleMessages = Record<string, unknown>;
6
+
7
+ function readNestedMessage(messages: LocaleMessages, path: string): string | undefined {
8
+ const parts = path.split(".");
9
+ let current: unknown = messages;
10
+ for (const part of parts) {
11
+ if (!current || typeof current !== "object") return undefined;
12
+ current = (current as Record<string, unknown>)[part];
13
+ }
14
+ return typeof current === "string" ? current : undefined;
15
+ }
16
+
17
+ function detectLocale(): string {
18
+ if (typeof document === "undefined") return "en";
19
+ const lang = document.documentElement.lang || navigator.language || "en";
20
+ return lang.toLowerCase().startsWith("zh") ? "zh" : "en";
21
+ }
22
+
23
+ async function fetchPluginLocale(pluginId: string, locale: string): Promise<LocaleMessages> {
24
+ const base = (await resolveApiBaseUrl("/api")).replace(/\/$/, "");
25
+ const token = (() => {
26
+ try {
27
+ const raw = localStorage.getItem("auth-storage");
28
+ if (!raw) return null;
29
+ const parsed = JSON.parse(raw) as { state?: { tokens?: { accessToken?: string } } };
30
+ return parsed.state?.tokens?.accessToken ?? null;
31
+ } catch {
32
+ return null;
33
+ }
34
+ })();
35
+ const res = await fetch(
36
+ `${base}/extension/plugins/${encodeURIComponent(pluginId)}/locales/${encodeURIComponent(locale)}`,
37
+ {
38
+ headers: {
39
+ ...(token ? { Authorization: `Bearer ${token}` } : {}),
40
+ },
41
+ },
42
+ );
43
+ if (!res.ok) return {};
44
+ const body = (await res.json()) as { data?: { messages?: LocaleMessages } };
45
+ return body.data?.messages ?? {};
46
+ }
47
+
48
+ export function usePluginDashboardText(pluginId: string) {
49
+ const locale = detectLocale();
50
+ const [messages, setMessages] = useState<LocaleMessages>({});
51
+
52
+ useEffect(() => {
53
+ let cancelled = false;
54
+ void fetchPluginLocale(pluginId, locale).then((next) => {
55
+ if (!cancelled) setMessages(next);
56
+ });
57
+ return () => {
58
+ cancelled = true;
59
+ };
60
+ }, [locale, pluginId]);
61
+
62
+ return useMemo(
63
+ () =>
64
+ (key: string, fallback: string) =>
65
+ readNestedMessage(messages, `dashboard.${key}`) ?? fallback,
66
+ [messages],
67
+ );
68
+ }
@@ -0,0 +1,80 @@
1
+ .optimizeDashboard {
2
+ margin-bottom: 24px;
3
+ padding-bottom: 20px;
4
+ border-bottom: 1px solid var(--ant-color-border-secondary);
5
+ }
6
+
7
+ .sectionTitle {
8
+ margin: 0 0 16px;
9
+ font-size: 14px;
10
+ font-weight: 600;
11
+ color: var(--ant-color-text);
12
+ }
13
+
14
+ .statsGrid {
15
+ display: grid;
16
+ grid-template-columns: repeat(auto-fill, minmax(140px, 1fr));
17
+ gap: 12px;
18
+ margin-bottom: 16px;
19
+ }
20
+
21
+ .statCard {
22
+ padding: 12px 14px;
23
+ border: 1px solid var(--ant-color-border-secondary);
24
+ border-radius: 8px;
25
+ background: var(--ant-color-fill-quaternary);
26
+ }
27
+
28
+ .statLabel {
29
+ margin: 0 0 4px;
30
+ font-size: 12px;
31
+ color: var(--ant-color-text-secondary);
32
+ }
33
+
34
+ .statValue {
35
+ margin: 0;
36
+ font-size: 20px;
37
+ font-weight: 600;
38
+ line-height: 1.2;
39
+ color: var(--ant-color-text);
40
+ }
41
+
42
+ .actions {
43
+ display: flex;
44
+ flex-wrap: wrap;
45
+ gap: 8px;
46
+ margin-bottom: 16px;
47
+ }
48
+
49
+ .progressBlock {
50
+ margin-top: 12px;
51
+ }
52
+
53
+ .hint {
54
+ margin: 8px 0 0;
55
+ font-size: 13px;
56
+ color: var(--ant-color-text-secondary);
57
+ }
58
+
59
+ .emptyJob {
60
+ margin: 8px 0 0;
61
+ font-size: 13px;
62
+ color: var(--ant-color-text-tertiary);
63
+ }
64
+
65
+ .warn {
66
+ margin: 0 0 16px;
67
+ padding: 10px 12px;
68
+ border-radius: 8px;
69
+ background: var(--ant-color-warning-bg);
70
+ border: 1px solid var(--ant-color-warning-border);
71
+ color: var(--ant-color-text);
72
+ font-size: 13px;
73
+ }
74
+
75
+ .errorList {
76
+ margin: 8px 0 0;
77
+ padding-left: 18px;
78
+ font-size: 13px;
79
+ color: var(--ant-color-error);
80
+ }
@@ -0,0 +1,8 @@
1
+ import type { PluginContext } from '@fecommunity/reactpress-toolkit/plugin/server';
2
+ export interface ImageOptimizerConfig {
3
+ batchSize: number;
4
+ skipGif: boolean;
5
+ rewriteContent: boolean;
6
+ cleanupOriginals: boolean;
7
+ }
8
+ export declare function resolveConfig(ctx: PluginContext): ImageOptimizerConfig;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveConfig = resolveConfig;
4
+ function resolveConfig(ctx) {
5
+ const raw = ctx.config ?? {};
6
+ const batchSize = Number(raw.batchSize);
7
+ return {
8
+ batchSize: Number.isFinite(batchSize) ? Math.min(Math.max(batchSize, 1), 200) : 50,
9
+ skipGif: raw.skipGif !== false,
10
+ rewriteContent: raw.rewriteContent === true,
11
+ cleanupOriginals: raw.cleanupOriginals === true,
12
+ };
13
+ }
@@ -0,0 +1,10 @@
1
+ import type { HookService, PluginContext } from '@fecommunity/reactpress-toolkit/plugin/server';
2
+ /**
3
+ * 图片资源优化:批量压缩历史素材由 Core FileOptimizationService 执行;
4
+ * 本插件负责注册与配置,管理端仪表盘见 admin/OptimizeDashboard。
5
+ */
6
+ export declare function register(_hooks: HookService, ctx: PluginContext): void;
7
+ declare const _default: {
8
+ register: typeof register;
9
+ };
10
+ export default _default;
@@ -0,0 +1,13 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.register = register;
4
+ const config_1 = require("./config");
5
+ /**
6
+ * 图片资源优化:批量压缩历史素材由 Core FileOptimizationService 执行;
7
+ * 本插件负责注册与配置,管理端仪表盘见 admin/OptimizeDashboard。
8
+ */
9
+ function register(_hooks, ctx) {
10
+ const config = (0, config_1.resolveConfig)(ctx);
11
+ ctx.logger.log(`registered (batchSize=${config.batchSize}, skipGif=${config.skipGif}, rewriteContent=${config.rewriteContent})`);
12
+ }
13
+ exports.default = { register };
@@ -0,0 +1,51 @@
1
+ {
2
+ "meta": {
3
+ "name": "Image Optimizer",
4
+ "description": "Analyze legacy media and batch-compress to WebP variants; optionally rewrite URLs in articles and pages."
5
+ },
6
+ "settings": {
7
+ "batchSize": {
8
+ "title": "Batch size",
9
+ "description": "Maximum images processed per job (1–200)."
10
+ },
11
+ "skipGif": {
12
+ "title": "Skip GIF",
13
+ "description": "GIF to WebP loses animation; skipped by default."
14
+ },
15
+ "rewriteContent": {
16
+ "title": "Rewrite content URLs after optimize",
17
+ "description": "Replace old image URLs in articles and pages when the job completes."
18
+ },
19
+ "cleanupOriginals": {
20
+ "title": "Delete originals after optimize",
21
+ "description": "Enable only after verification; old URLs will stop working."
22
+ }
23
+ },
24
+ "dashboard": {
25
+ "title": "Asset analysis",
26
+ "analyze": "Analyze",
27
+ "dryRun": "Dry run",
28
+ "run": "Start batch optimize",
29
+ "refresh": "Refresh",
30
+ "total": "Total assets",
31
+ "optimized": "Optimized",
32
+ "pending": "Pending",
33
+ "estimatedSaved": "Est. savings",
34
+ "contentRefs": "Content references",
35
+ "articles": "Articles",
36
+ "pages": "Pages",
37
+ "skipped": "Skipped",
38
+ "jobRunning": "Optimization in progress…",
39
+ "jobScanning": "Scanning for optimizable assets…",
40
+ "jobEmpty": "No images to optimize (already optimized or source files missing).",
41
+ "jobDone": "Job completed",
42
+ "jobFailed": "Job failed",
43
+ "processed": "Processed",
44
+ "succeeded": "Succeeded",
45
+ "failed": "Failed",
46
+ "savedBytes": "Space saved",
47
+ "inactiveHint": "Activate this plugin from the plugin list before running analysis or optimization.",
48
+ "analyzeFailed": "Analysis failed",
49
+ "runFailed": "Failed to start job"
50
+ }
51
+ }
@@ -0,0 +1,53 @@
1
+ {
2
+ "meta": {
3
+ "name": "图片资源优化",
4
+ "description": "分析历史图片素材并批量压缩为 WebP 多尺寸变体,可选回写文章与页面中的图片 URL。"
5
+ },
6
+ "settings": {
7
+ "batchSize": {
8
+ "title": "每批处理数量",
9
+ "description": "单次任务最多处理的图片数量(1–200)。"
10
+ },
11
+ "skipGif": {
12
+ "title": "跳过 GIF",
13
+ "description": "GIF 转 WebP 后会失去动画,默认跳过。"
14
+ },
15
+ "rewriteContent": {
16
+ "title": "优化后回写内容 URL",
17
+ "description": "批量优化完成后,自动替换文章/页面中引用的旧图片地址。"
18
+ },
19
+ "cleanupOriginals": {
20
+ "title": "优化后删除原文件",
21
+ "description": "确认无误后再开启;删除后无法通过旧 URL 访问原图。"
22
+ }
23
+ },
24
+ "dashboard": {
25
+ "title": "资源分析",
26
+ "analyze": "分析现状",
27
+ "dryRun": "试运行(不写入)",
28
+ "dryRunDone": "试运行完成(未写入数据库)",
29
+ "run": "开始批量优化",
30
+ "refresh": "刷新",
31
+ "total": "素材总数",
32
+ "optimized": "已优化",
33
+ "pending": "待优化",
34
+ "estimatedSaved": "预估可节省",
35
+ "contentRefs": "内容引用",
36
+ "articles": "文章",
37
+ "pages": "页面",
38
+ "skipped": "已跳过",
39
+ "jobRunning": "优化任务进行中…",
40
+ "jobScanning": "正在扫描待优化素材…",
41
+ "jobEmpty": "没有待优化的图片(可能已全部优化,或原文件已不存在)。",
42
+ "jobDone": "任务已完成",
43
+ "jobFailed": "任务失败",
44
+ "processed": "已处理",
45
+ "succeeded": "成功",
46
+ "failed": "失败",
47
+ "savedBytes": "已节省空间",
48
+ "inactiveHint": "请先在插件列表中启用本插件,才能执行分析与优化。",
49
+ "analyzeFailed": "分析失败",
50
+ "runFailed": "启动任务失败",
51
+ "jobPollFailed": "获取任务进度失败,请重试"
52
+ }
53
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@fecommunity/reactpress-plugin-image-optimizer",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "Legacy image optimization plugin for ReactPress",
6
+ "license": "MIT",
7
+ "main": "dist/index.js",
8
+ "types": "dist/index.d.ts",
9
+ "scripts": {
10
+ "build": "tsc",
11
+ "typecheck": "tsc --noEmit",
12
+ "clean": "rm -rf dist"
13
+ },
14
+ "dependencies": {
15
+ "@fecommunity/reactpress-toolkit": "workspace:*"
16
+ },
17
+ "devDependencies": {
18
+ "typescript": "~5.9.3"
19
+ }
20
+ }
@@ -0,0 +1,58 @@
1
+ {
2
+ "$schema": "../plugin.manifest.schema.json",
3
+ "id": "image-optimizer",
4
+ "name": "图片资源优化",
5
+ "version": "1.0.0",
6
+ "description": "分析历史图片素材并批量压缩为 WebP 多尺寸变体,可选回写文章与页面中的图片 URL。",
7
+ "author": "ReactPress",
8
+ "requires": ">=3.5.0",
9
+ "server": {
10
+ "module": "./dist/index.js"
11
+ },
12
+ "admin": {
13
+ "menu": {
14
+ "title": "图片优化",
15
+ "path": "/plugins/image-optimizer/settings",
16
+ "permission": "extension:manage",
17
+ "sort": 30
18
+ }
19
+ },
20
+ "settings": {
21
+ "schema": {
22
+ "type": "object",
23
+ "additionalProperties": false,
24
+ "properties": {
25
+ "batchSize": {
26
+ "type": "integer",
27
+ "title": "每批处理数量",
28
+ "description": "单次任务最多处理的图片数量(1–200)。",
29
+ "minimum": 1,
30
+ "maximum": 200,
31
+ "default": 50
32
+ },
33
+ "skipGif": {
34
+ "type": "boolean",
35
+ "title": "跳过 GIF",
36
+ "description": "GIF 转 WebP 后会失去动画,默认跳过。",
37
+ "default": true
38
+ },
39
+ "rewriteContent": {
40
+ "type": "boolean",
41
+ "title": "优化后回写内容 URL",
42
+ "description": "批量优化完成后,自动替换文章/页面中引用的旧图片地址。",
43
+ "default": false
44
+ },
45
+ "cleanupOriginals": {
46
+ "type": "boolean",
47
+ "title": "优化后删除原文件",
48
+ "description": "确认无误后再开启;删除后无法通过旧 URL 访问原图。",
49
+ "default": false
50
+ }
51
+ }
52
+ }
53
+ },
54
+ "permissions": ["file:read", "file:write", "article:read"],
55
+ "capabilities": {
56
+ "headless": true
57
+ }
58
+ }
@@ -0,0 +1,16 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2020",
4
+ "module": "Node16",
5
+ "moduleResolution": "Node16",
6
+ "rootDir": "./src",
7
+ "outDir": "./dist",
8
+ "strict": true,
9
+ "esModuleInterop": true,
10
+ "skipLibCheck": true,
11
+ "declaration": true,
12
+ "forceConsistentCasingInFileNames": true
13
+ },
14
+ "include": ["src/**/*"],
15
+ "exclude": ["node_modules", "dist"]
16
+ }
@@ -0,0 +1,14 @@
1
+ {
2
+ "name": "@fecommunity/reactpress-plugins",
3
+ "version": "1.0.0",
4
+ "private": true,
5
+ "description": "ReactPress plugin registry — local and npm plugin sources for the publishing platform",
6
+ "license": "MIT",
7
+ "scripts": {
8
+ "build": "pnpm --filter @fecommunity/reactpress-plugin-hello-world run build && pnpm --filter @fecommunity/reactpress-plugin-seo run build && pnpm --filter @fecommunity/reactpress-plugin-image-optimizer run build",
9
+ "typecheck": "pnpm --filter @fecommunity/reactpress-plugin-hello-world run typecheck && pnpm --filter @fecommunity/reactpress-plugin-seo run typecheck && pnpm --filter @fecommunity/reactpress-plugin-image-optimizer run typecheck"
10
+ },
11
+ "reactpress": {
12
+ "local": ["hello-world", "seo", "image-optimizer"]
13
+ }
14
+ }
@@ -0,0 +1,60 @@
1
+ # seo (SEO Enhancement)
2
+
3
+ Official plugin — **URL slug**, **SEO keywords**, and **meta description** with optional auto-fill and an Admin editor panel.
4
+
5
+ > WordPress-style SEO fields · Headless-friendly meta for Next.js themes
6
+
7
+ - **Plugin id**: `seo`
8
+ - **Version**: see [`plugin.json`](./plugin.json)
9
+
10
+ ## Features
11
+
12
+ | Field | Description |
13
+ | :--- | :--- |
14
+ | `slug` | URL alias, accessible at `/article/{slug}` |
15
+ | `seoKeywords` | meta keywords, comma-separated |
16
+ | `seoDescription` | meta description |
17
+
18
+ | Scenario | Behavior (when plugin enabled and option on) |
19
+ | :--- | :--- |
20
+ | Slug empty | Generate slug from title |
21
+ | SEO description empty | Prefer summary, else truncate body |
22
+ | SEO keywords empty | Use title + tag names |
23
+
24
+ ## Hooks
25
+
26
+ | Hook | Type | Description |
27
+ | :--- | :--- | :--- |
28
+ | `article.beforeCreate` | filter | Fill SEO fields on create |
29
+ | `article.beforePublish` | filter | Fill SEO fields on publish |
30
+
31
+ ## Configuration
32
+
33
+ | Field | Type | Default | Description |
34
+ | :--- | :--- | :--- | :--- |
35
+ | `enabled` | boolean | `true` | Master switch |
36
+ | `autoSlug` | boolean | `true` | Auto-generate slug |
37
+ | `autoDescription` | boolean | `true` | Auto-generate description |
38
+ | `autoKeywords` | boolean | `true` | Auto-generate keywords |
39
+ | `descriptionMaxLength` | number | `160` | Max description length |
40
+ | `descriptionSuffix` | string | `…` | Truncation suffix |
41
+
42
+ ## Usage
43
+
44
+ 1. `pnpm dev` (plugins are compiled automatically)
45
+ 2. Admin → **Plugins** → install **SEO Enhancement** → **Enable**
46
+ 3. Fill slug, keywords, and description in the **SEO Settings** panel (leave empty for auto-fill)
47
+ 4. After publish, check `<meta name="description">` and `<meta name="keywords">` on the theme article page
48
+
49
+ ## Admin UI
50
+
51
+ The **SEO Settings** panel in the article editor is injected via Admin slot `article.editor.meta.afterSummary` (see [`admin/`](./admin/)). The panel unmounts automatically when the plugin is deactivated.
52
+
53
+ ## Development
54
+
55
+ ```bash
56
+ pnpm --filter @fecommunity/reactpress-plugin-seo run build
57
+ pnpm --filter @fecommunity/reactpress-plugin-seo run typecheck
58
+ ```
59
+
60
+ See [`../README.md`](../README.md) for the plugin system overview.