radix-cms 0.1.0 → 1.0.0

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 (59) hide show
  1. package/.env.example +17 -0
  2. package/CHANGELOG.md +10 -0
  3. package/CONTRIBUTING.md +40 -0
  4. package/LICENSE +197 -0
  5. package/NOTICE +3 -0
  6. package/README.md +126 -56
  7. package/SECURITY.md +22 -0
  8. package/dist/app.js +80 -0
  9. package/dist/config/db.js +66 -0
  10. package/dist/init/dbInit.js +132 -0
  11. package/dist/init/dbInit2.js +234 -0
  12. package/dist/middleware/auth.js +48 -0
  13. package/dist/plugins/my-seo-plugin/index.js +29 -0
  14. package/dist/routes/admin.js +808 -0
  15. package/dist/routes/index.js +218 -0
  16. package/dist/sql/radix.sql +118 -0
  17. package/dist/types/plugin.js +10 -0
  18. package/dist/utils/PluginManager.js +112 -0
  19. package/dist/utils/settings.js +80 -0
  20. package/dist/utils/themeScanner.js +33 -0
  21. package/dist/utils/versionCheck.js +74 -0
  22. package/docs/CMS_CODE_GUIDE.md +372 -0
  23. package/docs/CMS_USER_GUIDE.md +344 -0
  24. package/docs/Publishing Guidelines.txt +14 -0
  25. package/package.json +62 -6
  26. package/public/css/admin.css +178 -0
  27. package/public/css/fallback.css +53 -0
  28. package/public/hello.html +1 -0
  29. package/views/admin/create-page.ejs +82 -0
  30. package/views/admin/dashboard.ejs +44 -0
  31. package/views/admin/edit-page.ejs +87 -0
  32. package/views/admin/edit-user.ejs +45 -0
  33. package/views/admin/login.ejs +39 -0
  34. package/views/admin/media.ejs +79 -0
  35. package/views/admin/pages.ejs +82 -0
  36. package/views/admin/partials/footer.ejs +4 -0
  37. package/views/admin/partials/header.ejs +57 -0
  38. package/views/admin/settings.ejs +91 -0
  39. package/views/admin/setup-admin.ejs +69 -0
  40. package/views/admin/setup-db.ejs +112 -0
  41. package/views/admin/themes.ejs +24 -0
  42. package/views/admin/updates.ejs +79 -0
  43. package/views/admin/users.ejs +107 -0
  44. package/views/defaults/404.ejs +20 -0
  45. package/views/defaults/500.ejs +20 -0
  46. package/views/layouts/admin-layout.ejs +18 -0
  47. package/views/pages/gallery.ejs +5 -0
  48. package/views/pages/hello.ejs +4 -0
  49. package/views/pages/services.ejs +3 -0
  50. package/views/themes/default/assets/css/style.css +199 -0
  51. package/views/themes/default/assets/js/main.js +8 -0
  52. package/views/themes/default/index.ejs +55 -0
  53. package/views/themes/landing/assets/css/style.css +6 -0
  54. package/views/themes/landing/assets/js/main.js +8 -0
  55. package/views/themes/landing/index.ejs +26 -0
  56. package/views/themes/test/default.ejs +0 -0
  57. package/views/themes/test/full-width.ejs +0 -0
  58. package/views/themes/test/landing-page.ejs +0 -0
  59. package/index.js +0 -15
@@ -0,0 +1,132 @@
1
+ "use strict";
2
+ /**************************************************************************************************
3
+ * RadixCMS
4
+ *
5
+ * DESCRIPTION: legacy database initialization helpers.
6
+ *
7
+ * Copyright (C) 2026 Ultra Spark Software <salve@ultraspark.net>
8
+ * SPDX-License-Identifier: GPL-3.0-or-later
9
+ **************************************************************************************************/
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.installDatabase = installDatabase;
15
+ exports.initDatabase = initDatabase;
16
+ const promise_1 = __importDefault(require("mysql2/promise"));
17
+ const fs_1 = __importDefault(require("fs"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const dotenv_1 = __importDefault(require("dotenv"));
20
+ // Load default env (installer may re-load a different .env later)
21
+ dotenv_1.default.config();
22
+ const schemaVersion = '1.0.0';
23
+ function stripSeedData(sql) {
24
+ return sql
25
+ .replace(/\/\*Data for the table.*?\*\/\s*/gis, '')
26
+ .replace(/insert\s+into\s+`?[\w-]+`?\s*\([^;]*?\)\s*values\s*[\s\S]*?;/gi, '');
27
+ }
28
+ async function ensureSchemaMarker(conn, dbName) {
29
+ const escapedDbName = dbName.replace(/`/g, '``');
30
+ await conn.query(`USE \`${escapedDbName}\``);
31
+ await conn.query(`
32
  CREATE TABLE IF NOT EXISTS schema_meta (
1
33
  key_name VARCHAR(100) NOT NULL,
2
34
  value_text VARCHAR(255) NOT NULL,
3
35
  updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
4
36
  PRIMARY KEY (key_name)
5
37
  ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
6
38
  `);
39
+ await conn.query(`INSERT INTO schema_meta (key_name, value_text)
7
40
  VALUES ('db_initialized', '1'), ('schema_version', ?)
8
41
  ON DUPLICATE KEY UPDATE value_text = VALUES(value_text), updated_at = CURRENT_TIMESTAMP`, [schemaVersion]);
42
+ }
43
+ /**
44
+ * Install database using explicit connection settings and a database name.
45
+ * If schemaSql is not provided the bundled SQL file will be used.
46
+ */
47
+ async function installDatabase(connectionConfig, dbName, schemaSql) {
48
+ const sqlPath = path_1.default.join(__dirname, '../sql/radix.sql');
49
+ if (!schemaSql) {
50
+ if (!fs_1.default.existsSync(sqlPath)) {
51
+ throw new Error(`SQL file not found at ${sqlPath}`);
52
+ }
53
+ const rawSql = fs_1.default.readFileSync(sqlPath, 'utf8');
54
+ schemaSql = stripSeedData(rawSql);
55
+ }
56
+ connectionConfig = {
57
+ ...connectionConfig,
58
+ multipleStatements: true,
59
+ connectTimeout: connectionConfig.connectTimeout || 5000
60
+ };
61
+ let conn;
62
+ try {
63
+ conn = await promise_1.default.createConnection(connectionConfig);
64
+ const [rows] = await conn.query('SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?', [dbName]);
65
+ if (rows.length > 0) {
66
+ const dbConn = await promise_1.default.createConnection({ ...connectionConfig, database: dbName });
67
+ try {
68
+ await ensureSchemaMarker(dbConn, dbName);
69
+ console.log(`Database "${dbName}" already exists. Schema marker ensured.`);
70
+ }
71
+ finally {
72
+ await dbConn.end();
73
+ }
74
+ return;
75
+ }
76
+ await conn.query(schemaSql);
77
+ await ensureSchemaMarker(conn, dbName);
78
+ console.log(`Created database "${dbName}" and applied schema.`);
79
+ }
80
+ finally {
81
+ if (conn) {
82
+ try {
83
+ await conn.end();
84
+ }
85
+ catch (e) { /* ignore */ }
86
+ }
87
+ }
88
+ }
89
+ /**
90
+ * Initialize DB using environment variables. Reads process.env at call time so callers
91
+ * that re-load dotenv (e.g. installer) will be honored.
92
+ */
93
+ async function initDatabase() {
94
+ const sqlPath = path_1.default.join(__dirname, '../sql/radix.sql');
95
+ if (!fs_1.default.existsSync(sqlPath)) {
96
+ console.warn(`SQL file not found at ${sqlPath}, skipping DB initialization.`);
97
+ return;
98
+ }
99
+ const rawSql = fs_1.default.readFileSync(sqlPath, 'utf8');
100
+ const schemaSql = stripSeedData(rawSql);
101
+ const databaseName = process.env.DB_NAME || 'radix';
102
+ const connectionConfig = {
103
+ host: process.env.DB_HOST || '127.0.0.1',
104
+ user: process.env.DB_USER,
105
+ password: process.env.DB_PASSWORD,
106
+ port: Number(process.env.DB_PORT) || 3306,
107
+ multipleStatements: true,
108
+ connectTimeout: 5000
109
+ };
110
+ let conn;
111
+ try {
112
+ conn = await promise_1.default.createConnection(connectionConfig);
113
+ const [rows] = await conn.query('SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?', [databaseName]);
114
+ if (rows.length > 0) {
115
+ const dbConn = await promise_1.default.createConnection({ ...connectionConfig, database: databaseName });
116
+ try {
117
+ await ensureSchemaMarker(dbConn, databaseName);
118
+ console.log(`Database "${databaseName}" already exists. Schema marker is ready.`);
119
+ }
120
+ finally {
121
+ await dbConn.end();
122
+ }
123
+ return;
124
+ }
125
+ console.log(`Database "${databaseName}" not found. Creating empty schema from ${sqlPath}`);
126
+ await conn.query(schemaSql);
127
+ await ensureSchemaMarker(conn, databaseName);
128
+ console.log('Database initialization finished.');
129
+ }
130
+ catch (err) {
131
+ console.error('Failed to initialize database:', err);
132
+ }
133
+ finally {
134
+ if (conn) {
135
+ try {
136
+ await conn.end();
137
+ }
138
+ catch (e) { /* ignore */ }
139
+ }
140
+ }
141
+ }
@@ -0,0 +1,234 @@
1
+ "use strict";
2
+ /**************************************************************************************************
3
+ * RadixCMS
4
+ *
5
+ * DESCRIPTION: database schema installation and initialization.
6
+ *
7
+ * Copyright (C) 2026 Ultra Spark Software <salve@ultraspark.net>
8
+ * SPDX-License-Identifier: GPL-3.0-or-later
9
+ **************************************************************************************************/
10
+ var __importDefault = (this && this.__importDefault) || function (mod) {
11
+ return (mod && mod.__esModule) ? mod : { "default": mod };
12
+ };
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ exports.installDatabase = installDatabase;
15
+ exports.initDatabase = initDatabase;
16
+ const promise_1 = __importDefault(require("mysql2/promise"));
17
+ const fs_1 = __importDefault(require("fs"));
18
+ const path_1 = __importDefault(require("path"));
19
+ const dotenv_1 = __importDefault(require("dotenv"));
20
+ // Load default env (installer may re-load a different .env later)
21
+ dotenv_1.default.config();
22
+ const schemaVersion = '1.0.0';
23
+ function stripSeedData(sql) {
24
+ return sql
25
+ .replace(/\/\*Data for the table.*?\*\/\s*/gis, '')
26
+ .replace(/insert\s+into\s+`?[\w-]+`?\s*\([^;]*?\)\s*values\s*[\s\S]*?;/gi, '');
27
+ }
28
+ function normalizeSchemaSql(sql, databaseName) {
29
+ const safeDbName = databaseName.replace(/`/g, '``');
30
+ return sql
31
+ .replace(/CREATE\s+DATABASE\s+(?:\/\*\!32312\s+IF NOT EXISTS\*\/)?`?[A-Za-z0-9_]+`?/gi, `CREATE DATABASE IF NOT EXISTS \`${safeDbName}\``)
32
+ .replace(/USE\s+`?[A-Za-z0-9_]+`?/gi, `USE \`${safeDbName}\``)
33
+ .replace(/`radix`/gi, `\`${safeDbName}\``);
34
+ }
35
+ async function ensureSchemaMarker(conn, dbName) {
36
+ const escapedDbName = dbName.replace(/`/g, '``');
37
+ await conn.query(`USE \`${escapedDbName}\``);
38
+ await conn.query(`
39
+ CREATE TABLE IF NOT EXISTS schema_meta (
40
+ key_name VARCHAR(100) NOT NULL,
41
+ value_text VARCHAR(255) NOT NULL,
42
+ updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
43
+ PRIMARY KEY (key_name)
44
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
45
+ `);
46
+ await conn.query(`INSERT INTO schema_meta (key_name, value_text)
47
+ VALUES ('db_initialized', '1'), ('schema_version', ?)
48
+ ON DUPLICATE KEY UPDATE value_text = VALUES(value_text), updated_at = CURRENT_TIMESTAMP`, [schemaVersion]);
49
+ }
50
+ function extractCreateTableStatements(sql) {
51
+ const out = [];
52
+ const re = /CREATE\s+TABLE\s+`?([A-Za-z0-9_]+)`?\s*\([\s\S]*?\)[\s\S]*?;/gi;
53
+ let m;
54
+ while ((m = re.exec(sql)) !== null) {
55
+ out.push({ name: m[1], statement: m[0] });
56
+ }
57
+ return out;
58
+ }
59
+ async function seedDefaultContent(dbConn, dbName) {
60
+ const pageRows = [
61
+ {
62
+ title: 'Home',
63
+ slug: 'home',
64
+ content: '<h1>Hello World!</h1><p>Welcome to my lightweight Node.js CMS.</p>',
65
+ template_name: 'default',
66
+ status: 'Published',
67
+ min_role: 'Unregistered',
68
+ publish_at: '2026-08-31 00:00:00'
69
+ },
70
+ {
71
+ title: 'About',
72
+ slug: 'about',
73
+ content: '<h1>About Our System</h1><p>This page is completely dynamic.</p>',
74
+ template_name: 'default',
75
+ status: 'Published',
76
+ min_role: 'Unregistered',
77
+ publish_at: '2026-08-31 00:00:00'
78
+ }
79
+ ];
80
+ for (const page of pageRows) {
81
+ await dbConn.execute(`INSERT INTO pages (title, slug, content, template_name, status, min_role, publish_at)
82
+ VALUES (?, ?, ?, ?, ?, ?, ?)
83
+ ON DUPLICATE KEY UPDATE
84
+ title = VALUES(title),
85
+ content = VALUES(content),
86
+ template_name = VALUES(template_name),
87
+ status = VALUES(status),
88
+ min_role = VALUES(min_role),
89
+ publish_at = VALUES(publish_at)`, [page.title, page.slug, page.content, page.template_name, page.status, page.min_role, page.publish_at]);
90
+ }
91
+ const defaultSettings = [
92
+ ['app_name', process.env.APP_NAME || 'Radix'],
93
+ ['app_version', process.env.APP_VERSION || '0.11.0'],
94
+ ['site_title', process.env.APP_NAME || 'Radix'],
95
+ ['search_engine_visibility', '1'],
96
+ ['mail_from', 'noreply@example.com'],
97
+ ['mail_host', process.env.MAIL_HOST || ''],
98
+ ['mail_port', process.env.MAIL_PORT || '25'],
99
+ ['mail_secure', process.env.MAIL_SECURE || '0'],
100
+ ['mail_user', process.env.MAIL_USER || ''],
101
+ ['mail_pass', process.env.MAIL_PASS || ''],
102
+ ['site_logo', '']
103
+ ];
104
+ for (const [key, value] of defaultSettings) {
105
+ await dbConn.execute('INSERT INTO settings (setting_key, setting_value) VALUES (?, ?) ON DUPLICATE KEY UPDATE setting_value = VALUES(setting_value)', [key, value]);
106
+ }
107
+ console.log(`Seeded default content for database "${dbName}".`);
108
+ }
109
+ async function installDatabase(connectionConfig, dbName, schemaSql) {
110
+ const sqlPath = path_1.default.join(__dirname, '../sql/radix.sql');
111
+ if (!schemaSql) {
112
+ if (!fs_1.default.existsSync(sqlPath)) {
113
+ throw new Error(`SQL file not found at ${sqlPath}`);
114
+ }
115
+ const rawSql = fs_1.default.readFileSync(sqlPath, 'utf8');
116
+ schemaSql = stripSeedData(rawSql);
117
+ }
118
+ schemaSql = normalizeSchemaSql(schemaSql, dbName);
119
+ connectionConfig = {
120
+ ...connectionConfig,
121
+ multipleStatements: true,
122
+ connectTimeout: connectionConfig.connectTimeout || 5000
123
+ };
124
+ let conn;
125
+ try {
126
+ conn = await promise_1.default.createConnection(connectionConfig);
127
+ const [rows] = await conn.query('SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?', [dbName]);
128
+ if (rows.length > 0) {
129
+ const dbConn = await promise_1.default.createConnection({ ...connectionConfig, database: dbName });
130
+ try {
131
+ await ensureSchemaMarker(dbConn, dbName);
132
+ console.log(`Database "${dbName}" already exists. Ensuring required tables exist.`);
133
+ const createTables = extractCreateTableStatements(schemaSql);
134
+ for (const tbl of createTables) {
135
+ const [trows] = await dbConn.query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', [dbName, tbl.name]);
136
+ if (!trows || (Array.isArray(trows) && trows.length === 0)) {
137
+ console.log(`Creating missing table: ${tbl.name}`);
138
+ await dbConn.query(tbl.statement);
139
+ }
140
+ }
141
+ await seedDefaultContent(dbConn, dbName);
142
+ console.log(`Database "${dbName}" checked and missing tables created.`);
143
+ }
144
+ finally {
145
+ await dbConn.end();
146
+ }
147
+ return;
148
+ }
149
+ await conn.query(schemaSql);
150
+ await ensureSchemaMarker(conn, dbName);
151
+ const dbConn = await promise_1.default.createConnection({ ...connectionConfig, database: dbName });
152
+ try {
153
+ await seedDefaultContent(dbConn, dbName);
154
+ }
155
+ finally {
156
+ await dbConn.end();
157
+ }
158
+ console.log(`Created database "${dbName}" and applied schema.`);
159
+ }
160
+ finally {
161
+ if (conn) {
162
+ try {
163
+ await conn.end();
164
+ }
165
+ catch (e) { /* ignore */ }
166
+ }
167
+ }
168
+ }
169
+ async function initDatabase() {
170
+ const sqlPath = path_1.default.join(__dirname, '../sql/radix.sql');
171
+ if (!fs_1.default.existsSync(sqlPath)) {
172
+ console.warn(`SQL file not found at ${sqlPath}, skipping DB initialization.`);
173
+ return;
174
+ }
175
+ const rawSql = fs_1.default.readFileSync(sqlPath, 'utf8');
176
+ const schemaSql = normalizeSchemaSql(stripSeedData(rawSql), process.env.DB_NAME || 'radix');
177
+ const databaseName = process.env.DB_NAME || 'radix';
178
+ const connectionConfig = {
179
+ host: process.env.DB_HOST || '127.0.0.1',
180
+ user: process.env.DB_USER,
181
+ password: process.env.DB_PASSWORD,
182
+ port: Number(process.env.DB_PORT) || 3306,
183
+ multipleStatements: true,
184
+ connectTimeout: 5000
185
+ };
186
+ let conn;
187
+ try {
188
+ conn = await promise_1.default.createConnection(connectionConfig);
189
+ const [rows] = await conn.query('SELECT SCHEMA_NAME FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME = ?', [databaseName]);
190
+ if (rows.length > 0) {
191
+ const dbConn = await promise_1.default.createConnection({ ...connectionConfig, database: databaseName });
192
+ try {
193
+ await ensureSchemaMarker(dbConn, databaseName);
194
+ console.log(`Database "${databaseName}" already exists. Schema marker is ready.`);
195
+ const createTables = extractCreateTableStatements(schemaSql);
196
+ for (const tbl of createTables) {
197
+ const [trows] = await dbConn.query('SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME = ?', [databaseName, tbl.name]);
198
+ if (!trows || (Array.isArray(trows) && trows.length === 0)) {
199
+ console.log(`Creating missing table: ${tbl.name}`);
200
+ await dbConn.query(tbl.statement);
201
+ }
202
+ }
203
+ await seedDefaultContent(dbConn, databaseName);
204
+ console.log(`Database "${databaseName}" checked and missing tables created.`);
205
+ }
206
+ finally {
207
+ await dbConn.end();
208
+ }
209
+ return;
210
+ }
211
+ console.log(`Database "${databaseName}" not found. Creating empty schema from ${sqlPath}`);
212
+ await conn.query(schemaSql);
213
+ await ensureSchemaMarker(conn, databaseName);
214
+ const dbConn = await promise_1.default.createConnection({ ...connectionConfig, database: databaseName });
215
+ try {
216
+ await seedDefaultContent(dbConn, databaseName);
217
+ }
218
+ finally {
219
+ await dbConn.end();
220
+ }
221
+ console.log('Database initialization finished.');
222
+ }
223
+ catch (err) {
224
+ console.error('Failed to initialize database:', err);
225
+ }
226
+ finally {
227
+ if (conn) {
228
+ try {
229
+ await conn.end();
230
+ }
231
+ catch (e) { /* ignore */ }
232
+ }
233
+ }
234
+ }
@@ -0,0 +1,48 @@
1
+ "use strict";
2
+ /**************************************************************************************************
3
+ * RadixCMS
4
+ *
5
+ * DESCRIPTION: authentication and role-based access middleware.
6
+ *
7
+ * Copyright (C) 2026 Ultra Spark Software <salve@ultraspark.net>
8
+ * SPDX-License-Identifier: GPL-3.0-or-later
9
+ **************************************************************************************************/
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.ROLE_HIERARCHY = void 0;
12
+ exports.requireRole = requireRole;
13
+ exports.ROLE_HIERARCHY = [
14
+ 'Unregistered',
15
+ 'Registered',
16
+ 'Editor',
17
+ 'Manager',
18
+ 'Administrator'
19
+ ];
20
+ /*
21
+ * requireRole
22
+ *
23
+ * PURPOSE:
24
+ * Creates middleware that permits requests only when the signed-in user's role
25
+ * meets or exceeds the required role.
26
+ *
27
+ * PARAMETERS:
28
+ * requiredRole (UserRole): Minimum role required to access the route.
29
+ *
30
+ * RETURNS:
31
+ * Returns Express middleware that calls next(), redirects to login, or sends
32
+ * an HTTP 403 response.
33
+ */
34
+ function requireRole(requiredRole) {
35
+ return (req, res, next) => {
36
+ const userRole = req.session?.user?.role || 'Unregistered';
37
+ const userLevel = exports.ROLE_HIERARCHY.indexOf(userRole);
38
+ const requiredLevel = exports.ROLE_HIERARCHY.indexOf(requiredRole);
39
+ if (userLevel >= requiredLevel) {
40
+ return next();
41
+ }
42
+ if (userLevel === 0) {
43
+ res.redirect('/admin/login');
44
+ return;
45
+ }
46
+ res.status(403).send('Forbidden: Insufficient privileges.');
47
+ };
48
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ /**************************************************************************************************
3
+ * RadixCMS
4
+ *
5
+ * DESCRIPTION: example SEO plugin and plugin lifecycle handlers.
6
+ *
7
+ * Copyright (C) 2026 Ultra Spark Software <salve@ultraspark.net>
8
+ * SPDX-License-Identifier: GPL-3.0-or-later
9
+ **************************************************************************************************/
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ const seoPlugin = {
12
+ name: 'SEO Meta Generator',
13
+ version: '1.0.0',
14
+ init: (ctx) => {
15
+ // 1. Transform page content before rendering
16
+ ctx.registerFilter('page:content', (content) => {
17
+ return `${content}\n<!-- SEO Engine Checked -->`;
18
+ });
19
+ // 2. Add an event listener on page creation
20
+ ctx.registerHook('page:created', (pageData) => {
21
+ console.log(`[SEO Plugin]: Generating sitemap entry for ${pageData.slug}...`);
22
+ });
23
+ // 3. Add custom plugin routes to Express
24
+ ctx.router.get('/admin/seo-settings', (req, res) => {
25
+ res.send('SEO Settings Dashboard');
26
+ });
27
+ }
28
+ };
29
+ exports.default = seoPlugin;