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
|
@@ -15,6 +15,7 @@ import { verifyRegistrationResponse, registerPasskey } from "@emdash-cms/auth/pa
|
|
|
15
15
|
|
|
16
16
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
17
17
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
18
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
18
19
|
import { signupCompleteBody } from "#api/schemas.js";
|
|
19
20
|
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
20
21
|
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
@@ -22,7 +23,6 @@ import { OptionsRepository } from "#db/repositories/options.js";
|
|
|
22
23
|
|
|
23
24
|
export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
24
25
|
const { emdash } = locals;
|
|
25
|
-
const passkeyPublicOrigin = emdash?.config.passkeyPublicOrigin;
|
|
26
26
|
|
|
27
27
|
if (!emdash?.db) {
|
|
28
28
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -38,7 +38,8 @@ export const POST: APIRoute = async ({ request, locals, session }) => {
|
|
|
38
38
|
const url = new URL(request.url);
|
|
39
39
|
const options = new OptionsRepository(emdash.db);
|
|
40
40
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
41
|
-
const
|
|
41
|
+
const siteUrl = getPublicOrigin(url, emdash?.config);
|
|
42
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
|
|
42
43
|
|
|
43
44
|
// Verify the passkey registration response
|
|
44
45
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -7,8 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
import type { APIRoute } from "astro";
|
|
9
9
|
|
|
10
|
-
import { requirePerm } from "#api/authorize.js";
|
|
11
|
-
import { apiError, unwrapResult } from "#api/error.js";
|
|
10
|
+
import { requirePerm, requireOwnerPerm } from "#api/authorize.js";
|
|
11
|
+
import { apiError, mapErrorStatus, unwrapResult } from "#api/error.js";
|
|
12
12
|
import { parseBody, parseQuery, isParseError } from "#api/parse.js";
|
|
13
13
|
import { contentListQuery, contentCreateBody } from "#api/schemas.js";
|
|
14
14
|
|
|
@@ -39,10 +39,38 @@ export const POST: APIRoute = async ({ params, request, locals, cache }) => {
|
|
|
39
39
|
const body = await parseBody(request, contentCreateBody);
|
|
40
40
|
if (isParseError(body)) return body;
|
|
41
41
|
|
|
42
|
-
if (!emdash?.handleContentCreate) {
|
|
42
|
+
if (!emdash?.handleContentCreate || !emdash?.handleContentGet) {
|
|
43
43
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
// Creating a translation requires edit permission on the source item
|
|
47
|
+
if (body.translationOf) {
|
|
48
|
+
const source = await emdash.handleContentGet(collection, body.translationOf);
|
|
49
|
+
if (!source.success) {
|
|
50
|
+
return apiError(
|
|
51
|
+
source.error?.code ?? "NOT_FOUND",
|
|
52
|
+
source.error?.message ?? "Translation source not found",
|
|
53
|
+
mapErrorStatus(source.error?.code),
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
const sourceData =
|
|
57
|
+
source.data && typeof source.data === "object"
|
|
58
|
+
? (source.data as Record<string, unknown>)
|
|
59
|
+
: undefined;
|
|
60
|
+
const sourceItem =
|
|
61
|
+
sourceData?.item && typeof sourceData.item === "object"
|
|
62
|
+
? (sourceData.item as Record<string, unknown>)
|
|
63
|
+
: sourceData;
|
|
64
|
+
const sourceAuthor = typeof sourceItem?.authorId === "string" ? sourceItem.authorId : "";
|
|
65
|
+
const translationDenied = requireOwnerPerm(
|
|
66
|
+
user,
|
|
67
|
+
sourceAuthor,
|
|
68
|
+
"content:edit_own",
|
|
69
|
+
"content:edit_any",
|
|
70
|
+
);
|
|
71
|
+
if (translationDenied) return translationDenied;
|
|
72
|
+
}
|
|
73
|
+
|
|
46
74
|
// Auto-set authorId to current user when creating content
|
|
47
75
|
const result = await emdash.handleContentCreate(collection, {
|
|
48
76
|
...body,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
ContentRepository,
|
|
14
14
|
importReusableBlocksAsSections,
|
|
15
15
|
type WxrPost,
|
|
16
|
+
parseWxrDate,
|
|
16
17
|
} from "emdash";
|
|
17
18
|
|
|
18
19
|
import { requirePerm } from "#api/authorize.js";
|
|
@@ -236,6 +237,12 @@ async function importContent(
|
|
|
236
237
|
bylineCache,
|
|
237
238
|
);
|
|
238
239
|
|
|
240
|
+
// Preserve original WordPress dates using the shared WXR date parser.
|
|
241
|
+
// Fallback chain: postDateGmt (UTC) → pubDate (RFC 2822) → postDate (site-local).
|
|
242
|
+
const parsedDate = parseWxrDate(post.postDateGmt, post.pubDate, post.postDate);
|
|
243
|
+
const createdAt = parsedDate ? parsedDate.toISOString() : undefined;
|
|
244
|
+
const publishedAt = status === "published" && createdAt ? createdAt : undefined;
|
|
245
|
+
|
|
239
246
|
// Create the content item
|
|
240
247
|
const createResult = await emdash.handleContentCreate(collection, {
|
|
241
248
|
data,
|
|
@@ -244,6 +251,8 @@ async function importContent(
|
|
|
244
251
|
authorId,
|
|
245
252
|
bylines: bylineId ? [{ bylineId }] : undefined,
|
|
246
253
|
locale,
|
|
254
|
+
createdAt,
|
|
255
|
+
publishedAt,
|
|
247
256
|
});
|
|
248
257
|
|
|
249
258
|
if (createResult.success) {
|
|
@@ -286,6 +286,14 @@ async function importContent(
|
|
|
286
286
|
}
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
// Preserve original dates from the source
|
|
290
|
+
const itemDateTime = item.date?.getTime();
|
|
291
|
+
const createdAt =
|
|
292
|
+
itemDateTime !== undefined && !Number.isNaN(itemDateTime)
|
|
293
|
+
? item.date.toISOString()
|
|
294
|
+
: undefined;
|
|
295
|
+
const publishedAt = status === "published" && createdAt ? createdAt : undefined;
|
|
296
|
+
|
|
289
297
|
// Create the content item
|
|
290
298
|
const createResult = await emdash.handleContentCreate(collection, {
|
|
291
299
|
data,
|
|
@@ -295,6 +303,8 @@ async function importContent(
|
|
|
295
303
|
bylines: bylineId ? [{ bylineId }] : undefined,
|
|
296
304
|
locale: item.locale,
|
|
297
305
|
translationOf,
|
|
306
|
+
createdAt,
|
|
307
|
+
publishedAt,
|
|
298
308
|
});
|
|
299
309
|
|
|
300
310
|
if (createResult.success) {
|
|
@@ -7,6 +7,7 @@
|
|
|
7
7
|
|
|
8
8
|
import type { APIRoute } from "astro";
|
|
9
9
|
|
|
10
|
+
import { requirePerm } from "#api/authorize.js";
|
|
10
11
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
11
12
|
|
|
12
13
|
export const prerender = false;
|
|
@@ -15,7 +16,9 @@ export const prerender = false;
|
|
|
15
16
|
* Get a single media item from a provider
|
|
16
17
|
*/
|
|
17
18
|
export const GET: APIRoute = async ({ params, locals }) => {
|
|
18
|
-
const { emdash } = locals;
|
|
19
|
+
const { emdash, user } = locals;
|
|
20
|
+
const denied = requirePerm(user, "media:read");
|
|
21
|
+
if (denied) return denied;
|
|
19
22
|
const { providerId, itemId } = params;
|
|
20
23
|
|
|
21
24
|
if (!providerId || !itemId) {
|
|
@@ -56,7 +59,9 @@ export const GET: APIRoute = async ({ params, locals }) => {
|
|
|
56
59
|
* Delete a media item from a provider
|
|
57
60
|
*/
|
|
58
61
|
export const DELETE: APIRoute = async ({ params, locals }) => {
|
|
59
|
-
const { emdash } = locals;
|
|
62
|
+
const { emdash, user } = locals;
|
|
63
|
+
const denied = requirePerm(user, "media:delete_any");
|
|
64
|
+
if (denied) return denied;
|
|
60
65
|
const { providerId, itemId } = params;
|
|
61
66
|
|
|
62
67
|
if (!providerId || !itemId) {
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
validateRedirectUri,
|
|
23
23
|
} from "#api/handlers/oauth-authorization.js";
|
|
24
24
|
import { lookupOAuthClient, validateClientRedirectUri } from "#api/handlers/oauth-clients.js";
|
|
25
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
25
26
|
import { VALID_SCOPES } from "#auth/api-tokens.js";
|
|
26
27
|
|
|
27
28
|
export const prerender = false;
|
|
@@ -40,14 +41,18 @@ function generateCsrfToken(): string {
|
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
/** Build the Set-Cookie header value for the CSRF token. */
|
|
43
|
-
function csrfCookieHeader(token: string, request: Request): string {
|
|
44
|
+
function csrfCookieHeader(token: string, request: Request, siteUrl?: string): string {
|
|
44
45
|
// SameSite=Strict prevents cross-site form submission.
|
|
45
46
|
// HttpOnly: the token value is embedded in the form hidden field server-side,
|
|
46
47
|
// so JS never needs to read the cookie. HttpOnly adds defense-in-depth.
|
|
47
|
-
// Secure is
|
|
48
|
-
//
|
|
49
|
-
|
|
50
|
-
|
|
48
|
+
// Secure is set when:
|
|
49
|
+
// - siteUrl is configured and uses https (proxy case — request may be http internally), OR
|
|
50
|
+
// - the actual request is over https (non-proxy case, preserve existing behaviour)
|
|
51
|
+
const isSecure = siteUrl
|
|
52
|
+
? siteUrl.startsWith("https:")
|
|
53
|
+
: new URL(request.url).protocol === "https:";
|
|
54
|
+
const secure = isSecure ? "; Secure" : "";
|
|
55
|
+
return `${CSRF_COOKIE_NAME}=${token}; Path=/_emdash/oauth/authorize; HttpOnly; SameSite=Strict${secure}`;
|
|
51
56
|
}
|
|
52
57
|
|
|
53
58
|
/** Extract the CSRF token from the request's cookies. */
|
|
@@ -130,7 +135,7 @@ export const GET: APIRoute = async ({ url, request, locals }) => {
|
|
|
130
135
|
|
|
131
136
|
// If not authenticated, redirect to login with return URL
|
|
132
137
|
if (!user) {
|
|
133
|
-
const loginUrl = new URL("/_emdash/admin/login", url
|
|
138
|
+
const loginUrl = new URL("/_emdash/admin/login", getPublicOrigin(url, emdash?.config));
|
|
134
139
|
loginUrl.searchParams.set("redirect", url.pathname + url.search);
|
|
135
140
|
return Response.redirect(loginUrl.toString(), 302);
|
|
136
141
|
}
|
|
@@ -169,7 +174,7 @@ export const GET: APIRoute = async ({ url, request, locals }) => {
|
|
|
169
174
|
return new Response(html, {
|
|
170
175
|
headers: {
|
|
171
176
|
"Content-Type": "text/html; charset=utf-8",
|
|
172
|
-
"Set-Cookie": csrfCookieHeader(csrfToken, request),
|
|
177
|
+
"Set-Cookie": csrfCookieHeader(csrfToken, request, getPublicOrigin(url, emdash?.config)),
|
|
173
178
|
},
|
|
174
179
|
});
|
|
175
180
|
};
|
|
@@ -13,6 +13,7 @@ import { z } from "zod";
|
|
|
13
13
|
import { apiError, handleError, unwrapResult } from "#api/error.js";
|
|
14
14
|
import { handleDeviceCodeRequest } from "#api/handlers/device-flow.js";
|
|
15
15
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
16
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
16
17
|
import { checkRateLimit, getClientIp, rateLimitResponse } from "#auth/rate-limit.js";
|
|
17
18
|
|
|
18
19
|
export const prerender = false;
|
|
@@ -41,7 +42,10 @@ export const POST: APIRoute = async ({ request, locals, url }) => {
|
|
|
41
42
|
}
|
|
42
43
|
|
|
43
44
|
// Build the verification URI — device page lives inside the admin SPA
|
|
44
|
-
const verificationUri = new URL(
|
|
45
|
+
const verificationUri = new URL(
|
|
46
|
+
"/_emdash/admin/device",
|
|
47
|
+
getPublicOrigin(url, emdash?.config),
|
|
48
|
+
).toString();
|
|
45
49
|
|
|
46
50
|
const result = await handleDeviceCodeRequest(emdash.db, body, verificationUri);
|
|
47
51
|
return unwrapResult(result);
|
|
@@ -14,6 +14,7 @@ import { verifyRegistrationResponse, registerPasskey } from "@emdash-cms/auth/pa
|
|
|
14
14
|
|
|
15
15
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
16
16
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
17
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
17
18
|
import { setupAdminVerifyBody } from "#api/schemas.js";
|
|
18
19
|
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
19
20
|
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
@@ -21,7 +22,6 @@ import { OptionsRepository } from "#db/repositories/options.js";
|
|
|
21
22
|
|
|
22
23
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
23
24
|
const { emdash } = locals;
|
|
24
|
-
const passkeyPublicOrigin = emdash?.config.passkeyPublicOrigin;
|
|
25
25
|
|
|
26
26
|
if (!emdash?.db) {
|
|
27
27
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -58,7 +58,8 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
58
58
|
// Get passkey config
|
|
59
59
|
const url = new URL(request.url);
|
|
60
60
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
61
|
-
const
|
|
61
|
+
const siteUrl = getPublicOrigin(url, emdash?.config);
|
|
62
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
|
|
62
63
|
|
|
63
64
|
// Verify the registration response
|
|
64
65
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -13,6 +13,7 @@ import { generateRegistrationOptions } from "@emdash-cms/auth/passkey";
|
|
|
13
13
|
|
|
14
14
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
15
15
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
16
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
16
17
|
import { setupAdminBody } from "#api/schemas.js";
|
|
17
18
|
import { createChallengeStore } from "#auth/challenge-store.js";
|
|
18
19
|
import { getPasskeyConfig } from "#auth/passkey-config.js";
|
|
@@ -20,7 +21,6 @@ import { OptionsRepository } from "#db/repositories/options.js";
|
|
|
20
21
|
|
|
21
22
|
export const POST: APIRoute = async ({ request, locals }) => {
|
|
22
23
|
const { emdash } = locals;
|
|
23
|
-
const passkeyPublicOrigin = emdash?.config.passkeyPublicOrigin;
|
|
24
24
|
|
|
25
25
|
if (!emdash?.db) {
|
|
26
26
|
return apiError("NOT_CONFIGURED", "EmDash is not initialized", 500);
|
|
@@ -57,7 +57,8 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
57
57
|
// Get passkey config
|
|
58
58
|
const url = new URL(request.url);
|
|
59
59
|
const siteName = (await options.get<string>("emdash:site_title")) ?? undefined;
|
|
60
|
-
const
|
|
60
|
+
const siteUrl = getPublicOrigin(url, emdash?.config);
|
|
61
|
+
const passkeyConfig = getPasskeyConfig(url, siteName, siteUrl);
|
|
61
62
|
|
|
62
63
|
// Generate registration options
|
|
63
64
|
const challengeStore = createChallengeStore(emdash.db);
|
|
@@ -24,6 +24,7 @@ import { ulid } from "ulidx";
|
|
|
24
24
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
25
25
|
import { escapeHtml } from "#api/escape.js";
|
|
26
26
|
import { handleApiTokenCreate } from "#api/handlers/api-tokens.js";
|
|
27
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
27
28
|
import { isSafeRedirect } from "#api/redirect.js";
|
|
28
29
|
import { runMigrations } from "#db/migrations/runner.js";
|
|
29
30
|
import { OptionsRepository } from "#db/repositories/options.js";
|
|
@@ -120,7 +121,7 @@ async function handleDevBypass(context: Parameters<APIRoute>[0]): Promise<Respon
|
|
|
120
121
|
}
|
|
121
122
|
|
|
122
123
|
// Store canonical site URL (used by magic-link/recovery emails)
|
|
123
|
-
await options.set("emdash:site_url", url
|
|
124
|
+
await options.set("emdash:site_url", getPublicOrigin(url, emdash?.config));
|
|
124
125
|
|
|
125
126
|
// Mark setup complete
|
|
126
127
|
await options.set("emdash:setup_complete", true);
|
|
@@ -10,6 +10,7 @@ export const prerender = false;
|
|
|
10
10
|
|
|
11
11
|
import { apiError, apiSuccess, handleError } from "#api/error.js";
|
|
12
12
|
import { isParseError, parseBody } from "#api/parse.js";
|
|
13
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
13
14
|
import { setupBody } from "#api/schemas.js";
|
|
14
15
|
import { getAuthMode } from "#auth/mode.js";
|
|
15
16
|
import { runMigrations } from "#db/migrations/runner.js";
|
|
@@ -18,7 +19,7 @@ import { applySeed } from "#seed/apply.js";
|
|
|
18
19
|
import { loadSeed } from "#seed/load.js";
|
|
19
20
|
import { validateSeed } from "#seed/validate.js";
|
|
20
21
|
|
|
21
|
-
export const POST: APIRoute = async ({ request, locals }) => {
|
|
22
|
+
export const POST: APIRoute = async ({ request, url, locals }) => {
|
|
22
23
|
const { emdash } = locals;
|
|
23
24
|
|
|
24
25
|
if (!emdash?.db) {
|
|
@@ -89,7 +90,7 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
|
|
89
90
|
|
|
90
91
|
// Store the canonical site URL from the setup request.
|
|
91
92
|
// This is trusted because setup runs on the real domain.
|
|
92
|
-
const siteUrl =
|
|
93
|
+
const siteUrl = getPublicOrigin(url, emdash.config);
|
|
93
94
|
await options.set("emdash:site_url", siteUrl);
|
|
94
95
|
|
|
95
96
|
if (useExternalAuth) {
|
|
@@ -16,6 +16,7 @@ import {
|
|
|
16
16
|
parsePreviewSignatureHeader,
|
|
17
17
|
verifyPreviewSignature,
|
|
18
18
|
} from "#api/handlers/snapshot.js";
|
|
19
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
19
20
|
|
|
20
21
|
export const prerender = false;
|
|
21
22
|
|
|
@@ -65,7 +66,7 @@ export const GET: APIRoute = async ({ request, locals, url }) => {
|
|
|
65
66
|
const includeDrafts = url.searchParams.get("drafts") === "true";
|
|
66
67
|
const snapshot = await generateSnapshot(emdash.db, {
|
|
67
68
|
includeDrafts,
|
|
68
|
-
origin: url.
|
|
69
|
+
origin: getPublicOrigin(url, emdash.config),
|
|
69
70
|
});
|
|
70
71
|
|
|
71
72
|
return apiSuccess(snapshot);
|
|
@@ -11,6 +11,7 @@ import type { APIRoute } from "astro";
|
|
|
11
11
|
|
|
12
12
|
import { requirePerm } from "#api/authorize.js";
|
|
13
13
|
import { apiError, apiSuccess } from "#api/error.js";
|
|
14
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
14
15
|
|
|
15
16
|
export const prerender = false;
|
|
16
17
|
|
|
@@ -52,7 +53,7 @@ export const POST: APIRoute = async ({ request, url, locals }) => {
|
|
|
52
53
|
return apiError("INVALID_REQUEST", "previewUrl must use HTTPS", 400);
|
|
53
54
|
}
|
|
54
55
|
|
|
55
|
-
const source = url
|
|
56
|
+
const source = getPublicOrigin(url, emdash?.config);
|
|
56
57
|
const ttl = 3600; // 1 hour
|
|
57
58
|
const exp = Math.floor(Date.now() / 1000) + ttl;
|
|
58
59
|
|
|
@@ -9,12 +9,13 @@
|
|
|
9
9
|
|
|
10
10
|
import type { APIRoute } from "astro";
|
|
11
11
|
|
|
12
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
12
13
|
import { VALID_SCOPES } from "#auth/api-tokens.js";
|
|
13
14
|
|
|
14
15
|
export const prerender = false;
|
|
15
16
|
|
|
16
|
-
export const GET: APIRoute = async ({ url }) => {
|
|
17
|
-
const origin = url.
|
|
17
|
+
export const GET: APIRoute = async ({ url, locals }) => {
|
|
18
|
+
const origin = getPublicOrigin(url, locals.emdash?.config);
|
|
18
19
|
const issuer = `${origin}/_emdash`;
|
|
19
20
|
|
|
20
21
|
return Response.json(
|
|
@@ -13,12 +13,13 @@
|
|
|
13
13
|
|
|
14
14
|
import type { APIRoute } from "astro";
|
|
15
15
|
|
|
16
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
16
17
|
import { VALID_SCOPES } from "#auth/api-tokens.js";
|
|
17
18
|
|
|
18
19
|
export const prerender = false;
|
|
19
20
|
|
|
20
|
-
export const GET: APIRoute = async ({ url }) => {
|
|
21
|
-
const origin = url.
|
|
21
|
+
export const GET: APIRoute = async ({ url, locals }) => {
|
|
22
|
+
const origin = getPublicOrigin(url, locals.emdash?.config);
|
|
22
23
|
|
|
23
24
|
return Response.json(
|
|
24
25
|
{
|
|
@@ -10,6 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
import type { APIRoute } from "astro";
|
|
12
12
|
|
|
13
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
13
14
|
import { getSiteSettingsWithDb } from "#settings/index.js";
|
|
14
15
|
|
|
15
16
|
export const prerender = false;
|
|
@@ -29,7 +30,10 @@ export const GET: APIRoute = async ({ locals, url }) => {
|
|
|
29
30
|
|
|
30
31
|
try {
|
|
31
32
|
const settings = await getSiteSettingsWithDb(emdash.db);
|
|
32
|
-
const siteUrl = (settings.url || url
|
|
33
|
+
const siteUrl = (settings.url || getPublicOrigin(url, emdash?.config)).replace(
|
|
34
|
+
TRAILING_SLASH_RE,
|
|
35
|
+
"",
|
|
36
|
+
);
|
|
33
37
|
const sitemapUrl = `${siteUrl}/sitemap.xml`;
|
|
34
38
|
|
|
35
39
|
// Use custom robots.txt if configured
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Per-collection sitemap endpoint
|
|
3
|
+
*
|
|
4
|
+
* GET /sitemap-{collection}.xml - Sitemap for a single content collection.
|
|
5
|
+
*
|
|
6
|
+
* Uses the collection's url_pattern to build URLs. Falls back to
|
|
7
|
+
* /{collection}/{slug} when no pattern is configured.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { APIRoute } from "astro";
|
|
11
|
+
|
|
12
|
+
import { handleSitemapData } from "#api/handlers/seo.js";
|
|
13
|
+
import { getSiteSettingsWithDb } from "#settings/index.js";
|
|
14
|
+
|
|
15
|
+
export const prerender = false;
|
|
16
|
+
|
|
17
|
+
const TRAILING_SLASH_RE = /\/$/;
|
|
18
|
+
const AMP_RE = /&/g;
|
|
19
|
+
const LT_RE = /</g;
|
|
20
|
+
const GT_RE = />/g;
|
|
21
|
+
const QUOT_RE = /"/g;
|
|
22
|
+
const APOS_RE = /'/g;
|
|
23
|
+
const SLUG_PLACEHOLDER = "{slug}";
|
|
24
|
+
const ID_PLACEHOLDER = "{id}";
|
|
25
|
+
|
|
26
|
+
export const GET: APIRoute = async ({ params, locals, url }) => {
|
|
27
|
+
const { emdash } = locals;
|
|
28
|
+
const collectionSlug = params.collection;
|
|
29
|
+
|
|
30
|
+
if (!emdash?.db || !collectionSlug) {
|
|
31
|
+
return new Response("<!-- EmDash not configured -->", {
|
|
32
|
+
status: 500,
|
|
33
|
+
headers: { "Content-Type": "application/xml" },
|
|
34
|
+
});
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
const settings = await getSiteSettingsWithDb(emdash.db);
|
|
39
|
+
const siteUrl = (settings.url || url.origin).replace(TRAILING_SLASH_RE, "");
|
|
40
|
+
|
|
41
|
+
const result = await handleSitemapData(emdash.db, collectionSlug);
|
|
42
|
+
|
|
43
|
+
if (!result.success || !result.data) {
|
|
44
|
+
return new Response("<!-- Failed to generate sitemap -->", {
|
|
45
|
+
status: 500,
|
|
46
|
+
headers: { "Content-Type": "application/xml" },
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const col = result.data.collections[0];
|
|
51
|
+
if (!col) {
|
|
52
|
+
return new Response("<!-- Collection not found or empty -->", {
|
|
53
|
+
status: 404,
|
|
54
|
+
headers: { "Content-Type": "application/xml" },
|
|
55
|
+
});
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
const lines: string[] = [
|
|
59
|
+
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
60
|
+
'<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
61
|
+
];
|
|
62
|
+
|
|
63
|
+
for (const entry of col.entries) {
|
|
64
|
+
const slug = entry.slug || entry.id;
|
|
65
|
+
const path = col.urlPattern
|
|
66
|
+
? col.urlPattern
|
|
67
|
+
.replace(SLUG_PLACEHOLDER, encodeURIComponent(slug))
|
|
68
|
+
.replace(ID_PLACEHOLDER, encodeURIComponent(entry.id))
|
|
69
|
+
: `/${encodeURIComponent(col.collection)}/${encodeURIComponent(slug)}`;
|
|
70
|
+
|
|
71
|
+
const loc = `${siteUrl}${path}`;
|
|
72
|
+
|
|
73
|
+
lines.push(" <url>");
|
|
74
|
+
lines.push(` <loc>${escapeXml(loc)}</loc>`);
|
|
75
|
+
lines.push(` <lastmod>${escapeXml(entry.updatedAt)}</lastmod>`);
|
|
76
|
+
lines.push(" </url>");
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
lines.push("</urlset>");
|
|
80
|
+
|
|
81
|
+
return new Response(lines.join("\n"), {
|
|
82
|
+
status: 200,
|
|
83
|
+
headers: {
|
|
84
|
+
"Content-Type": "application/xml; charset=utf-8",
|
|
85
|
+
"Cache-Control": "public, max-age=3600",
|
|
86
|
+
},
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
return new Response("<!-- Internal error generating sitemap -->", {
|
|
90
|
+
status: 500,
|
|
91
|
+
headers: { "Content-Type": "application/xml" },
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
/** Escape special XML characters in a string */
|
|
97
|
+
function escapeXml(str: string): string {
|
|
98
|
+
return str
|
|
99
|
+
.replace(AMP_RE, "&")
|
|
100
|
+
.replace(LT_RE, "<")
|
|
101
|
+
.replace(GT_RE, ">")
|
|
102
|
+
.replace(QUOT_RE, """)
|
|
103
|
+
.replace(APOS_RE, "'");
|
|
104
|
+
}
|
|
@@ -1,18 +1,17 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Sitemap
|
|
2
|
+
* Sitemap index endpoint
|
|
3
3
|
*
|
|
4
|
-
* GET /sitemap.xml -
|
|
4
|
+
* GET /sitemap.xml - Sitemap index listing one sitemap per collection.
|
|
5
5
|
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
* Default URL pattern: /{collection}/{slug-or-id}. Users can override
|
|
10
|
-
* by creating their own /sitemap.xml route in their Astro project.
|
|
6
|
+
* Each collection with published, indexable content gets its own
|
|
7
|
+
* child sitemap at /sitemap-{collection}.xml. The index includes
|
|
8
|
+
* a <lastmod> per child derived from the most recently updated entry.
|
|
11
9
|
*/
|
|
12
10
|
|
|
13
11
|
import type { APIRoute } from "astro";
|
|
14
12
|
|
|
15
13
|
import { handleSitemapData } from "#api/handlers/seo.js";
|
|
14
|
+
import { getPublicOrigin } from "#api/public-url.js";
|
|
16
15
|
import { getSiteSettingsWithDb } from "#settings/index.js";
|
|
17
16
|
|
|
18
17
|
export const prerender = false;
|
|
@@ -35,9 +34,11 @@ export const GET: APIRoute = async ({ locals, url }) => {
|
|
|
35
34
|
}
|
|
36
35
|
|
|
37
36
|
try {
|
|
38
|
-
// Determine site URL from settings or request origin
|
|
39
37
|
const settings = await getSiteSettingsWithDb(emdash.db);
|
|
40
|
-
const siteUrl = (settings.url || url
|
|
38
|
+
const siteUrl = (settings.url || getPublicOrigin(url, emdash?.config)).replace(
|
|
39
|
+
TRAILING_SLASH_RE,
|
|
40
|
+
"",
|
|
41
|
+
);
|
|
41
42
|
|
|
42
43
|
const result = await handleSitemapData(emdash.db);
|
|
43
44
|
|
|
@@ -48,28 +49,22 @@ export const GET: APIRoute = async ({ locals, url }) => {
|
|
|
48
49
|
});
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const
|
|
52
|
+
const { collections } = result.data;
|
|
52
53
|
|
|
53
|
-
// Build XML
|
|
54
54
|
const lines: string[] = [
|
|
55
55
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
|
56
|
-
'<
|
|
56
|
+
'<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
57
57
|
];
|
|
58
58
|
|
|
59
|
-
for (const
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
const loc = `${siteUrl}/${encodeURIComponent(entry.collection)}/${encodeURIComponent(entry.identifier)}`;
|
|
63
|
-
|
|
64
|
-
lines.push(" <url>");
|
|
59
|
+
for (const col of collections) {
|
|
60
|
+
const loc = `${siteUrl}/sitemap-${encodeURIComponent(col.collection)}.xml`;
|
|
61
|
+
lines.push(" <sitemap>");
|
|
65
62
|
lines.push(` <loc>${escapeXml(loc)}</loc>`);
|
|
66
|
-
lines.push(` <lastmod>${escapeXml(
|
|
67
|
-
lines.push("
|
|
68
|
-
lines.push(" <priority>0.7</priority>");
|
|
69
|
-
lines.push(" </url>");
|
|
63
|
+
lines.push(` <lastmod>${escapeXml(col.lastmod)}</lastmod>`);
|
|
64
|
+
lines.push(" </sitemap>");
|
|
70
65
|
}
|
|
71
66
|
|
|
72
|
-
lines.push("</
|
|
67
|
+
lines.push("</sitemapindex>");
|
|
73
68
|
|
|
74
69
|
return new Response(lines.join("\n"), {
|
|
75
70
|
status: 200,
|