emdash 0.1.1 → 0.2.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.
- package/dist/{adapters-BLMa4JGD.d.mts → adapters-N6BF7RCD.d.mts} +1 -1
- package/dist/{adapters-BLMa4JGD.d.mts.map → adapters-N6BF7RCD.d.mts.map} +1 -1
- package/dist/{apply-kC39ev1Z.mjs → apply-wmVEOSbR.mjs} +56 -9
- package/dist/apply-wmVEOSbR.mjs.map +1 -0
- package/dist/astro/index.d.mts +6 -6
- package/dist/astro/index.mjs +80 -27
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +5 -5
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +127 -56
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/request-context.mjs +1 -1
- package/dist/astro/middleware/setup.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +74 -39
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +30 -9
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-CL847F26.mjs → byline-1WQPlISL.mjs} +51 -29
- package/dist/byline-1WQPlISL.mjs.map +1 -0
- package/dist/{bylines-C2a-2TGt.mjs → bylines-BYdTYmia.mjs} +10 -8
- package/dist/{bylines-C2a-2TGt.mjs.map → bylines-BYdTYmia.mjs.map} +1 -1
- package/dist/cli/index.mjs +15 -12
- package/dist/cli/index.mjs.map +1 -1
- package/dist/client/cf-access.d.mts +1 -1
- package/dist/client/index.d.mts +1 -1
- package/dist/client/index.mjs +1 -1
- package/dist/{config-CKE8p9xM.mjs → config-Cq8H0SfX.mjs} +2 -10
- package/dist/{config-CKE8p9xM.mjs.map → config-Cq8H0SfX.mjs.map} +1 -1
- package/dist/{content-D6C2WsZC.mjs → content-BmXndhdi.mjs} +16 -3
- package/dist/content-BmXndhdi.mjs.map +1 -0
- package/dist/db/index.d.mts +3 -3
- package/dist/db/index.mjs +1 -1
- package/dist/db/libsql.d.mts +1 -1
- package/dist/db/postgres.d.mts +1 -1
- package/dist/db/sqlite.d.mts +1 -1
- package/dist/{default-Cyi4aAxu.mjs → default-WYlzADZL.mjs} +1 -1
- package/dist/{default-Cyi4aAxu.mjs.map → default-WYlzADZL.mjs.map} +1 -1
- package/dist/{error-Cxz0tQeO.mjs → error-DrxtnGPg.mjs} +1 -1
- package/dist/{error-Cxz0tQeO.mjs.map → error-DrxtnGPg.mjs.map} +1 -1
- package/dist/{index-CLBc4gw-.d.mts → index-UHEVQMus.d.mts} +55 -17
- package/dist/index-UHEVQMus.d.mts.map +1 -0
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +17 -17
- package/dist/{load-yOOlckBj.mjs → load-Veizk2cT.mjs} +1 -1
- package/dist/{load-yOOlckBj.mjs.map → load-Veizk2cT.mjs.map} +1 -1
- package/dist/{loader-fz8Q_3EO.mjs → loader-CHb2v0jm.mjs} +1 -1
- package/dist/{loader-fz8Q_3EO.mjs.map → loader-CHb2v0jm.mjs.map} +1 -1
- package/dist/{manifest-schema-CL8DWO9b.mjs → manifest-schema-CuMio1A9.mjs} +1 -1
- package/dist/{manifest-schema-CL8DWO9b.mjs.map → manifest-schema-CuMio1A9.mjs.map} +1 -1
- package/dist/media/index.d.mts +1 -1
- package/dist/media/local-runtime.d.mts +7 -7
- package/dist/{mode-C2EzN1uE.mjs → mode-CYeM2rPt.mjs} +1 -1
- package/dist/{mode-C2EzN1uE.mjs.map → mode-CYeM2rPt.mjs.map} +1 -1
- package/dist/page/index.d.mts +10 -1
- package/dist/page/index.d.mts.map +1 -1
- package/dist/page/index.mjs +8 -4
- package/dist/page/index.mjs.map +1 -1
- package/dist/{placeholder-SvFCKbz_.d.mts → placeholder-bOx1xCTY.d.mts} +1 -1
- package/dist/{placeholder-SvFCKbz_.d.mts.map → placeholder-bOx1xCTY.d.mts.map} +1 -1
- package/dist/plugins/adapt-sandbox-entry.d.mts +5 -5
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-BVYN0PJ6.mjs → query-5Hcv_5ER.mjs} +20 -8
- package/dist/{query-BVYN0PJ6.mjs.map → query-5Hcv_5ER.mjs.map} +1 -1
- package/dist/{registry-BNYQKX_d.mjs → registry-1EvbAfsC.mjs} +6 -2
- package/dist/{registry-BNYQKX_d.mjs.map → registry-1EvbAfsC.mjs.map} +1 -1
- package/dist/{runner-BraqvGYk.mjs → runner-BoN0-FPi.mjs} +155 -130
- package/dist/runner-BoN0-FPi.mjs.map +1 -0
- package/dist/{runner-EAtf0ZIe.d.mts → runner-DTqkzOzc.d.mts} +2 -2
- package/dist/{runner-EAtf0ZIe.d.mts.map → runner-DTqkzOzc.d.mts.map} +1 -1
- package/dist/runtime.d.mts +6 -6
- package/dist/runtime.mjs +1 -1
- package/dist/{search-C1gg67nN.mjs → search-BsYMed12.mjs} +235 -105
- package/dist/search-BsYMed12.mjs.map +1 -0
- package/dist/seed/index.d.mts +2 -2
- package/dist/seed/index.mjs +8 -8
- package/dist/seo/index.d.mts +1 -1
- package/dist/storage/local.d.mts +1 -1
- package/dist/storage/local.mjs +1 -1
- package/dist/storage/s3.d.mts +1 -1
- package/dist/storage/s3.mjs +1 -1
- package/dist/{tokens-DpgrkrXK.mjs → tokens-DrB-W6Q-.mjs} +1 -1
- package/dist/{tokens-DpgrkrXK.mjs.map → tokens-DrB-W6Q-.mjs.map} +1 -1
- package/dist/{transport-yxiQsi8I.mjs → transport-Bl8cTdYt.mjs} +1 -1
- package/dist/{transport-yxiQsi8I.mjs.map → transport-Bl8cTdYt.mjs.map} +1 -1
- package/dist/{transport-BFGblqwG.d.mts → transport-COOs9GSE.d.mts} +1 -1
- package/dist/{transport-BFGblqwG.d.mts.map → transport-COOs9GSE.d.mts.map} +1 -1
- package/dist/{types-BQo5JS0J.d.mts → types-6dqxBqsH.d.mts} +80 -106
- package/dist/types-6dqxBqsH.d.mts.map +1 -0
- package/dist/{types-DRjfYOEv.d.mts → types-7-UjSEyB.d.mts} +1 -1
- package/dist/{types-DRjfYOEv.d.mts.map → types-7-UjSEyB.d.mts.map} +1 -1
- package/dist/{types-CUBbjgmP.mjs → types-Bec-r_3_.mjs} +1 -1
- package/dist/{types-CUBbjgmP.mjs.map → types-Bec-r_3_.mjs.map} +1 -1
- package/dist/{types-DaNLHo_T.d.mts → types-BljtYPSd.d.mts} +1 -1
- package/dist/{types-DaNLHo_T.d.mts.map → types-BljtYPSd.d.mts.map} +1 -1
- package/dist/{types-BRuPJGdV.d.mts → types-CIsTnQvJ.d.mts} +3 -1
- package/dist/types-CIsTnQvJ.d.mts.map +1 -0
- package/dist/types-CMMN0pNg.mjs.map +1 -1
- package/dist/{types-DPfzHnjW.d.mts → types-CcreFIIH.d.mts} +1 -1
- package/dist/{types-DPfzHnjW.d.mts.map → types-CcreFIIH.d.mts.map} +1 -1
- package/dist/{types-CiA5Gac0.mjs → types-DuNbGKjF.mjs} +1 -1
- package/dist/{types-CiA5Gac0.mjs.map → types-DuNbGKjF.mjs.map} +1 -1
- package/dist/{validate-HtxZeaBi.d.mts → validate-B7KP7VLM.d.mts} +4 -4
- package/dist/{validate-HtxZeaBi.d.mts.map → validate-B7KP7VLM.d.mts.map} +1 -1
- package/dist/{validate-_rsF-Dx_.mjs → validate-CXnRKfJK.mjs} +2 -2
- package/dist/{validate-_rsF-Dx_.mjs.map → validate-CXnRKfJK.mjs.map} +1 -1
- package/package.json +6 -6
- package/src/api/csrf.ts +13 -2
- package/src/api/handlers/content.ts +7 -0
- package/src/api/handlers/dashboard.ts +4 -8
- package/src/api/handlers/device-flow.ts +55 -37
- package/src/api/handlers/index.ts +6 -1
- package/src/api/handlers/seo.ts +48 -21
- package/src/api/public-url.ts +84 -0
- package/src/api/schemas/content.ts +2 -2
- package/src/api/schemas/menus.ts +12 -2
- package/src/astro/integration/index.ts +30 -7
- package/src/astro/integration/routes.ts +13 -2
- package/src/astro/integration/runtime.ts +7 -5
- package/src/astro/integration/vite-config.ts +52 -9
- package/src/astro/middleware/auth.ts +60 -56
- package/src/astro/middleware/csp.ts +25 -0
- package/src/astro/middleware.ts +31 -3
- package/src/astro/routes/PluginRegistry.tsx +8 -2
- package/src/astro/routes/admin.astro +7 -2
- package/src/astro/routes/api/admin/users/[id]/disable.ts +18 -12
- package/src/astro/routes/api/admin/users/[id]/index.ts +26 -5
- package/src/astro/routes/api/auth/invite/complete.ts +3 -2
- package/src/astro/routes/api/auth/oauth/[provider]/callback.ts +2 -1
- package/src/astro/routes/api/auth/oauth/[provider].ts +2 -1
- package/src/astro/routes/api/auth/passkey/options.ts +3 -2
- package/src/astro/routes/api/auth/passkey/register/options.ts +3 -2
- package/src/astro/routes/api/auth/passkey/register/verify.ts +3 -2
- package/src/astro/routes/api/auth/passkey/verify.ts +3 -2
- package/src/astro/routes/api/auth/signup/complete.ts +3 -2
- package/src/astro/routes/api/content/[collection]/index.ts +31 -3
- package/src/astro/routes/api/import/wordpress/execute.ts +9 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +10 -0
- package/src/astro/routes/api/manifest.ts +1 -0
- package/src/astro/routes/api/media/providers/[providerId]/[itemId].ts +7 -2
- package/src/astro/routes/api/oauth/authorize.ts +12 -7
- package/src/astro/routes/api/oauth/device/code.ts +5 -1
- package/src/astro/routes/api/setup/admin-verify.ts +3 -2
- package/src/astro/routes/api/setup/admin.ts +3 -2
- package/src/astro/routes/api/setup/dev-bypass.ts +2 -1
- package/src/astro/routes/api/setup/index.ts +3 -2
- package/src/astro/routes/api/snapshot.ts +2 -1
- package/src/astro/routes/api/themes/preview.ts +2 -1
- package/src/astro/routes/api/well-known/auth.ts +1 -0
- package/src/astro/routes/api/well-known/oauth-authorization-server.ts +3 -2
- package/src/astro/routes/api/well-known/oauth-protected-resource.ts +3 -2
- package/src/astro/routes/robots.txt.ts +5 -1
- package/src/astro/routes/sitemap-[collection].xml.ts +104 -0
- package/src/astro/routes/sitemap.xml.ts +18 -23
- package/src/astro/types.ts +27 -1
- package/src/auth/passkey-config.ts +6 -10
- package/src/bylines/index.ts +11 -8
- package/src/cli/commands/login.ts +5 -2
- package/src/components/InlinePortableTextEditor.tsx +5 -3
- package/src/content/converters/portable-text-to-prosemirror.ts +50 -2
- package/src/database/migrations/034_published_at_index.ts +29 -0
- package/src/database/migrations/runner.ts +2 -0
- package/src/database/repositories/byline.ts +48 -42
- package/src/database/repositories/content.ts +23 -1
- package/src/database/repositories/options.ts +9 -3
- package/src/database/repositories/seo.ts +34 -17
- package/src/database/repositories/types.ts +2 -0
- package/src/emdash-runtime.ts +61 -18
- package/src/import/index.ts +1 -1
- package/src/import/sources/wxr.ts +45 -2
- package/src/index.ts +9 -1
- package/src/mcp/server.ts +85 -5
- package/src/menus/index.ts +2 -1
- package/src/page/context.ts +13 -1
- package/src/page/jsonld.ts +10 -6
- package/src/page/seo-contributions.ts +1 -1
- package/src/plugins/context.ts +145 -35
- package/src/plugins/manager.ts +12 -0
- package/src/plugins/types.ts +80 -4
- package/src/query.ts +18 -0
- package/src/schema/registry.ts +5 -0
- package/src/settings/index.ts +64 -0
- package/src/utils/chunks.ts +17 -0
- package/dist/apply-kC39ev1Z.mjs.map +0 -1
- package/dist/byline-CL847F26.mjs.map +0 -1
- package/dist/content-D6C2WsZC.mjs.map +0 -1
- package/dist/index-CLBc4gw-.d.mts.map +0 -1
- package/dist/runner-BraqvGYk.mjs.map +0 -1
- package/dist/search-C1gg67nN.mjs.map +0 -1
- package/dist/types-BQo5JS0J.d.mts.map +0 -1
- package/dist/types-BRuPJGdV.d.mts.map +0 -1
- /package/src/astro/routes/api/media/file/{[key].ts → [...key].ts} +0 -0
package/src/emdash-runtime.ts
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
|
|
10
10
|
import type { Element } from "@emdash-cms/blocks";
|
|
11
11
|
import { Kysely, sql, type Dialect } from "kysely";
|
|
12
|
+
import virtualConfig from "virtual:emdash/config";
|
|
12
13
|
|
|
13
14
|
import { validateRev } from "./api/rev.js";
|
|
14
15
|
import type {
|
|
@@ -400,11 +401,14 @@ export class EmDashRuntime {
|
|
|
400
401
|
this.pluginStates.set(pluginId, status);
|
|
401
402
|
if (status === "active") {
|
|
402
403
|
this.enabledPlugins.add(pluginId);
|
|
404
|
+
await this.rebuildHookPipeline();
|
|
405
|
+
await this._hooks.runPluginActivate(pluginId);
|
|
403
406
|
} else {
|
|
407
|
+
// Fire deactivate on the current pipeline while the plugin is still in it
|
|
408
|
+
await this._hooks.runPluginDeactivate(pluginId);
|
|
404
409
|
this.enabledPlugins.delete(pluginId);
|
|
410
|
+
await this.rebuildHookPipeline();
|
|
405
411
|
}
|
|
406
|
-
|
|
407
|
-
await this.rebuildHookPipeline();
|
|
408
412
|
}
|
|
409
413
|
|
|
410
414
|
/**
|
|
@@ -1153,7 +1157,10 @@ export class EmDashRuntime {
|
|
|
1153
1157
|
label?: string;
|
|
1154
1158
|
required?: boolean;
|
|
1155
1159
|
widget?: string;
|
|
1156
|
-
|
|
1160
|
+
// Two shapes: legacy enum-style `[{ value, label }]` for select widgets,
|
|
1161
|
+
// or arbitrary `Record<string, unknown>` for plugin field widgets that
|
|
1162
|
+
// need per-field config (e.g. a checkbox grid receiving its column defs).
|
|
1163
|
+
options?: Array<{ value: string; label: string }> | Record<string, unknown>;
|
|
1157
1164
|
}
|
|
1158
1165
|
> = {};
|
|
1159
1166
|
|
|
@@ -1165,7 +1172,14 @@ export class EmDashRuntime {
|
|
|
1165
1172
|
required: field.required,
|
|
1166
1173
|
};
|
|
1167
1174
|
if (field.widget) entry.widget = field.widget;
|
|
1168
|
-
//
|
|
1175
|
+
// Plugin field widgets read their per-field config from `field.options`,
|
|
1176
|
+
// which the seed schema types as `Record<string, unknown>`. Pass it
|
|
1177
|
+
// through to the manifest so plugin widgets in the admin SPA receive it.
|
|
1178
|
+
if (field.options) {
|
|
1179
|
+
entry.options = field.options;
|
|
1180
|
+
}
|
|
1181
|
+
// Legacy: select/multiSelect enum options live on `field.validation.options`.
|
|
1182
|
+
// Wins over `field.options` to preserve existing behavior for enum widgets.
|
|
1169
1183
|
if (field.validation?.options) {
|
|
1170
1184
|
entry.options = field.validation.options.map((v) => ({
|
|
1171
1185
|
value: v,
|
|
@@ -1243,8 +1257,8 @@ export class EmDashRuntime {
|
|
|
1243
1257
|
version: plugin.version,
|
|
1244
1258
|
enabled,
|
|
1245
1259
|
adminMode,
|
|
1246
|
-
adminPages: plugin.admin?.pages,
|
|
1247
|
-
dashboardWidgets: plugin.admin?.widgets,
|
|
1260
|
+
adminPages: plugin.admin?.pages ?? [],
|
|
1261
|
+
dashboardWidgets: plugin.admin?.widgets ?? [],
|
|
1248
1262
|
portableTextBlocks: plugin.admin?.portableTextBlocks,
|
|
1249
1263
|
fieldWidgets: plugin.admin?.fieldWidgets,
|
|
1250
1264
|
};
|
|
@@ -1266,8 +1280,8 @@ export class EmDashRuntime {
|
|
|
1266
1280
|
enabled,
|
|
1267
1281
|
sandboxed: true,
|
|
1268
1282
|
adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
|
|
1269
|
-
adminPages: entry.adminPages,
|
|
1270
|
-
dashboardWidgets: entry.adminWidgets,
|
|
1283
|
+
adminPages: entry.adminPages ?? [],
|
|
1284
|
+
dashboardWidgets: entry.adminWidgets ?? [],
|
|
1271
1285
|
};
|
|
1272
1286
|
}
|
|
1273
1287
|
|
|
@@ -1289,26 +1303,51 @@ export class EmDashRuntime {
|
|
|
1289
1303
|
enabled,
|
|
1290
1304
|
sandboxed: true,
|
|
1291
1305
|
adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
|
|
1292
|
-
adminPages: pages,
|
|
1293
|
-
dashboardWidgets: widgets,
|
|
1306
|
+
adminPages: pages ?? [],
|
|
1307
|
+
dashboardWidgets: widgets ?? [],
|
|
1294
1308
|
};
|
|
1295
1309
|
}
|
|
1296
1310
|
|
|
1297
|
-
//
|
|
1298
|
-
|
|
1311
|
+
// Build taxonomies from database
|
|
1312
|
+
let manifestTaxonomies: Array<{
|
|
1313
|
+
name: string;
|
|
1314
|
+
label: string;
|
|
1315
|
+
labelSingular?: string;
|
|
1316
|
+
hierarchical: boolean;
|
|
1317
|
+
collections: string[];
|
|
1318
|
+
}> = [];
|
|
1319
|
+
try {
|
|
1320
|
+
const rows = await this.db
|
|
1321
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
1322
|
+
.selectAll()
|
|
1323
|
+
.orderBy("name")
|
|
1324
|
+
.execute();
|
|
1325
|
+
manifestTaxonomies = rows.map((row) => ({
|
|
1326
|
+
name: row.name,
|
|
1327
|
+
label: row.label,
|
|
1328
|
+
labelSingular: row.label_singular ?? undefined,
|
|
1329
|
+
hierarchical: row.hierarchical === 1,
|
|
1330
|
+
collections: row.collections ? (JSON.parse(row.collections) as string[]).toSorted() : [],
|
|
1331
|
+
}));
|
|
1332
|
+
} catch (error) {
|
|
1333
|
+
console.debug("EmDash: Could not load taxonomy definitions:", error);
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// Build manifest hash
|
|
1299
1337
|
const manifestHash = await hashString(
|
|
1300
|
-
JSON.stringify(manifestCollections) +
|
|
1338
|
+
JSON.stringify(manifestCollections) +
|
|
1339
|
+
JSON.stringify(manifestPlugins) +
|
|
1340
|
+
JSON.stringify(manifestTaxonomies),
|
|
1301
1341
|
);
|
|
1302
1342
|
|
|
1303
1343
|
// Determine auth mode
|
|
1304
1344
|
const authMode = getAuthMode(this.config);
|
|
1305
1345
|
const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey";
|
|
1306
1346
|
|
|
1307
|
-
// Include i18n config if enabled
|
|
1308
|
-
const
|
|
1309
|
-
const i18nConfig = getI18nConfig();
|
|
1347
|
+
// Include i18n config if enabled (read from virtual module to avoid SSR module singleton mismatch)
|
|
1348
|
+
const i18nConfig = virtualConfig?.i18n;
|
|
1310
1349
|
const i18n =
|
|
1311
|
-
|
|
1350
|
+
i18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1
|
|
1312
1351
|
? { defaultLocale: i18nConfig.defaultLocale, locales: i18nConfig.locales }
|
|
1313
1352
|
: undefined;
|
|
1314
1353
|
|
|
@@ -1317,6 +1356,7 @@ export class EmDashRuntime {
|
|
|
1317
1356
|
hash: manifestHash,
|
|
1318
1357
|
collections: manifestCollections,
|
|
1319
1358
|
plugins: manifestPlugins,
|
|
1359
|
+
taxonomies: manifestTaxonomies,
|
|
1320
1360
|
authMode: authModeValue,
|
|
1321
1361
|
i18n,
|
|
1322
1362
|
marketplace: !!this.config.marketplace,
|
|
@@ -1805,7 +1845,10 @@ export class EmDashRuntime {
|
|
|
1805
1845
|
// resolution order in getPluginRouteMeta to avoid auth/execution mismatches.
|
|
1806
1846
|
const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
|
|
1807
1847
|
if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
|
|
1808
|
-
const routeRegistry = new PluginRouteRegistry({
|
|
1848
|
+
const routeRegistry = new PluginRouteRegistry({
|
|
1849
|
+
db: this.db,
|
|
1850
|
+
emailPipeline: this.email ?? undefined,
|
|
1851
|
+
});
|
|
1809
1852
|
routeRegistry.register(trustedPlugin);
|
|
1810
1853
|
|
|
1811
1854
|
const routeKey = path.replace(LEADING_SLASH_PATTERN, "");
|
package/src/import/index.ts
CHANGED
|
@@ -68,7 +68,7 @@ export {
|
|
|
68
68
|
export { validateExternalUrl, ssrfSafeFetch, SsrfError } from "./ssrf.js";
|
|
69
69
|
|
|
70
70
|
// Sources
|
|
71
|
-
export { wxrSource } from "./sources/wxr.js";
|
|
71
|
+
export { wxrSource, parseWxrDate } from "./sources/wxr.js";
|
|
72
72
|
export { wordpressRestSource } from "./sources/wordpress-rest.js";
|
|
73
73
|
export {
|
|
74
74
|
wordpressPluginSource,
|
|
@@ -302,8 +302,8 @@ function wxrPostToNormalizedItem(
|
|
|
302
302
|
title: post.title || "Untitled",
|
|
303
303
|
content,
|
|
304
304
|
excerpt: post.excerpt,
|
|
305
|
-
date: post.
|
|
306
|
-
modified: post.
|
|
305
|
+
date: parseWxrDate(post.postDateGmt, post.pubDate, post.postDate) ?? new Date(),
|
|
306
|
+
modified: parseWxrDate(post.postModifiedGmt, undefined, post.postModified),
|
|
307
307
|
author: post.creator,
|
|
308
308
|
categories: post.categories,
|
|
309
309
|
tags: post.tags,
|
|
@@ -317,6 +317,49 @@ function wxrPostToNormalizedItem(
|
|
|
317
317
|
};
|
|
318
318
|
}
|
|
319
319
|
|
|
320
|
+
/**
|
|
321
|
+
* WordPress uses "0000-00-00 00:00:00" as a sentinel for missing GMT dates
|
|
322
|
+
* (e.g. unpublished drafts). This must be treated as absent.
|
|
323
|
+
*/
|
|
324
|
+
export const WXR_ZERO_DATE = "0000-00-00 00:00:00";
|
|
325
|
+
|
|
326
|
+
/**
|
|
327
|
+
* Parse a WXR date with the correct fallback chain:
|
|
328
|
+
* 1. GMT date (always UTC, most reliable)
|
|
329
|
+
* 2. pubDate (RFC 2822, includes timezone offset)
|
|
330
|
+
* 3. Site-local date (MySQL datetime without timezone, imprecise but best available)
|
|
331
|
+
*
|
|
332
|
+
* Returns undefined when none of the inputs yield a valid date.
|
|
333
|
+
* Callers that need a guaranteed Date should use `?? new Date()`.
|
|
334
|
+
*/
|
|
335
|
+
export function parseWxrDate(
|
|
336
|
+
gmtDate: string | undefined,
|
|
337
|
+
pubDate: string | undefined,
|
|
338
|
+
localDate: string | undefined,
|
|
339
|
+
): Date | undefined {
|
|
340
|
+
if (gmtDate && gmtDate !== WXR_ZERO_DATE) {
|
|
341
|
+
// GMT dates from WordPress are "YYYY-MM-DD HH:MM:SS" in UTC.
|
|
342
|
+
// Append "Z" so the JS Date constructor treats them as UTC.
|
|
343
|
+
return new Date(gmtDate.replace(" ", "T") + "Z");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
if (pubDate) {
|
|
347
|
+
// RFC 2822 format includes timezone offset, JS Date parses it correctly
|
|
348
|
+
const d = new Date(pubDate);
|
|
349
|
+
if (!isNaN(d.getTime())) return d;
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
if (localDate) {
|
|
353
|
+
// Site-local time without timezone. Normalize to ISO-like form so
|
|
354
|
+
// runtimes that reject "YYYY-MM-DD HH:MM:SS" can still parse it as
|
|
355
|
+
// local time. If parsing still fails, return undefined.
|
|
356
|
+
const d = new Date(localDate.replace(" ", "T"));
|
|
357
|
+
if (!isNaN(d.getTime())) return d;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
return undefined;
|
|
361
|
+
}
|
|
362
|
+
|
|
320
363
|
// Export for use in other sources
|
|
321
364
|
export { analyzeWxrData, wxrPostToNormalizedItem };
|
|
322
365
|
|
package/src/index.ts
CHANGED
|
@@ -290,6 +290,7 @@ export {
|
|
|
290
290
|
probeUrl,
|
|
291
291
|
clearSources,
|
|
292
292
|
wxrSource,
|
|
293
|
+
parseWxrDate,
|
|
293
294
|
wordpressRestSource,
|
|
294
295
|
importReusableBlocksAsSections,
|
|
295
296
|
} from "./import/index.js";
|
|
@@ -336,7 +337,13 @@ export type {
|
|
|
336
337
|
GetPreviewUrlOptions,
|
|
337
338
|
} from "./preview/index.js";
|
|
338
339
|
// Site Settings
|
|
339
|
-
export {
|
|
340
|
+
export {
|
|
341
|
+
getPluginSetting,
|
|
342
|
+
getPluginSettings,
|
|
343
|
+
getSiteSetting,
|
|
344
|
+
getSiteSettings,
|
|
345
|
+
setSiteSettings,
|
|
346
|
+
} from "./settings/index.js";
|
|
340
347
|
export type {
|
|
341
348
|
SiteSettings,
|
|
342
349
|
SiteSettingKey,
|
|
@@ -352,6 +359,7 @@ export type { SeoMeta, SeoMetaOptions } from "./seo/index.js";
|
|
|
352
359
|
export type {
|
|
353
360
|
PagePlacement,
|
|
354
361
|
PublicPageContext,
|
|
362
|
+
BreadcrumbItem,
|
|
355
363
|
PageMetadataEvent,
|
|
356
364
|
PageMetadataContribution,
|
|
357
365
|
PageMetadataHandler,
|
package/src/mcp/server.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
import type { Permission, RoleLevel } from "@emdash-cms/auth";
|
|
13
|
-
import { canActOnOwn, Role } from "@emdash-cms/auth";
|
|
13
|
+
import { canActOnOwn, hasPermission, Role } from "@emdash-cms/auth";
|
|
14
14
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
15
15
|
import { McpError, ErrorCode } from "@modelcontextprotocol/sdk/types.js";
|
|
16
16
|
import { z } from "zod";
|
|
@@ -299,7 +299,7 @@ export function createMcpServer(): McpServer {
|
|
|
299
299
|
status: z
|
|
300
300
|
.enum(["draft", "published"])
|
|
301
301
|
.optional()
|
|
302
|
-
.describe("Initial status (default 'draft')"),
|
|
302
|
+
.describe("Initial status (default 'draft'). Requires publish permission."),
|
|
303
303
|
locale: z
|
|
304
304
|
.string()
|
|
305
305
|
.optional()
|
|
@@ -317,11 +317,47 @@ export function createMcpServer(): McpServer {
|
|
|
317
317
|
requireScope(extra, "content:write");
|
|
318
318
|
requireRole(extra, Role.CONTRIBUTOR);
|
|
319
319
|
const { emdash, userId } = getExtra(extra);
|
|
320
|
+
|
|
321
|
+
// Creating a translation requires edit permission on the source item
|
|
322
|
+
if (args.translationOf) {
|
|
323
|
+
const source = await emdash.handleContentGet(args.collection, args.translationOf);
|
|
324
|
+
if (!source.success) return unwrap(source);
|
|
325
|
+
requireOwnership(
|
|
326
|
+
extra,
|
|
327
|
+
extractContentAuthorId(source.data),
|
|
328
|
+
"content:edit_own",
|
|
329
|
+
"content:edit_any",
|
|
330
|
+
);
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
// Publishing requires publish permission — create as draft then publish
|
|
334
|
+
if (args.status === "published") {
|
|
335
|
+
const user = { id: userId, role: getExtra(extra).userRole };
|
|
336
|
+
if (!hasPermission(user, "content:publish_own" as Permission)) {
|
|
337
|
+
throw new McpError(
|
|
338
|
+
ErrorCode.InvalidRequest,
|
|
339
|
+
"Insufficient permissions: publishing requires content:publish_own",
|
|
340
|
+
);
|
|
341
|
+
}
|
|
342
|
+
const result = await emdash.handleContentCreate(args.collection, {
|
|
343
|
+
data: args.data,
|
|
344
|
+
slug: args.slug,
|
|
345
|
+
authorId: userId,
|
|
346
|
+
locale: args.locale,
|
|
347
|
+
translationOf: args.translationOf,
|
|
348
|
+
});
|
|
349
|
+
if (!result.success) return unwrap(result);
|
|
350
|
+
const itemId = extractContentId(result.data);
|
|
351
|
+
if (itemId) {
|
|
352
|
+
return unwrap(await emdash.handleContentPublish(args.collection, itemId));
|
|
353
|
+
}
|
|
354
|
+
return unwrap(result);
|
|
355
|
+
}
|
|
356
|
+
|
|
320
357
|
return unwrap(
|
|
321
358
|
await emdash.handleContentCreate(args.collection, {
|
|
322
359
|
data: args.data,
|
|
323
360
|
slug: args.slug,
|
|
324
|
-
status: args.status,
|
|
325
361
|
authorId: userId,
|
|
326
362
|
locale: args.locale,
|
|
327
363
|
translationOf: args.translationOf,
|
|
@@ -347,7 +383,12 @@ export function createMcpServer(): McpServer {
|
|
|
347
383
|
.optional()
|
|
348
384
|
.describe("Field values to update (only include changed fields)"),
|
|
349
385
|
slug: z.string().optional().describe("New URL slug"),
|
|
350
|
-
status: z
|
|
386
|
+
status: z
|
|
387
|
+
.enum(["draft", "published"])
|
|
388
|
+
.optional()
|
|
389
|
+
.describe(
|
|
390
|
+
"New status. Setting to 'published' requires publish permission. Setting to 'draft' unpublishes the item and also requires publish permission.",
|
|
391
|
+
),
|
|
351
392
|
_rev: z
|
|
352
393
|
.string()
|
|
353
394
|
.optional()
|
|
@@ -372,11 +413,50 @@ export function createMcpServer(): McpServer {
|
|
|
372
413
|
);
|
|
373
414
|
|
|
374
415
|
const resolvedId = extractContentId(existing.data) ?? args.id;
|
|
416
|
+
|
|
417
|
+
// Status transitions route through dedicated handlers for proper revision management
|
|
418
|
+
if (args.status === "published") {
|
|
419
|
+
requireOwnership(
|
|
420
|
+
extra,
|
|
421
|
+
extractContentAuthorId(existing.data),
|
|
422
|
+
"content:publish_own",
|
|
423
|
+
"content:publish_any",
|
|
424
|
+
);
|
|
425
|
+
if (args.data || args.slug) {
|
|
426
|
+
const updateResult = await emdash.handleContentUpdate(args.collection, resolvedId, {
|
|
427
|
+
data: args.data,
|
|
428
|
+
slug: args.slug,
|
|
429
|
+
authorId: userId,
|
|
430
|
+
_rev: args._rev,
|
|
431
|
+
});
|
|
432
|
+
if (!updateResult.success) return unwrap(updateResult);
|
|
433
|
+
}
|
|
434
|
+
return unwrap(await emdash.handleContentPublish(args.collection, resolvedId));
|
|
435
|
+
}
|
|
436
|
+
|
|
437
|
+
if (args.status === "draft") {
|
|
438
|
+
requireOwnership(
|
|
439
|
+
extra,
|
|
440
|
+
extractContentAuthorId(existing.data),
|
|
441
|
+
"content:publish_own",
|
|
442
|
+
"content:publish_any",
|
|
443
|
+
);
|
|
444
|
+
if (args.data || args.slug) {
|
|
445
|
+
const updateResult = await emdash.handleContentUpdate(args.collection, resolvedId, {
|
|
446
|
+
data: args.data,
|
|
447
|
+
slug: args.slug,
|
|
448
|
+
authorId: userId,
|
|
449
|
+
_rev: args._rev,
|
|
450
|
+
});
|
|
451
|
+
if (!updateResult.success) return unwrap(updateResult);
|
|
452
|
+
}
|
|
453
|
+
return unwrap(await emdash.handleContentUnpublish(args.collection, resolvedId));
|
|
454
|
+
}
|
|
455
|
+
|
|
375
456
|
return unwrap(
|
|
376
457
|
await emdash.handleContentUpdate(args.collection, resolvedId, {
|
|
377
458
|
data: args.data,
|
|
378
459
|
slug: args.slug,
|
|
379
|
-
status: args.status,
|
|
380
460
|
authorId: userId,
|
|
381
461
|
_rev: args._rev,
|
|
382
462
|
}),
|
package/src/menus/index.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { sql } from "kysely";
|
|
|
9
9
|
|
|
10
10
|
import type { Database } from "../database/types.js";
|
|
11
11
|
import { getDb } from "../loader.js";
|
|
12
|
+
import { sanitizeHref } from "../utils/url.js";
|
|
12
13
|
import type { Menu, MenuItem, MenuItemRow } from "./types.js";
|
|
13
14
|
|
|
14
15
|
/**
|
|
@@ -235,7 +236,7 @@ async function resolveMenuItem(
|
|
|
235
236
|
return {
|
|
236
237
|
id: item.id,
|
|
237
238
|
label: item.label,
|
|
238
|
-
url,
|
|
239
|
+
url: sanitizeHref(url),
|
|
239
240
|
target: item.target || undefined,
|
|
240
241
|
titleAttr: item.title_attr || undefined,
|
|
241
242
|
cssClasses: item.css_classes || undefined,
|
package/src/page/context.ts
CHANGED
|
@@ -5,13 +5,14 @@
|
|
|
5
5
|
* The resulting context is passed to EmDashHead / EmDashBodyStart / EmDashBodyEnd.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { PublicPageContext } from "../plugins/types.js";
|
|
8
|
+
import type { BreadcrumbItem, PublicPageContext } from "../plugins/types.js";
|
|
9
9
|
|
|
10
10
|
/** Fields shared by both input forms */
|
|
11
11
|
interface PageContextFields {
|
|
12
12
|
kind: "content" | "custom";
|
|
13
13
|
pageType?: string;
|
|
14
14
|
title?: string | null;
|
|
15
|
+
pageTitle?: string | null;
|
|
15
16
|
description?: string | null;
|
|
16
17
|
canonical?: string | null;
|
|
17
18
|
image?: string | null;
|
|
@@ -31,6 +32,14 @@ interface PageContextFields {
|
|
|
31
32
|
};
|
|
32
33
|
/** Site name for structured data and og:site_name */
|
|
33
34
|
siteName?: string;
|
|
35
|
+
/**
|
|
36
|
+
* Breadcrumb trail for this page, root first. Pass an empty array
|
|
37
|
+
* to explicitly opt out of breadcrumbs (e.g. homepage), or omit the
|
|
38
|
+
* field to let consumers fall back to their own derivation.
|
|
39
|
+
*/
|
|
40
|
+
breadcrumbs?: BreadcrumbItem[];
|
|
41
|
+
/** Public-facing site URL (origin) for structured data */
|
|
42
|
+
siteUrl?: string;
|
|
34
43
|
}
|
|
35
44
|
|
|
36
45
|
/** Input with Astro global -- used in .astro files */
|
|
@@ -76,6 +85,7 @@ export function createPublicPageContext(input: CreatePublicPageContextInput): Pu
|
|
|
76
85
|
kind: input.kind,
|
|
77
86
|
pageType: input.pageType ?? (input.kind === "content" ? "article" : "website"),
|
|
78
87
|
title: input.title ?? null,
|
|
88
|
+
pageTitle: input.pageTitle ?? null,
|
|
79
89
|
description: input.description ?? null,
|
|
80
90
|
canonical: input.canonical ?? null,
|
|
81
91
|
image: input.image ?? null,
|
|
@@ -89,5 +99,7 @@ export function createPublicPageContext(input: CreatePublicPageContextInput): Pu
|
|
|
89
99
|
seo: input.seo,
|
|
90
100
|
articleMeta: input.articleMeta,
|
|
91
101
|
siteName: input.siteName,
|
|
102
|
+
breadcrumbs: input.breadcrumbs,
|
|
103
|
+
siteUrl: input.siteUrl,
|
|
92
104
|
};
|
|
93
105
|
}
|
package/src/page/jsonld.ts
CHANGED
|
@@ -33,7 +33,7 @@ export function cleanJsonLd(obj: Record<string, unknown>): Record<string, unknow
|
|
|
33
33
|
export function buildBlogPostingJsonLd(page: PublicPageContext): Record<string, unknown> | null {
|
|
34
34
|
if (page.pageType !== "article" || !page.canonical) return null;
|
|
35
35
|
|
|
36
|
-
const ogTitle = page.seo?.ogTitle
|
|
36
|
+
const ogTitle = page.seo?.ogTitle ?? page.pageTitle ?? page.title;
|
|
37
37
|
const description = page.seo?.ogDescription || page.description;
|
|
38
38
|
const ogImage = page.seo?.ogImage || page.image;
|
|
39
39
|
const publishedTime = page.articleMeta?.publishedTime;
|
|
@@ -77,12 +77,16 @@ export function buildWebSiteJsonLd(page: PublicPageContext): Record<string, unkn
|
|
|
77
77
|
const siteName = page.siteName;
|
|
78
78
|
if (!siteName) return null;
|
|
79
79
|
|
|
80
|
-
// Use origin
|
|
80
|
+
// Use configured public origin, falling back to page URL origin
|
|
81
81
|
let siteUrl: string;
|
|
82
|
-
|
|
83
|
-
siteUrl =
|
|
84
|
-
}
|
|
85
|
-
|
|
82
|
+
if (page.siteUrl) {
|
|
83
|
+
siteUrl = page.siteUrl;
|
|
84
|
+
} else {
|
|
85
|
+
try {
|
|
86
|
+
siteUrl = new URL(page.url).origin;
|
|
87
|
+
} catch {
|
|
88
|
+
siteUrl = page.canonical || page.url;
|
|
89
|
+
}
|
|
86
90
|
}
|
|
87
91
|
|
|
88
92
|
return cleanJsonLd({
|
|
@@ -20,7 +20,7 @@ export function generateBaseSeoContributions(page: PublicPageContext): PageMetad
|
|
|
20
20
|
const contributions: PageMetadataContribution[] = [];
|
|
21
21
|
|
|
22
22
|
const description = page.description;
|
|
23
|
-
const ogTitle = page.seo?.ogTitle
|
|
23
|
+
const ogTitle = page.seo?.ogTitle ?? page.pageTitle ?? page.title;
|
|
24
24
|
const ogDescription = page.seo?.ogDescription || description;
|
|
25
25
|
const ogImage = page.seo?.ogImage || page.image;
|
|
26
26
|
const robots = page.seo?.robots;
|