@fecommunity/reactpress 4.0.0-beta.14 → 4.0.0-beta.17

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 (77) 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.js +4 -0
  21. package/out/core/services/local-site.js +68 -0
  22. package/out/lib/i18n/strings.js +2 -0
  23. package/out/lib/theme-api-proxy-patch.js +62 -17
  24. package/package.json +2 -1
  25. package/plugins/hello-world/README.md +104 -0
  26. package/plugins/hello-world/dist/config.d.ts +4 -0
  27. package/plugins/hello-world/dist/config.js +17 -0
  28. package/plugins/hello-world/dist/index.d.ts +10 -0
  29. package/plugins/hello-world/dist/index.js +15 -0
  30. package/plugins/hello-world/dist/summary.d.ts +3 -0
  31. package/plugins/hello-world/dist/summary.js +33 -0
  32. package/plugins/hello-world/dist/types.d.ts +16 -0
  33. package/plugins/hello-world/dist/types.js +2 -0
  34. package/plugins/hello-world/locales/en.json +24 -0
  35. package/plugins/hello-world/locales/zh.json +24 -0
  36. package/plugins/hello-world/package.json +20 -0
  37. package/plugins/hello-world/plugin.json +61 -0
  38. package/plugins/hello-world/tsconfig.json +16 -0
  39. package/plugins/image-optimizer/README.md +43 -0
  40. package/plugins/image-optimizer/admin/OptimizeDashboard.tsx +249 -0
  41. package/plugins/image-optimizer/admin/api.ts +133 -0
  42. package/plugins/image-optimizer/admin/index.ts +16 -0
  43. package/plugins/image-optimizer/admin/locale.ts +68 -0
  44. package/plugins/image-optimizer/admin/optimize-dashboard.module.css +80 -0
  45. package/plugins/image-optimizer/dist/config.d.ts +8 -0
  46. package/plugins/image-optimizer/dist/config.js +13 -0
  47. package/plugins/image-optimizer/dist/index.d.ts +10 -0
  48. package/plugins/image-optimizer/dist/index.js +13 -0
  49. package/plugins/image-optimizer/locales/en.json +51 -0
  50. package/plugins/image-optimizer/locales/zh.json +53 -0
  51. package/plugins/image-optimizer/package.json +20 -0
  52. package/plugins/image-optimizer/plugin.json +58 -0
  53. package/plugins/image-optimizer/tsconfig.json +16 -0
  54. package/plugins/package.json +14 -0
  55. package/plugins/seo/README.md +60 -0
  56. package/plugins/seo/admin/ArticleEditorSeoPanel.tsx +67 -0
  57. package/plugins/seo/admin/article-editor-seo-panel.module.css +74 -0
  58. package/plugins/seo/admin/index.ts +18 -0
  59. package/plugins/seo/dist/config.d.ts +4 -0
  60. package/plugins/seo/dist/config.js +19 -0
  61. package/plugins/seo/dist/index.d.ts +10 -0
  62. package/plugins/seo/dist/index.js +23 -0
  63. package/plugins/seo/dist/seo.d.ts +3 -0
  64. package/plugins/seo/dist/seo.js +79 -0
  65. package/plugins/seo/dist/slug.d.ts +2 -0
  66. package/plugins/seo/dist/slug.js +13 -0
  67. package/plugins/seo/dist/types.d.ts +23 -0
  68. package/plugins/seo/dist/types.js +2 -0
  69. package/plugins/seo/locales/en.json +32 -0
  70. package/plugins/seo/locales/zh.json +32 -0
  71. package/plugins/seo/package.json +20 -0
  72. package/plugins/seo/plugin.json +76 -0
  73. package/plugins/seo/tsconfig.json +16 -0
  74. package/server/package-lock.json +3 -3
  75. package/server/package.json +1 -1
  76. package/toolkit/dist/theme/build/next-config.js +4 -1
  77. package/toolkit/package.json +1 -1
@@ -4,55 +4,100 @@ Object.defineProperty(exports, "__esModule", { value: true });
4
4
  const fs = require('fs');
5
5
  const path = require('path');
6
6
  const PATCH_MARKER = '.reactpress-api-proxy-patched';
7
+ const ENCODING_PATCH_MARKER = 'reactpress-api-proxy-encoding';
7
8
  const ROUTE_REL = path.join('app', 'api', '[[...path]]', 'route.ts');
9
+ const PROXY_REL = path.join('lib', 'mock-api', 'proxy.ts');
8
10
  /** Buggy pattern: body read before mock/proxy branch (breaks upstream proxy). */
9
- const BUGGY_BLOCK = ` const keyword = new URL(request.url).searchParams.get('keyword')?.trim() ?? ''
11
+ const BUGGY_ROUTE_BLOCK = ` const keyword = new URL(request.url).searchParams.get('keyword')?.trim() ?? ''
10
12
  const body = await readJsonBody(request)
11
13
 
12
14
  let response: Response
13
15
  if (isMockApiEnabled()) {
14
16
  const mockResponse = matchMockRoute(request.method, path, { keyword, body })`;
15
- const FIXED_BLOCK = ` const keyword = new URL(request.url).searchParams.get('keyword')?.trim() ?? ''
17
+ const FIXED_ROUTE_BLOCK = ` const keyword = new URL(request.url).searchParams.get('keyword')?.trim() ?? ''
16
18
 
17
19
  let response: Response
18
20
  if (isMockApiEnabled()) {
19
21
  const body = await readJsonBody(request)
20
22
  const mockResponse = matchMockRoute(request.method, path, { keyword, body })`;
23
+ const BUGGY_PROXY_BLOCK = ` return new Response(upstream.body, {
24
+ status: upstream.status,
25
+ headers: upstream.headers,
26
+ })`;
27
+ const FIXED_PROXY_BLOCK = ` const responseHeaders = new Headers(upstream.headers)
28
+ // ${ENCODING_PATCH_MARKER}: Node fetch decompresses bodies; strip stale encoding headers.
29
+ responseHeaders.delete('content-encoding')
30
+ responseHeaders.delete('content-length')
31
+ responseHeaders.delete('transfer-encoding')
32
+
33
+ return new Response(upstream.body, {
34
+ status: upstream.status,
35
+ headers: responseHeaders,
36
+ })`;
21
37
  function resolveApiRoutePath(themeDir) {
22
38
  if (!themeDir)
23
39
  return null;
24
40
  const routePath = path.join(themeDir, ROUTE_REL);
25
41
  return fs.existsSync(routePath) ? routePath : null;
26
42
  }
27
- /**
28
- * App Router themes proxy /api via app/api/[[...path]]/route.ts.
29
- * Older theme-starter builds read the request body before the mock branch,
30
- * which leaves nothing for proxyToUpstreamApi() in production.
31
- */
32
- function patchThemeApiProxyRoute(themeDir) {
43
+ function resolveApiProxyPath(themeDir) {
44
+ if (!themeDir)
45
+ return null;
46
+ const proxyPath = path.join(themeDir, PROXY_REL);
47
+ return fs.existsSync(proxyPath) ? proxyPath : null;
48
+ }
49
+ function touchPatchMarker(themeDir) {
50
+ fs.writeFileSync(path.join(themeDir, PATCH_MARKER), `${new Date().toISOString()}\n`, 'utf8');
51
+ }
52
+ function patchThemeApiRouteBodyRead(themeDir) {
33
53
  const routePath = resolveApiRoutePath(themeDir);
34
54
  if (!routePath)
35
55
  return false;
36
- const markerPath = path.join(themeDir, PATCH_MARKER);
37
56
  let src = fs.readFileSync(routePath, 'utf8');
38
- if (src.includes(FIXED_BLOCK)) {
39
- if (!fs.existsSync(markerPath)) {
40
- fs.writeFileSync(markerPath, `${new Date().toISOString()}\n`, 'utf8');
41
- }
57
+ if (src.includes(FIXED_ROUTE_BLOCK) || !src.includes(BUGGY_ROUTE_BLOCK)) {
42
58
  return false;
43
59
  }
44
- if (!src.includes(BUGGY_BLOCK)) {
60
+ src = src.replace(BUGGY_ROUTE_BLOCK, FIXED_ROUTE_BLOCK);
61
+ fs.writeFileSync(routePath, src, 'utf8');
62
+ return true;
63
+ }
64
+ /**
65
+ * Node fetch() auto-decompresses upstream bodies but leaves Content-Encoding on the Response.
66
+ * Browsers then fail with ERR_CONTENT_DECODING_FAILED (HTTP 200).
67
+ */
68
+ function patchThemeApiProxyEncodingHeaders(themeDir) {
69
+ const proxyPath = resolveApiProxyPath(themeDir);
70
+ if (!proxyPath)
71
+ return false;
72
+ let src = fs.readFileSync(proxyPath, 'utf8');
73
+ if (src.includes(ENCODING_PATCH_MARKER) || !src.includes(BUGGY_PROXY_BLOCK)) {
45
74
  return false;
46
75
  }
47
- src = src.replace(BUGGY_BLOCK, FIXED_BLOCK);
48
- fs.writeFileSync(routePath, src, 'utf8');
49
- fs.writeFileSync(markerPath, `${new Date().toISOString()}\n`, 'utf8');
76
+ src = src.replace(BUGGY_PROXY_BLOCK, FIXED_PROXY_BLOCK);
77
+ fs.writeFileSync(proxyPath, src, 'utf8');
50
78
  return true;
51
79
  }
80
+ /**
81
+ * App Router themes proxy /api via app/api/[[...path]]/route.ts.
82
+ * Patches known theme-starter issues before production build.
83
+ */
84
+ function patchThemeApiProxyRoute(themeDir) {
85
+ const routePatched = patchThemeApiRouteBodyRead(themeDir);
86
+ const proxyPatched = patchThemeApiProxyEncodingHeaders(themeDir);
87
+ if (routePatched || proxyPatched) {
88
+ touchPatchMarker(themeDir);
89
+ }
90
+ return routePatched || proxyPatched;
91
+ }
52
92
  module.exports = {
53
93
  PATCH_MARKER,
94
+ ENCODING_PATCH_MARKER,
54
95
  ROUTE_REL,
96
+ PROXY_REL,
55
97
  patchThemeApiProxyRoute,
98
+ patchThemeApiRouteBodyRead,
99
+ patchThemeApiProxyEncodingHeaders,
56
100
  resolveApiRoutePath,
101
+ resolveApiProxyPath,
57
102
  };
58
103
  //# sourceMappingURL=theme-api-proxy-patch.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fecommunity/reactpress",
3
- "version": "4.0.0-beta.14",
3
+ "version": "4.0.0-beta.17",
4
4
  "description": "ReactPress CLI — publishing platform for modern developers. CMS, Admin, API, and Next.js themes in one command. Self-hosted, MIT. `npx @fecommunity/reactpress init`",
5
5
  "author": "fecommunity",
6
6
  "license": "MIT",
@@ -45,6 +45,7 @@
45
45
  "server",
46
46
  "toolkit",
47
47
  "admin-dist",
48
+ "plugins",
48
49
  "templates",
49
50
  "scripts/install-bundled-runtime.mjs",
50
51
  "README.md",
@@ -0,0 +1,104 @@
1
+ # hello-world (Auto Summary)
2
+
3
+ Official example plugin — **auto-fill article summary on publish** when empty. Template for custom ReactPress plugins.
4
+
5
+ > Part of the ReactPress publishing platform · [plugin system docs](../README.md)
6
+
7
+ - **Plugin id**: `hello-world` (historical id, unchanged)
8
+ - **Display name**: Auto Summary
9
+ - **Version**: see [`plugin.json`](./plugin.json)
10
+
11
+ ## Features
12
+
13
+ | Scenario | Behavior |
14
+ | :--- | :--- |
15
+ | Summary already set on publish | No change |
16
+ | Summary empty, body has content | Strip Markdown, truncate, write `summary` |
17
+ | Summary and body both empty | Use title if `fallbackToTitle` is enabled |
18
+
19
+ ## Hooks
20
+
21
+ Subscribes to a single hook with a focused responsibility:
22
+
23
+ | Hook | Type | Description |
24
+ | :--- | :--- | :--- |
25
+ | `article.beforePublish` | filter | Fill in `summary` before publish |
26
+
27
+ ## Configuration
28
+
29
+ Declared via `plugin.json` → `settings.schema`, stored in `globalSetting.plugins.entries.hello-world.config`.
30
+
31
+ Admin UI strings (plugin name, descriptions, field labels) live in [`locales/`](./locales/) (`locales/{locale}.json`), following the same convention as themes; Chinese strings in `plugin.json` serve as fallback.
32
+
33
+ | Field | Type | Default | Description |
34
+ | :--- | :--- | :--- | :--- |
35
+ | `enabled` | boolean | `true` | Enable auto summary |
36
+ | `maxLength` | number | `160` | Max length (40–500) |
37
+ | `suffix` | string | `…` | Suffix when truncated |
38
+ | `fallbackToTitle` | boolean | `true` | Use title when body is empty |
39
+
40
+ Update config (admin login required):
41
+
42
+ ```bash
43
+ curl -X PUT http://localhost:3002/api/extension/plugins/hello-world/config \
44
+ -H "Authorization: Bearer <token>" \
45
+ -H "Content-Type: application/json" \
46
+ -d '{"config":{"enabled":true,"maxLength":120}}'
47
+ ```
48
+
49
+ Config save triggers an automatic plugin reload — no server restart needed.
50
+
51
+ ## Usage
52
+
53
+ 1. `pnpm dev` (plugins are compiled automatically)
54
+ 2. Admin → **Plugins** → install **hello-world** → **Enable**
55
+ 3. Click **Settings** on the plugin row (or visit `/admin/plugins/hello-world/settings`)
56
+ 4. Leave summary empty when writing → publish → summary field should be auto-filled
57
+
58
+ Example server logs:
59
+
60
+ ```text
61
+ [PluginService] [hello-world] registered (enabled=true, maxLength=160)
62
+ [PluginLoaderService] Loaded plugin "hello-world" v1.2.0
63
+ ```
64
+
65
+ ## Directory structure
66
+
67
+ ```
68
+ hello-world/
69
+ ├── plugin.json
70
+ ├── package.json
71
+ ├── tsconfig.json
72
+ ├── README.md
73
+ └── src/
74
+ ├── index.ts # register() entry
75
+ ├── config.ts # defaults and merge
76
+ ├── summary.ts # Markdown strip + truncate
77
+ └── types.ts
78
+ ↓ tsc
79
+ dist/
80
+ └── index.js # runtime loads this file
81
+ ```
82
+
83
+ ## Development
84
+
85
+ ```bash
86
+ # from monorepo root
87
+ pnpm --filter @reactpress/plugin-hello-world run build
88
+ pnpm --filter @reactpress/plugin-hello-world run typecheck
89
+
90
+ # after editing src, rebuild and deactivate → reactivate in admin
91
+ ```
92
+
93
+ Dependencies:
94
+
95
+ - `@fecommunity/reactpress-toolkit/plugin/server` — `HookService`, `PluginContext` types
96
+
97
+ ## Use as a new plugin template
98
+
99
+ 1. Copy this directory to `plugins/my-plugin/`
100
+ 2. Update `plugin.json` `id`, `name`, `hooks.subscribe`
101
+ 3. Rewrite `src/index.ts` and business modules
102
+ 4. Add the id to [`plugins/package.json`](../package.json) → `local` list
103
+
104
+ See [`../README.md`](../README.md) for the plugin system overview.
@@ -0,0 +1,4 @@
1
+ import type { PluginContext } from '@fecommunity/reactpress-toolkit/plugin/server';
2
+ import type { SummaryPluginConfig } from './types';
3
+ export declare const DEFAULT_CONFIG: SummaryPluginConfig;
4
+ export declare function resolveConfig(ctx: PluginContext): SummaryPluginConfig;
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.DEFAULT_CONFIG = void 0;
4
+ exports.resolveConfig = resolveConfig;
5
+ exports.DEFAULT_CONFIG = {
6
+ enabled: true,
7
+ maxLength: 160,
8
+ suffix: '…',
9
+ fallbackToTitle: true,
10
+ };
11
+ function resolveConfig(ctx) {
12
+ const patch = ctx.config;
13
+ return {
14
+ ...exports.DEFAULT_CONFIG,
15
+ ...patch,
16
+ };
17
+ }
@@ -0,0 +1,10 @@
1
+ import type { HookService, PluginContext } from '@fecommunity/reactpress-toolkit/plugin/server';
2
+ /**
3
+ * hello-world:自动摘要插件。
4
+ * 发布前若 summary 为空,从 Markdown 正文(或标题)生成摘要。
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,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.register = register;
4
+ const config_1 = require("./config");
5
+ const summary_1 = require("./summary");
6
+ /**
7
+ * hello-world:自动摘要插件。
8
+ * 发布前若 summary 为空,从 Markdown 正文(或标题)生成摘要。
9
+ */
10
+ function register(hooks, ctx) {
11
+ const config = () => (0, config_1.resolveConfig)(ctx);
12
+ hooks.addFilter('article.beforePublish', async (article) => (0, summary_1.applyAutoSummary)(article, config()), { priority: 10, pluginId: ctx.id });
13
+ ctx.logger.log(`registered (enabled=${config().enabled}, maxLength=${config().maxLength})`);
14
+ }
15
+ exports.default = { register };
@@ -0,0 +1,3 @@
1
+ import type { ArticleDraft, SummaryPluginConfig } from './types';
2
+ /** 摘要为空时从正文(或标题)生成 summary。 */
3
+ export declare function applyAutoSummary(article: ArticleDraft, config: SummaryPluginConfig): ArticleDraft;
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.applyAutoSummary = applyAutoSummary;
4
+ function stripMarkdown(text) {
5
+ return String(text || '')
6
+ .replace(/```[\s\S]*?```/g, ' ')
7
+ .replace(/`[^`]+`/g, ' ')
8
+ .replace(/!\[[^\]]*]\([^)]+\)/g, ' ')
9
+ .replace(/\[([^\]]*)]\([^)]+\)/g, '$1')
10
+ .replace(/^#{1,6}\s+/gm, '')
11
+ .replace(/^>\s?/gm, '')
12
+ .replace(/[*_~`]/g, '')
13
+ .replace(/\s+/g, ' ')
14
+ .trim();
15
+ }
16
+ /** 摘要为空时从正文(或标题)生成 summary。 */
17
+ function applyAutoSummary(article, config) {
18
+ if (!config.enabled)
19
+ return article;
20
+ const existing = String(article.summary ?? '').trim();
21
+ if (existing)
22
+ return article;
23
+ const content = stripMarkdown(String(article.content ?? article.html ?? ''));
24
+ const title = String(article.title ?? '').trim();
25
+ const source = content || (config.fallbackToTitle ? title : '');
26
+ if (!source)
27
+ return article;
28
+ const max = Math.max(40, config.maxLength);
29
+ let summary = source.slice(0, max);
30
+ if (source.length > max)
31
+ summary += config.suffix;
32
+ return { ...article, summary };
33
+ }
@@ -0,0 +1,16 @@
1
+ /** 插件 Hook 使用的文章字段子集。 */
2
+ export interface ArticleDraft {
3
+ title?: string;
4
+ summary?: string | null;
5
+ content?: string | null;
6
+ html?: string | null;
7
+ [key: string]: unknown;
8
+ }
9
+ export interface SummaryPluginConfig {
10
+ /** 发布时摘要为空则自动生成 */
11
+ enabled: boolean;
12
+ maxLength: number;
13
+ suffix: string;
14
+ /** 正文为空时是否回退到标题 */
15
+ fallbackToTitle: boolean;
16
+ }
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1,24 @@
1
+ {
2
+ "meta": {
3
+ "name": "Auto Summary",
4
+ "description": "When publishing, if the summary is empty, generate one from the body or title."
5
+ },
6
+ "settings": {
7
+ "enabled": {
8
+ "title": "Enable auto summary",
9
+ "description": "When disabled, summary is not auto-filled on publish."
10
+ },
11
+ "maxLength": {
12
+ "title": "Maximum length",
13
+ "description": "Max characters taken from the body (40–500)."
14
+ },
15
+ "suffix": {
16
+ "title": "Truncation suffix",
17
+ "description": "Appended when content exceeds max length, e.g. …"
18
+ },
19
+ "fallbackToTitle": {
20
+ "title": "Use title when body is empty",
21
+ "description": "When stripped Markdown body is empty, use the article title as summary."
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,24 @@
1
+ {
2
+ "meta": {
3
+ "name": "自动摘要",
4
+ "description": "发布文章时,若摘要为空,从正文或标题自动生成 summary。"
5
+ },
6
+ "settings": {
7
+ "enabled": {
8
+ "title": "启用自动摘要",
9
+ "description": "关闭后,发布时不再自动写入 summary。"
10
+ },
11
+ "maxLength": {
12
+ "title": "摘要最大长度",
13
+ "description": "从正文截取的最大字符数(40–500)。"
14
+ },
15
+ "suffix": {
16
+ "title": "截断后缀",
17
+ "description": "超出最大长度时追加在末尾,例如 …"
18
+ },
19
+ "fallbackToTitle": {
20
+ "title": "正文为空时使用标题",
21
+ "description": "Markdown 正文去格式后为空时,用文章标题作为摘要。"
22
+ }
23
+ }
24
+ }
@@ -0,0 +1,20 @@
1
+ {
2
+ "name": "@fecommunity/reactpress-plugin-hello-world",
3
+ "version": "1.2.0",
4
+ "private": true,
5
+ "description": "Auto summary 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,61 @@
1
+ {
2
+ "$schema": "../plugin.manifest.schema.json",
3
+ "id": "hello-world",
4
+ "name": "自动摘要",
5
+ "version": "1.2.0",
6
+ "description": "发布文章时,若摘要为空,从正文或标题自动生成 summary。",
7
+ "author": "ReactPress",
8
+ "requires": ">=3.5.0",
9
+ "server": {
10
+ "module": "./dist/index.js",
11
+ "hooks": {
12
+ "subscribe": ["article.beforePublish"]
13
+ }
14
+ },
15
+ "admin": {
16
+ "menu": {
17
+ "title": "自动摘要",
18
+ "path": "/plugins/hello-world/settings",
19
+ "permission": "extension:manage",
20
+ "sort": 10
21
+ }
22
+ },
23
+ "settings": {
24
+ "schema": {
25
+ "type": "object",
26
+ "additionalProperties": false,
27
+ "properties": {
28
+ "enabled": {
29
+ "type": "boolean",
30
+ "title": "启用自动摘要",
31
+ "description": "关闭后,发布时不再自动写入 summary。",
32
+ "default": true
33
+ },
34
+ "maxLength": {
35
+ "type": "integer",
36
+ "title": "摘要最大长度",
37
+ "description": "从正文截取的最大字符数(40–500)。",
38
+ "minimum": 40,
39
+ "maximum": 500,
40
+ "default": 160
41
+ },
42
+ "suffix": {
43
+ "type": "string",
44
+ "title": "截断后缀",
45
+ "description": "超出最大长度时追加在末尾,例如 …",
46
+ "default": "…"
47
+ },
48
+ "fallbackToTitle": {
49
+ "type": "boolean",
50
+ "title": "正文为空时使用标题",
51
+ "description": "Markdown 正文去格式后为空时,用文章标题作为摘要。",
52
+ "default": true
53
+ }
54
+ }
55
+ }
56
+ },
57
+ "permissions": ["article:read"],
58
+ "capabilities": {
59
+ "headless": true
60
+ }
61
+ }
@@ -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,43 @@
1
+ # image-optimizer (Image Asset Optimization)
2
+
3
+ Official plugin — **analyze legacy uploads and batch-compress to WebP multi-size variants**, with optional content URL rewrite.
4
+
5
+ > Improve Lighthouse and page weight without leaving Admin
6
+
7
+ - **Plugin id**: `image-optimizer`
8
+ - **Version**: see [`plugin.json`](./plugin.json)
9
+
10
+ ## Features
11
+
12
+ | Capability | Description |
13
+ | :--- | :--- |
14
+ | Batch optimization | Process historical uploads via Core `FileOptimizationService` |
15
+ | WebP variants | Generate compressed multi-size WebP outputs |
16
+ | URL rewrite | Optionally replace old image URLs in article/page content after batch run |
17
+ | Admin dashboard | Full UI at **Plugins → Image Optimization** |
18
+
19
+ Batch compression is executed by the core file optimization service; this plugin registers configuration and exposes the admin dashboard (`admin/OptimizeDashboard`).
20
+
21
+ ## Configuration
22
+
23
+ | Field | Type | Default | Description |
24
+ | :--- | :--- | :--- | :--- |
25
+ | `batchSize` | integer | `50` | Max images per batch (1–200) |
26
+ | `skipGif` | boolean | `true` | Skip GIF (WebP conversion loses animation) |
27
+ | `rewriteContent` | boolean | `false` | Rewrite article/page image URLs after optimization |
28
+ | `cleanupOriginals` | boolean | `false` | Delete original files after optimization (irreversible) |
29
+
30
+ ## Usage
31
+
32
+ 1. `pnpm dev` (plugins are compiled automatically)
33
+ 2. Admin → **Plugins** → install **Image Optimization** → **Enable**
34
+ 3. Open **Plugins → Image Optimization** to run batch jobs and review results
35
+
36
+ ## Development
37
+
38
+ ```bash
39
+ pnpm --filter @fecommunity/reactpress-plugin-image-optimizer run build
40
+ pnpm --filter @fecommunity/reactpress-plugin-image-optimizer run typecheck
41
+ ```
42
+
43
+ See [`../README.md`](../README.md) for the plugin system overview.