emdash 0.1.1 → 0.3.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/{apply-kC39ev1Z.mjs → apply-Bqoekfbe.mjs} +57 -10
- package/dist/apply-Bqoekfbe.mjs.map +1 -0
- package/dist/astro/index.d.mts +23 -9
- package/dist/astro/index.d.mts.map +1 -1
- package/dist/astro/index.mjs +90 -25
- package/dist/astro/index.mjs.map +1 -1
- package/dist/astro/middleware/auth.d.mts +3 -3
- package/dist/astro/middleware/auth.d.mts.map +1 -1
- package/dist/astro/middleware/auth.mjs +126 -55
- package/dist/astro/middleware/auth.mjs.map +1 -1
- package/dist/astro/middleware/redirect.mjs +2 -2
- package/dist/astro/middleware/request-context.mjs +1 -1
- package/dist/astro/middleware.d.mts.map +1 -1
- package/dist/astro/middleware.mjs +80 -41
- package/dist/astro/middleware.mjs.map +1 -1
- package/dist/astro/types.d.mts +27 -6
- package/dist/astro/types.d.mts.map +1 -1
- package/dist/{byline-CL847F26.mjs → byline-BGj9p9Ht.mjs} +53 -31
- package/dist/byline-BGj9p9Ht.mjs.map +1 -0
- package/dist/{bylines-C2a-2TGt.mjs → bylines-BihaoIDY.mjs} +12 -10
- package/dist/{bylines-C2a-2TGt.mjs.map → bylines-BihaoIDY.mjs.map} +1 -1
- package/dist/cli/index.mjs +17 -14
- package/dist/cli/index.mjs.map +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-BsBoyj8G.mjs} +35 -5
- package/dist/content-BsBoyj8G.mjs.map +1 -0
- package/dist/db/index.mjs +2 -2
- package/dist/{default-Cyi4aAxu.mjs → default-WYlzADZL.mjs} +1 -1
- package/dist/{default-Cyi4aAxu.mjs.map → default-WYlzADZL.mjs.map} +1 -1
- package/dist/{dialect-helpers-B9uSp2GJ.mjs → dialect-helpers-DhTzaUxP.mjs} +4 -1
- package/dist/dialect-helpers-DhTzaUxP.mjs.map +1 -0
- 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-Cff7AimE.d.mts} +77 -15
- package/dist/index-Cff7AimE.d.mts.map +1 -0
- package/dist/index.d.mts +6 -6
- package/dist/index.mjs +19 -19
- 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-BmYdf3Dr.mjs} +4 -2
- package/dist/loader-BmYdf3Dr.mjs.map +1 -0
- 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/local-runtime.d.mts +4 -4
- 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/plugins/adapt-sandbox-entry.d.mts +3 -3
- package/dist/plugins/adapt-sandbox-entry.mjs +1 -1
- package/dist/{query-BVYN0PJ6.mjs → query-sesiOndV.mjs} +20 -8
- package/dist/{query-BVYN0PJ6.mjs.map → query-sesiOndV.mjs.map} +1 -1
- package/dist/{redirect-DIfIni3r.mjs → redirect-DUAk-Yl_.mjs} +9 -2
- package/dist/redirect-DUAk-Yl_.mjs.map +1 -0
- package/dist/{registry-BNYQKX_d.mjs → registry-DU18yVo0.mjs} +14 -4
- package/dist/registry-DU18yVo0.mjs.map +1 -0
- package/dist/{runner-BraqvGYk.mjs → runner-Biufrii2.mjs} +157 -132
- package/dist/runner-Biufrii2.mjs.map +1 -0
- package/dist/runner-EAtf0ZIe.d.mts.map +1 -1
- package/dist/runtime.d.mts +3 -3
- package/dist/runtime.mjs +2 -2
- package/dist/{search-C1gg67nN.mjs → search-BXB-jfu2.mjs} +241 -109
- package/dist/search-BXB-jfu2.mjs.map +1 -0
- package/dist/seed/index.d.mts +1 -1
- package/dist/seed/index.mjs +10 -10
- 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 +11 -3
- package/dist/storage/s3.d.mts.map +1 -1
- package/dist/storage/s3.mjs +76 -15
- package/dist/storage/s3.mjs.map +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/{types-BRuPJGdV.d.mts → types-BbsYgi_R.d.mts} +3 -1
- package/dist/types-BbsYgi_R.d.mts.map +1 -0
- package/dist/{types-CUBbjgmP.mjs → types-Bec-r_3_.mjs} +1 -1
- package/dist/types-Bec-r_3_.mjs.map +1 -0
- package/dist/{types-DaNLHo_T.d.mts → types-C1-PVaS_.d.mts} +14 -6
- package/dist/types-C1-PVaS_.d.mts.map +1 -0
- package/dist/types-CMMN0pNg.mjs.map +1 -1
- package/dist/{types-BQo5JS0J.d.mts → types-CaKte3hR.d.mts} +78 -6
- package/dist/types-CaKte3hR.d.mts.map +1 -0
- 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-_rsF-Dx_.mjs → validate-CXnRKfJK.mjs} +2 -2
- package/dist/{validate-_rsF-Dx_.mjs.map → validate-CXnRKfJK.mjs.map} +1 -1
- package/dist/{validate-CqRJb_xU.mjs → validate-VPnKoIzW.mjs} +11 -11
- package/dist/{validate-CqRJb_xU.mjs.map → validate-VPnKoIzW.mjs.map} +1 -1
- package/dist/{validate-HtxZeaBi.d.mts → validate-bfg9OR6N.d.mts} +2 -2
- package/dist/{validate-HtxZeaBi.d.mts.map → validate-bfg9OR6N.d.mts.map} +1 -1
- package/dist/version-REAapfsU.mjs +7 -0
- package/dist/version-REAapfsU.mjs.map +1 -0
- 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/redirects.ts +95 -3
- 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/api/schemas/redirects.ts +1 -0
- 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 +55 -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/comments/[collection]/[contentId]/index.ts +2 -0
- 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/rewrite-urls.ts +2 -0
- package/src/astro/routes/api/import/wordpress-plugin/execute.ts +10 -0
- package/src/astro/routes/api/manifest.ts +4 -1
- 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/storage/adapters.ts +19 -5
- package/src/astro/storage/types.ts +12 -4
- package/src/astro/types.ts +28 -1
- package/src/auth/passkey-config.ts +6 -10
- package/src/bylines/index.ts +13 -10
- 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/dialect-helpers.ts +3 -0
- 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 +28 -1
- package/src/database/repositories/options.ts +9 -3
- package/src/database/repositories/redirect.ts +13 -0
- package/src/database/repositories/seo.ts +34 -17
- package/src/database/repositories/types.ts +2 -0
- package/src/database/validate.ts +10 -10
- package/src/emdash-runtime.ts +66 -19
- package/src/import/index.ts +1 -1
- package/src/import/sources/wxr.ts +45 -2
- package/src/index.ts +10 -1
- package/src/loader.ts +2 -0
- package/src/mcp/server.ts +85 -5
- package/src/menus/index.ts +6 -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/redirects/loops.ts +318 -0
- package/src/schema/registry.ts +8 -0
- package/src/search/fts-manager.ts +4 -0
- package/src/settings/index.ts +64 -0
- package/src/storage/s3.ts +94 -25
- package/src/storage/types.ts +13 -5
- package/src/utils/chunks.ts +17 -0
- package/src/utils/slugify.ts +11 -0
- package/src/version.ts +12 -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/dialect-helpers-B9uSp2GJ.mjs.map +0 -1
- package/dist/index-CLBc4gw-.d.mts.map +0 -1
- package/dist/loader-fz8Q_3EO.mjs.map +0 -1
- package/dist/redirect-DIfIni3r.mjs.map +0 -1
- package/dist/registry-BNYQKX_d.mjs.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/dist/types-CUBbjgmP.mjs.map +0 -1
- package/dist/types-DaNLHo_T.d.mts.map +0 -1
- /package/src/astro/routes/api/media/file/{[key].ts → [...key].ts} +0 -0
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { sql, type Kysely, type Selectable } from "kysely";
|
|
2
2
|
import { ulid } from "ulidx";
|
|
3
3
|
|
|
4
|
+
import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
|
|
4
5
|
import { listTablesLike } from "../dialect-helpers.js";
|
|
5
6
|
import type { BylineTable, Database } from "../types.js";
|
|
6
7
|
import { validateIdentifier } from "../validate.js";
|
|
@@ -259,41 +260,44 @@ export class BylineRepository {
|
|
|
259
260
|
const result = new Map<string, ContentBylineCredit[]>();
|
|
260
261
|
if (contentIds.length === 0) return result;
|
|
261
262
|
|
|
262
|
-
const
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
"
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
263
|
+
const uniqueContentIds = [...new Set(contentIds)];
|
|
264
|
+
for (const chunk of chunks(uniqueContentIds, SQL_BATCH_SIZE)) {
|
|
265
|
+
const rows = await this.db
|
|
266
|
+
.selectFrom("_emdash_content_bylines as cb")
|
|
267
|
+
.innerJoin("_emdash_bylines as b", "b.id", "cb.byline_id")
|
|
268
|
+
.select([
|
|
269
|
+
"cb.content_id as content_id",
|
|
270
|
+
"cb.sort_order as sort_order",
|
|
271
|
+
"cb.role_label as role_label",
|
|
272
|
+
"b.id as id",
|
|
273
|
+
"b.slug as slug",
|
|
274
|
+
"b.display_name as display_name",
|
|
275
|
+
"b.bio as bio",
|
|
276
|
+
"b.avatar_media_id as avatar_media_id",
|
|
277
|
+
"b.website_url as website_url",
|
|
278
|
+
"b.user_id as user_id",
|
|
279
|
+
"b.is_guest as is_guest",
|
|
280
|
+
"b.created_at as created_at",
|
|
281
|
+
"b.updated_at as updated_at",
|
|
282
|
+
])
|
|
283
|
+
.where("cb.collection_slug", "=", collectionSlug)
|
|
284
|
+
.where("cb.content_id", "in", chunk)
|
|
285
|
+
.orderBy("cb.sort_order", "asc")
|
|
286
|
+
.execute();
|
|
284
287
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
288
|
+
for (const row of rows) {
|
|
289
|
+
const contentId = row.content_id;
|
|
290
|
+
const credit: ContentBylineCredit = {
|
|
291
|
+
byline: rowToByline(row),
|
|
292
|
+
sortOrder: row.sort_order,
|
|
293
|
+
roleLabel: row.role_label,
|
|
294
|
+
};
|
|
295
|
+
const existing = result.get(contentId);
|
|
296
|
+
if (existing) {
|
|
297
|
+
existing.push(credit);
|
|
298
|
+
} else {
|
|
299
|
+
result.set(contentId, [credit]);
|
|
300
|
+
}
|
|
297
301
|
}
|
|
298
302
|
}
|
|
299
303
|
|
|
@@ -308,15 +312,17 @@ export class BylineRepository {
|
|
|
308
312
|
const result = new Map<string, BylineSummary>();
|
|
309
313
|
if (userIds.length === 0) return result;
|
|
310
314
|
|
|
311
|
-
const
|
|
312
|
-
.
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
315
|
+
for (const chunk of chunks(userIds, SQL_BATCH_SIZE)) {
|
|
316
|
+
const rows = await this.db
|
|
317
|
+
.selectFrom("_emdash_bylines")
|
|
318
|
+
.selectAll()
|
|
319
|
+
.where("user_id", "in", chunk)
|
|
320
|
+
.execute();
|
|
316
321
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
322
|
+
for (const row of rows) {
|
|
323
|
+
if (row.user_id) {
|
|
324
|
+
result.set(row.user_id, rowToByline(row));
|
|
325
|
+
}
|
|
320
326
|
}
|
|
321
327
|
}
|
|
322
328
|
return result;
|
|
@@ -3,6 +3,7 @@ import { ulid } from "ulidx";
|
|
|
3
3
|
|
|
4
4
|
import { slugify } from "../../utils/slugify.js";
|
|
5
5
|
import type { Database } from "../types.js";
|
|
6
|
+
import { validateIdentifier } from "../validate.js";
|
|
6
7
|
import { RevisionRepository } from "./revision.js";
|
|
7
8
|
import type {
|
|
8
9
|
CreateContentInput,
|
|
@@ -41,6 +42,7 @@ const SYSTEM_COLUMNS = new Set([
|
|
|
41
42
|
* Get the table name for a collection type
|
|
42
43
|
*/
|
|
43
44
|
function getTableName(type: string): string {
|
|
45
|
+
validateIdentifier(type, "collection type");
|
|
44
46
|
return `ec_${type}`;
|
|
45
47
|
}
|
|
46
48
|
|
|
@@ -116,6 +118,7 @@ export class ContentRepository {
|
|
|
116
118
|
locale,
|
|
117
119
|
translationOf,
|
|
118
120
|
publishedAt,
|
|
121
|
+
createdAt,
|
|
119
122
|
} = input;
|
|
120
123
|
|
|
121
124
|
// Validate required fields
|
|
@@ -155,7 +158,7 @@ export class ContentRepository {
|
|
|
155
158
|
status,
|
|
156
159
|
authorId || null,
|
|
157
160
|
primaryBylineId ?? null,
|
|
158
|
-
now,
|
|
161
|
+
createdAt || now,
|
|
159
162
|
now,
|
|
160
163
|
publishedAt || null,
|
|
161
164
|
1,
|
|
@@ -167,6 +170,7 @@ export class ContentRepository {
|
|
|
167
170
|
if (data && typeof data === "object") {
|
|
168
171
|
for (const [key, value] of Object.entries(data)) {
|
|
169
172
|
if (!SYSTEM_COLUMNS.has(key)) {
|
|
173
|
+
validateIdentifier(key, "content field name");
|
|
170
174
|
columns.push(key);
|
|
171
175
|
values.push(serializeValue(value));
|
|
172
176
|
}
|
|
@@ -577,6 +581,7 @@ export class ContentRepository {
|
|
|
577
581
|
if (input.data !== undefined && typeof input.data === "object") {
|
|
578
582
|
for (const [key, value] of Object.entries(input.data)) {
|
|
579
583
|
if (!SYSTEM_COLUMNS.has(key)) {
|
|
584
|
+
validateIdentifier(key, "content field name");
|
|
580
585
|
updates[key] = serializeValue(value);
|
|
581
586
|
}
|
|
582
587
|
}
|
|
@@ -767,6 +772,27 @@ export class ContentRepository {
|
|
|
767
772
|
return Number(result?.count || 0);
|
|
768
773
|
}
|
|
769
774
|
|
|
775
|
+
// get overall statistics (total, published, draft) for a content type in a single query
|
|
776
|
+
async getStats(type: string): Promise<{ total: number; published: number; draft: number }> {
|
|
777
|
+
const tableName = getTableName(type);
|
|
778
|
+
|
|
779
|
+
const result = await this.db
|
|
780
|
+
.selectFrom(tableName as keyof Database)
|
|
781
|
+
.select((eb) => [
|
|
782
|
+
eb.fn.count("id").as("total"),
|
|
783
|
+
eb.fn.sum(eb.case().when("status", "=", "published").then(1).else(0).end()).as("published"),
|
|
784
|
+
eb.fn.sum(eb.case().when("status", "=", "draft").then(1).else(0).end()).as("draft"),
|
|
785
|
+
])
|
|
786
|
+
.where("deleted_at" as never, "is", null)
|
|
787
|
+
.executeTakeFirst();
|
|
788
|
+
|
|
789
|
+
return {
|
|
790
|
+
total: Number(result?.total || 0),
|
|
791
|
+
published: Number(result?.published || 0),
|
|
792
|
+
draft: Number(result?.draft || 0),
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
|
|
770
796
|
/**
|
|
771
797
|
* Schedule content for future publishing
|
|
772
798
|
*
|
|
@@ -1057,6 +1083,7 @@ export class ContentRepository {
|
|
|
1057
1083
|
for (const [key, value] of Object.entries(data)) {
|
|
1058
1084
|
if (SYSTEM_COLUMNS.has(key)) continue;
|
|
1059
1085
|
if (key.startsWith("_")) continue; // revision metadata
|
|
1086
|
+
validateIdentifier(key, "content field name");
|
|
1060
1087
|
updates[key] = serializeValue(value);
|
|
1061
1088
|
}
|
|
1062
1089
|
|
|
@@ -1,7 +1,11 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { sql, type Kysely, type SqlBool } from "kysely";
|
|
2
2
|
|
|
3
3
|
import type { Database, OptionTable } from "../types.js";
|
|
4
4
|
|
|
5
|
+
function escapeLike(value: string): string {
|
|
6
|
+
return value.replaceAll("\\", "\\\\").replaceAll("%", "\\%").replaceAll("_", "\\_");
|
|
7
|
+
}
|
|
8
|
+
|
|
5
9
|
/**
|
|
6
10
|
* Options repository for key-value settings storage
|
|
7
11
|
*
|
|
@@ -122,10 +126,11 @@ export class OptionsRepository {
|
|
|
122
126
|
* Get all options matching a prefix
|
|
123
127
|
*/
|
|
124
128
|
async getByPrefix<T = unknown>(prefix: string): Promise<Map<string, T>> {
|
|
129
|
+
const pattern = `${escapeLike(prefix)}%`;
|
|
125
130
|
const rows = await this.db
|
|
126
131
|
.selectFrom("options")
|
|
127
132
|
.select(["name", "value"])
|
|
128
|
-
.where(
|
|
133
|
+
.where(sql<SqlBool>`name LIKE ${pattern} ESCAPE '\\'`)
|
|
129
134
|
.execute();
|
|
130
135
|
|
|
131
136
|
const result = new Map<string, T>();
|
|
@@ -140,9 +145,10 @@ export class OptionsRepository {
|
|
|
140
145
|
* Delete all options matching a prefix
|
|
141
146
|
*/
|
|
142
147
|
async deleteByPrefix(prefix: string): Promise<number> {
|
|
148
|
+
const pattern = `${escapeLike(prefix)}%`;
|
|
143
149
|
const result = await this.db
|
|
144
150
|
.deleteFrom("options")
|
|
145
|
-
.where(
|
|
151
|
+
.where(sql<SqlBool>`name LIKE ${pattern} ESCAPE '\\'`)
|
|
146
152
|
.executeTakeFirst();
|
|
147
153
|
|
|
148
154
|
return Number(result.numDeletedRows ?? 0);
|
|
@@ -237,6 +237,19 @@ export class RedirectRepository {
|
|
|
237
237
|
return BigInt(result.numDeletedRows) > 0n;
|
|
238
238
|
}
|
|
239
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Fetch all enabled redirects (for loop detection graph building).
|
|
242
|
+
* Not paginated — returns the full set.
|
|
243
|
+
*/
|
|
244
|
+
async findAllEnabled(): Promise<Redirect[]> {
|
|
245
|
+
const rows = await this.db
|
|
246
|
+
.selectFrom("_emdash_redirects")
|
|
247
|
+
.selectAll()
|
|
248
|
+
.where("enabled", "=", 1)
|
|
249
|
+
.execute();
|
|
250
|
+
return rows.map(rowToRedirect);
|
|
251
|
+
}
|
|
252
|
+
|
|
240
253
|
// --- Matching -----------------------------------------------------------
|
|
241
254
|
|
|
242
255
|
async findExactMatch(path: string): Promise<Redirect | null> {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import { sql, type Kysely } from "kysely";
|
|
2
2
|
|
|
3
|
+
import { chunks, SQL_BATCH_SIZE } from "../../utils/chunks.js";
|
|
3
4
|
import type { Database } from "../types.js";
|
|
4
5
|
import type { ContentSeo, ContentSeoInput } from "./types.js";
|
|
5
6
|
|
|
@@ -36,6 +37,19 @@ function hasAnyField(input: ContentSeoInput): boolean {
|
|
|
36
37
|
export class SeoRepository {
|
|
37
38
|
constructor(private db: Kysely<Database>) {}
|
|
38
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Check whether a collection has SEO enabled (`has_seo = 1`).
|
|
42
|
+
* Returns `false` if the collection does not exist.
|
|
43
|
+
*/
|
|
44
|
+
async isEnabled(collection: string): Promise<boolean> {
|
|
45
|
+
const row = await this.db
|
|
46
|
+
.selectFrom("_emdash_collections")
|
|
47
|
+
.select("has_seo")
|
|
48
|
+
.where("slug", "=", collection)
|
|
49
|
+
.executeTakeFirst();
|
|
50
|
+
return row?.has_seo === 1;
|
|
51
|
+
}
|
|
52
|
+
|
|
39
53
|
/**
|
|
40
54
|
* Get SEO data for a content item. Returns null defaults if no row exists.
|
|
41
55
|
*/
|
|
@@ -61,37 +75,40 @@ export class SeoRepository {
|
|
|
61
75
|
}
|
|
62
76
|
|
|
63
77
|
/**
|
|
64
|
-
* Get SEO data for multiple content items
|
|
78
|
+
* Get SEO data for multiple content items.
|
|
65
79
|
* Returns a Map keyed by content_id. Items without SEO rows get defaults.
|
|
80
|
+
*
|
|
81
|
+
* Chunks the `content_id IN (…)` clause so the total bound-parameter count
|
|
82
|
+
* per statement (ids + the `collection = ?` filter) stays within Cloudflare
|
|
83
|
+
* D1's 100-variable limit regardless of how many content items are passed.
|
|
66
84
|
*/
|
|
67
85
|
async getMany(collection: string, contentIds: string[]): Promise<Map<string, ContentSeo>> {
|
|
68
86
|
const result = new Map<string, ContentSeo>();
|
|
69
87
|
|
|
70
88
|
if (contentIds.length === 0) return result;
|
|
71
89
|
|
|
72
|
-
//
|
|
73
|
-
const rows = await this.db
|
|
74
|
-
.selectFrom("_emdash_seo")
|
|
75
|
-
.selectAll()
|
|
76
|
-
.where("collection", "=", collection)
|
|
77
|
-
.where("content_id", "in", contentIds)
|
|
78
|
-
.execute();
|
|
79
|
-
|
|
80
|
-
// Index fetched rows by content_id
|
|
81
|
-
const rowMap = new Map(rows.map((r) => [r.content_id, r]));
|
|
82
|
-
|
|
90
|
+
// Pre-fill with defaults so every input id has an entry even if no row exists.
|
|
83
91
|
for (const id of contentIds) {
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
92
|
+
result.set(id, { ...SEO_DEFAULTS });
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const uniqueContentIds = [...new Set(contentIds)];
|
|
96
|
+
for (const chunk of chunks(uniqueContentIds, SQL_BATCH_SIZE)) {
|
|
97
|
+
const rows = await this.db
|
|
98
|
+
.selectFrom("_emdash_seo")
|
|
99
|
+
.selectAll()
|
|
100
|
+
.where("collection", "=", collection)
|
|
101
|
+
.where("content_id", "in", chunk)
|
|
102
|
+
.execute();
|
|
103
|
+
|
|
104
|
+
for (const row of rows) {
|
|
105
|
+
result.set(row.content_id, {
|
|
87
106
|
title: row.seo_title ?? null,
|
|
88
107
|
description: row.seo_description ?? null,
|
|
89
108
|
image: row.seo_image ?? null,
|
|
90
109
|
canonical: row.seo_canonical ?? null,
|
|
91
110
|
noIndex: row.seo_no_index === 1,
|
|
92
111
|
});
|
|
93
|
-
} else {
|
|
94
|
-
result.set(id, { ...SEO_DEFAULTS });
|
|
95
112
|
}
|
|
96
113
|
}
|
|
97
114
|
|
|
@@ -10,6 +10,8 @@ export interface CreateContentInput {
|
|
|
10
10
|
locale?: string;
|
|
11
11
|
translationOf?: string;
|
|
12
12
|
publishedAt?: string | null;
|
|
13
|
+
/** Override created_at (ISO 8601). Used by importers to preserve original dates. */
|
|
14
|
+
createdAt?: string | null;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
export interface UpdateContentInput {
|
package/src/database/validate.ts
CHANGED
|
@@ -79,16 +79,6 @@ export function validateIdentifier(value: string, label = "identifier"): void {
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
-
/**
|
|
83
|
-
* Validate that a string is a safe SQL identifier, allowing hyphens.
|
|
84
|
-
*
|
|
85
|
-
* Like `validateIdentifier` but also permits hyphens, which appear in
|
|
86
|
-
* plugin IDs (e.g., "my-plugin"). Matches `/^[a-z][a-z0-9_-]*$/`.
|
|
87
|
-
*
|
|
88
|
-
* @param value - The string to validate
|
|
89
|
-
* @param label - Human-readable label for error messages
|
|
90
|
-
* @throws {IdentifierError} If the value is not valid
|
|
91
|
-
*/
|
|
92
82
|
/**
|
|
93
83
|
* Validate that a string is a safe JSON field name for use in json_extract paths.
|
|
94
84
|
*
|
|
@@ -120,6 +110,16 @@ export function validateJsonFieldName(value: string, label = "JSON field name"):
|
|
|
120
110
|
}
|
|
121
111
|
}
|
|
122
112
|
|
|
113
|
+
/**
|
|
114
|
+
* Validate that a string is a safe SQL identifier, allowing hyphens.
|
|
115
|
+
*
|
|
116
|
+
* Like `validateIdentifier` but also permits hyphens, which appear in
|
|
117
|
+
* plugin IDs (e.g., "my-plugin"). Matches `/^[a-z][a-z0-9_-]*$/`.
|
|
118
|
+
*
|
|
119
|
+
* @param value - The string to validate
|
|
120
|
+
* @param label - Human-readable label for error messages
|
|
121
|
+
* @throws {IdentifierError} If the value is not valid
|
|
122
|
+
*/
|
|
123
123
|
export function validatePluginIdentifier(value: string, label = "plugin identifier"): void {
|
|
124
124
|
if (!value || typeof value !== "string") {
|
|
125
125
|
throw new IdentifierError(`${label} must be a non-empty string`, String(value));
|
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 {
|
|
@@ -22,6 +23,7 @@ import { isSqlite } from "./database/dialect-helpers.js";
|
|
|
22
23
|
import { runMigrations } from "./database/migrations/runner.js";
|
|
23
24
|
import { RevisionRepository } from "./database/repositories/revision.js";
|
|
24
25
|
import type { ContentItem as ContentItemInternal } from "./database/repositories/types.js";
|
|
26
|
+
import { validateIdentifier } from "./database/validate.js";
|
|
25
27
|
import { normalizeMediaValue } from "./media/normalize.js";
|
|
26
28
|
import type { MediaProvider, MediaProviderCapabilities } from "./media/types.js";
|
|
27
29
|
import type { SandboxedPlugin, SandboxRunner } from "./plugins/sandbox/types.js";
|
|
@@ -37,6 +39,7 @@ import type {
|
|
|
37
39
|
} from "./plugins/types.js";
|
|
38
40
|
import type { FieldType } from "./schema/types.js";
|
|
39
41
|
import { hashString } from "./utils/hash.js";
|
|
42
|
+
import { COMMIT, VERSION } from "./version.js";
|
|
40
43
|
|
|
41
44
|
const LEADING_SLASH_PATTERN = /^\//;
|
|
42
45
|
|
|
@@ -400,11 +403,14 @@ export class EmDashRuntime {
|
|
|
400
403
|
this.pluginStates.set(pluginId, status);
|
|
401
404
|
if (status === "active") {
|
|
402
405
|
this.enabledPlugins.add(pluginId);
|
|
406
|
+
await this.rebuildHookPipeline();
|
|
407
|
+
await this._hooks.runPluginActivate(pluginId);
|
|
403
408
|
} else {
|
|
409
|
+
// Fire deactivate on the current pipeline while the plugin is still in it
|
|
410
|
+
await this._hooks.runPluginDeactivate(pluginId);
|
|
404
411
|
this.enabledPlugins.delete(pluginId);
|
|
412
|
+
await this.rebuildHookPipeline();
|
|
405
413
|
}
|
|
406
|
-
|
|
407
|
-
await this.rebuildHookPipeline();
|
|
408
414
|
}
|
|
409
415
|
|
|
410
416
|
/**
|
|
@@ -1153,7 +1159,10 @@ export class EmDashRuntime {
|
|
|
1153
1159
|
label?: string;
|
|
1154
1160
|
required?: boolean;
|
|
1155
1161
|
widget?: string;
|
|
1156
|
-
|
|
1162
|
+
// Two shapes: legacy enum-style `[{ value, label }]` for select widgets,
|
|
1163
|
+
// or arbitrary `Record<string, unknown>` for plugin field widgets that
|
|
1164
|
+
// need per-field config (e.g. a checkbox grid receiving its column defs).
|
|
1165
|
+
options?: Array<{ value: string; label: string }> | Record<string, unknown>;
|
|
1157
1166
|
}
|
|
1158
1167
|
> = {};
|
|
1159
1168
|
|
|
@@ -1165,7 +1174,14 @@ export class EmDashRuntime {
|
|
|
1165
1174
|
required: field.required,
|
|
1166
1175
|
};
|
|
1167
1176
|
if (field.widget) entry.widget = field.widget;
|
|
1168
|
-
//
|
|
1177
|
+
// Plugin field widgets read their per-field config from `field.options`,
|
|
1178
|
+
// which the seed schema types as `Record<string, unknown>`. Pass it
|
|
1179
|
+
// through to the manifest so plugin widgets in the admin SPA receive it.
|
|
1180
|
+
if (field.options) {
|
|
1181
|
+
entry.options = field.options;
|
|
1182
|
+
}
|
|
1183
|
+
// Legacy: select/multiSelect enum options live on `field.validation.options`.
|
|
1184
|
+
// Wins over `field.options` to preserve existing behavior for enum widgets.
|
|
1169
1185
|
if (field.validation?.options) {
|
|
1170
1186
|
entry.options = field.validation.options.map((v) => ({
|
|
1171
1187
|
value: v,
|
|
@@ -1243,8 +1259,8 @@ export class EmDashRuntime {
|
|
|
1243
1259
|
version: plugin.version,
|
|
1244
1260
|
enabled,
|
|
1245
1261
|
adminMode,
|
|
1246
|
-
adminPages: plugin.admin?.pages,
|
|
1247
|
-
dashboardWidgets: plugin.admin?.widgets,
|
|
1262
|
+
adminPages: plugin.admin?.pages ?? [],
|
|
1263
|
+
dashboardWidgets: plugin.admin?.widgets ?? [],
|
|
1248
1264
|
portableTextBlocks: plugin.admin?.portableTextBlocks,
|
|
1249
1265
|
fieldWidgets: plugin.admin?.fieldWidgets,
|
|
1250
1266
|
};
|
|
@@ -1266,8 +1282,8 @@ export class EmDashRuntime {
|
|
|
1266
1282
|
enabled,
|
|
1267
1283
|
sandboxed: true,
|
|
1268
1284
|
adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
|
|
1269
|
-
adminPages: entry.adminPages,
|
|
1270
|
-
dashboardWidgets: entry.adminWidgets,
|
|
1285
|
+
adminPages: entry.adminPages ?? [],
|
|
1286
|
+
dashboardWidgets: entry.adminWidgets ?? [],
|
|
1271
1287
|
};
|
|
1272
1288
|
}
|
|
1273
1289
|
|
|
@@ -1289,34 +1305,61 @@ export class EmDashRuntime {
|
|
|
1289
1305
|
enabled,
|
|
1290
1306
|
sandboxed: true,
|
|
1291
1307
|
adminMode: hasAdminPages || hasWidgets ? "blocks" : "none",
|
|
1292
|
-
adminPages: pages,
|
|
1293
|
-
dashboardWidgets: widgets,
|
|
1308
|
+
adminPages: pages ?? [],
|
|
1309
|
+
dashboardWidgets: widgets ?? [],
|
|
1294
1310
|
};
|
|
1295
1311
|
}
|
|
1296
1312
|
|
|
1297
|
-
//
|
|
1298
|
-
|
|
1313
|
+
// Build taxonomies from database
|
|
1314
|
+
let manifestTaxonomies: Array<{
|
|
1315
|
+
name: string;
|
|
1316
|
+
label: string;
|
|
1317
|
+
labelSingular?: string;
|
|
1318
|
+
hierarchical: boolean;
|
|
1319
|
+
collections: string[];
|
|
1320
|
+
}> = [];
|
|
1321
|
+
try {
|
|
1322
|
+
const rows = await this.db
|
|
1323
|
+
.selectFrom("_emdash_taxonomy_defs")
|
|
1324
|
+
.selectAll()
|
|
1325
|
+
.orderBy("name")
|
|
1326
|
+
.execute();
|
|
1327
|
+
manifestTaxonomies = rows.map((row) => ({
|
|
1328
|
+
name: row.name,
|
|
1329
|
+
label: row.label,
|
|
1330
|
+
labelSingular: row.label_singular ?? undefined,
|
|
1331
|
+
hierarchical: row.hierarchical === 1,
|
|
1332
|
+
collections: row.collections ? (JSON.parse(row.collections) as string[]).toSorted() : [],
|
|
1333
|
+
}));
|
|
1334
|
+
} catch (error) {
|
|
1335
|
+
console.debug("EmDash: Could not load taxonomy definitions:", error);
|
|
1336
|
+
}
|
|
1337
|
+
|
|
1338
|
+
// Build manifest hash
|
|
1299
1339
|
const manifestHash = await hashString(
|
|
1300
|
-
JSON.stringify(manifestCollections) +
|
|
1340
|
+
JSON.stringify(manifestCollections) +
|
|
1341
|
+
JSON.stringify(manifestPlugins) +
|
|
1342
|
+
JSON.stringify(manifestTaxonomies),
|
|
1301
1343
|
);
|
|
1302
1344
|
|
|
1303
1345
|
// Determine auth mode
|
|
1304
1346
|
const authMode = getAuthMode(this.config);
|
|
1305
1347
|
const authModeValue = authMode.type === "external" ? authMode.providerType : "passkey";
|
|
1306
1348
|
|
|
1307
|
-
// Include i18n config if enabled
|
|
1308
|
-
const
|
|
1309
|
-
const i18nConfig = getI18nConfig();
|
|
1349
|
+
// Include i18n config if enabled (read from virtual module to avoid SSR module singleton mismatch)
|
|
1350
|
+
const i18nConfig = virtualConfig?.i18n;
|
|
1310
1351
|
const i18n =
|
|
1311
|
-
|
|
1352
|
+
i18nConfig && i18nConfig.locales && i18nConfig.locales.length > 1
|
|
1312
1353
|
? { defaultLocale: i18nConfig.defaultLocale, locales: i18nConfig.locales }
|
|
1313
1354
|
: undefined;
|
|
1314
1355
|
|
|
1315
1356
|
return {
|
|
1316
|
-
version:
|
|
1357
|
+
version: VERSION,
|
|
1358
|
+
commit: COMMIT,
|
|
1317
1359
|
hash: manifestHash,
|
|
1318
1360
|
collections: manifestCollections,
|
|
1319
1361
|
plugins: manifestPlugins,
|
|
1362
|
+
taxonomies: manifestTaxonomies,
|
|
1320
1363
|
authMode: authModeValue,
|
|
1321
1364
|
i18n,
|
|
1322
1365
|
marketplace: !!this.config.marketplace,
|
|
@@ -1500,6 +1543,7 @@ export class EmDashRuntime {
|
|
|
1500
1543
|
});
|
|
1501
1544
|
|
|
1502
1545
|
// Update entry to point to new draft (metadata only, not data columns)
|
|
1546
|
+
validateIdentifier(collection, "collection");
|
|
1503
1547
|
const tableName = `ec_${collection}`;
|
|
1504
1548
|
await sql`
|
|
1505
1549
|
UPDATE ${sql.ref(tableName)}
|
|
@@ -1805,7 +1849,10 @@ export class EmDashRuntime {
|
|
|
1805
1849
|
// resolution order in getPluginRouteMeta to avoid auth/execution mismatches.
|
|
1806
1850
|
const trustedPlugin = this.configuredPlugins.find((p) => p.id === pluginId);
|
|
1807
1851
|
if (trustedPlugin && this.enabledPlugins.has(trustedPlugin.id)) {
|
|
1808
|
-
const routeRegistry = new PluginRouteRegistry({
|
|
1852
|
+
const routeRegistry = new PluginRouteRegistry({
|
|
1853
|
+
db: this.db,
|
|
1854
|
+
emailPipeline: this.email ?? undefined,
|
|
1855
|
+
});
|
|
1809
1856
|
routeRegistry.register(trustedPlugin);
|
|
1810
1857
|
|
|
1811
1858
|
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
|
@@ -102,6 +102,7 @@ export type {
|
|
|
102
102
|
export { ulid } from "ulidx";
|
|
103
103
|
export { computeContentHash, hashString } from "./utils/hash.js";
|
|
104
104
|
export { sanitizeHref, isSafeHref } from "./utils/url.js";
|
|
105
|
+
export { decodeSlug } from "./utils/slugify.js";
|
|
105
106
|
|
|
106
107
|
// Live Collections query functions (loader is in emdash/runtime)
|
|
107
108
|
export {
|
|
@@ -290,6 +291,7 @@ export {
|
|
|
290
291
|
probeUrl,
|
|
291
292
|
clearSources,
|
|
292
293
|
wxrSource,
|
|
294
|
+
parseWxrDate,
|
|
293
295
|
wordpressRestSource,
|
|
294
296
|
importReusableBlocksAsSections,
|
|
295
297
|
} from "./import/index.js";
|
|
@@ -336,7 +338,13 @@ export type {
|
|
|
336
338
|
GetPreviewUrlOptions,
|
|
337
339
|
} from "./preview/index.js";
|
|
338
340
|
// Site Settings
|
|
339
|
-
export {
|
|
341
|
+
export {
|
|
342
|
+
getPluginSetting,
|
|
343
|
+
getPluginSettings,
|
|
344
|
+
getSiteSetting,
|
|
345
|
+
getSiteSettings,
|
|
346
|
+
setSiteSettings,
|
|
347
|
+
} from "./settings/index.js";
|
|
340
348
|
export type {
|
|
341
349
|
SiteSettings,
|
|
342
350
|
SiteSettingKey,
|
|
@@ -352,6 +360,7 @@ export type { SeoMeta, SeoMetaOptions } from "./seo/index.js";
|
|
|
352
360
|
export type {
|
|
353
361
|
PagePlacement,
|
|
354
362
|
PublicPageContext,
|
|
363
|
+
BreadcrumbItem,
|
|
355
364
|
PageMetadataEvent,
|
|
356
365
|
PageMetadataContribution,
|
|
357
366
|
PageMetadataHandler,
|