@xexr/blog-platform 1.0.17 → 1.1.1
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/CHANGELOG.md +12 -0
- package/README.md +94 -5
- package/dist/app/api/blog/config/route.ts +259 -0
- package/dist/app/api/blog/publish/route.ts +1 -1
- package/dist/app/api/blog/slugs/[slug]/route.ts +1 -1
- package/dist/app/api/blog/unpublish/route.ts +1 -1
- package/dist/app/api/blog/version/route.ts +1 -1
- package/dist/app/blog/[slug]/page.mdx-security.test.ts +23 -0
- package/dist/app/blog/[slug]/page.tsx +2 -2
- package/dist/app/blog/authors/[slug]/page.tsx +2 -2
- package/dist/app/blog/page.tsx +2 -2
- package/dist/app/bloggy-indexnow-key.txt/route.ts +22 -0
- package/dist/components/AuthorByline.tsx +1 -1
- package/dist/components/AuthorPages.tsx +2 -2
- package/dist/components/TopicArchivePages.tsx +2 -2
- package/dist/index.d.mts +605 -5
- package/dist/index.d.ts +605 -5
- package/dist/index.js +20 -12
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +20 -12
- package/dist/index.mjs.map +1 -1
- package/dist/lib/auth.ts +1 -1
- package/dist/lib/cors.ts +2 -2
- package/dist/lib/database.ts +20 -0
- package/dist/lib/security-headers.ts +1 -1
- package/dist/lib/storage/drizzle.ts +229 -18
- package/dist/lib/structured-data.ts +6 -1
- package/dist/sitemap.js +5 -5
- package/dist/sitemap.mjs +5 -5
- package/dist/types/contracts.ts +21 -0
- package/package.json +1 -1
package/dist/lib/auth.ts
CHANGED
package/dist/lib/cors.ts
CHANGED
|
@@ -5,8 +5,8 @@
|
|
|
5
5
|
* for cross-origin requests from Bloggy core applications.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { NextRequest } from "next/server";
|
|
9
|
-
import { NextResponse } from "next/server";
|
|
8
|
+
import type { NextRequest } from "next/server.js";
|
|
9
|
+
import { NextResponse } from "next/server.js";
|
|
10
10
|
import { getAPISecurityHeaders, type SecurityConfig } from "./security-headers";
|
|
11
11
|
|
|
12
12
|
/**
|
package/dist/lib/database.ts
CHANGED
|
@@ -52,6 +52,16 @@ export interface BlogPlatformStorage {
|
|
|
52
52
|
idempotencyKey: string,
|
|
53
53
|
result: PublishResult,
|
|
54
54
|
): Promise<void>;
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Store platform metadata/configuration by key.
|
|
58
|
+
*/
|
|
59
|
+
setMetadata(key: string, value: unknown): Promise<void>;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Retrieve platform metadata/configuration by key.
|
|
63
|
+
*/
|
|
64
|
+
getMetadata<T = unknown>(key: string): Promise<T | null>;
|
|
55
65
|
}
|
|
56
66
|
|
|
57
67
|
/**
|
|
@@ -61,6 +71,7 @@ export interface BlogPlatformStorage {
|
|
|
61
71
|
export class InMemoryStorage implements BlogPlatformStorage {
|
|
62
72
|
private articles = new Map<string, PublishedArticle>();
|
|
63
73
|
private operations = new Map<string, PublishResult>();
|
|
74
|
+
private metadata = new Map<string, unknown>();
|
|
64
75
|
|
|
65
76
|
async saveArticle(
|
|
66
77
|
article: PublishedArticle,
|
|
@@ -226,6 +237,15 @@ export class InMemoryStorage implements BlogPlatformStorage {
|
|
|
226
237
|
): Promise<void> {
|
|
227
238
|
this.operations.set(idempotencyKey, result);
|
|
228
239
|
}
|
|
240
|
+
|
|
241
|
+
async setMetadata(key: string, value: unknown): Promise<void> {
|
|
242
|
+
this.metadata.set(key, value);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
async getMetadata<T = unknown>(key: string): Promise<T | null> {
|
|
246
|
+
if (!this.metadata.has(key)) return null;
|
|
247
|
+
return this.metadata.get(key) as T;
|
|
248
|
+
}
|
|
229
249
|
}
|
|
230
250
|
|
|
231
251
|
type GlobalWithStorage = typeof globalThis & {
|
|
@@ -53,6 +53,28 @@ export const bpOperations = sqliteTable("bp_operations", {
|
|
|
53
53
|
expiresAt: integer("expires_at", { mode: "timestamp_ms" }),
|
|
54
54
|
});
|
|
55
55
|
|
|
56
|
+
export const bpMetadata = sqliteTable("bp_metadata", {
|
|
57
|
+
key: text("key").primaryKey(),
|
|
58
|
+
valueJson: text("value_json").notNull(),
|
|
59
|
+
updatedAt: integer("updated_at", { mode: "timestamp_ms" }).notNull(),
|
|
60
|
+
});
|
|
61
|
+
|
|
62
|
+
export const BLOG_PLATFORM_SCHEMA_VERSION = 2;
|
|
63
|
+
|
|
64
|
+
export const BLOG_PLATFORM_TABLE_NAMES = [
|
|
65
|
+
"bp_articles",
|
|
66
|
+
"bp_operations",
|
|
67
|
+
"bp_metadata",
|
|
68
|
+
] as const;
|
|
69
|
+
|
|
70
|
+
export type BlogPlatformTableName = (typeof BLOG_PLATFORM_TABLE_NAMES)[number];
|
|
71
|
+
|
|
72
|
+
export const blogPlatformDrizzleSchema = {
|
|
73
|
+
bpArticles,
|
|
74
|
+
bpOperations,
|
|
75
|
+
bpMetadata,
|
|
76
|
+
} as const;
|
|
77
|
+
|
|
56
78
|
type ArticleRow = InferSelectModel<typeof bpArticles>;
|
|
57
79
|
|
|
58
80
|
function toRow(article: PublishedArticle): ArticleRow {
|
|
@@ -76,6 +98,7 @@ function toRow(article: PublishedArticle): ArticleRow {
|
|
|
76
98
|
metaJson: JSON.stringify({
|
|
77
99
|
...article.meta,
|
|
78
100
|
__bloggyTopics: article.topics ?? [],
|
|
101
|
+
__bloggyCta: article.cta ?? null,
|
|
79
102
|
}),
|
|
80
103
|
version: article.version,
|
|
81
104
|
};
|
|
@@ -101,24 +124,36 @@ function fromRow(row: ArticleRow): PublishedArticle {
|
|
|
101
124
|
? (JSON.parse(row.authorJson) as PublishedAuthor)
|
|
102
125
|
: null,
|
|
103
126
|
site: JSON.parse(row.siteJson) as PublishedSite,
|
|
127
|
+
cta: extractCta(row.metaJson),
|
|
104
128
|
topics: extractTopics(row.metaJson),
|
|
105
129
|
meta: extractMeta(row.metaJson),
|
|
106
130
|
version: row.version,
|
|
107
131
|
};
|
|
108
132
|
}
|
|
109
133
|
|
|
134
|
+
type StoredArticleMeta = PublishedArticle["meta"] & {
|
|
135
|
+
__bloggyTopics?: PublishedArticle["topics"];
|
|
136
|
+
__bloggyCta?: PublishedArticle["cta"] | null;
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
function parseStoredMeta(metaJson: string): StoredArticleMeta {
|
|
140
|
+
return JSON.parse(metaJson) as StoredArticleMeta;
|
|
141
|
+
}
|
|
142
|
+
|
|
110
143
|
function extractTopics(metaJson: string): PublishedArticle["topics"] {
|
|
111
|
-
const parsed =
|
|
112
|
-
__bloggyTopics?: PublishedArticle["topics"];
|
|
113
|
-
};
|
|
144
|
+
const parsed = parseStoredMeta(metaJson);
|
|
114
145
|
return parsed.__bloggyTopics ?? [];
|
|
115
146
|
}
|
|
116
147
|
|
|
148
|
+
function extractCta(metaJson: string): PublishedArticle["cta"] {
|
|
149
|
+
const parsed = parseStoredMeta(metaJson);
|
|
150
|
+
return parsed.__bloggyCta ?? undefined;
|
|
151
|
+
}
|
|
152
|
+
|
|
117
153
|
function extractMeta(metaJson: string): PublishedArticle["meta"] {
|
|
118
|
-
const parsed =
|
|
119
|
-
__bloggyTopics?: PublishedArticle["topics"];
|
|
120
|
-
};
|
|
154
|
+
const parsed = parseStoredMeta(metaJson);
|
|
121
155
|
delete parsed.__bloggyTopics;
|
|
156
|
+
delete parsed.__bloggyCta;
|
|
122
157
|
const meta = parsed;
|
|
123
158
|
return meta;
|
|
124
159
|
}
|
|
@@ -128,6 +163,38 @@ export interface DrizzleStorageOptions {
|
|
|
128
163
|
operationTtlMs?: number;
|
|
129
164
|
}
|
|
130
165
|
|
|
166
|
+
export interface BlogPlatformSchemaCheckResult {
|
|
167
|
+
ok: boolean;
|
|
168
|
+
schemaVersion: number;
|
|
169
|
+
requiredTables: readonly BlogPlatformTableName[];
|
|
170
|
+
presentTables: BlogPlatformTableName[];
|
|
171
|
+
missingTables: BlogPlatformTableName[];
|
|
172
|
+
setupSql: string[];
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
export class BlogPlatformSchemaError extends Error {
|
|
176
|
+
readonly code = "BLOG_PLATFORM_SCHEMA_MISSING";
|
|
177
|
+
readonly missingTables: BlogPlatformTableName[];
|
|
178
|
+
readonly setupSql: string[];
|
|
179
|
+
|
|
180
|
+
constructor(
|
|
181
|
+
missingTables: BlogPlatformTableName[],
|
|
182
|
+
options: { cause?: unknown } = {},
|
|
183
|
+
) {
|
|
184
|
+
super(
|
|
185
|
+
`Blog platform database schema is missing required table(s): ${missingTables.join(
|
|
186
|
+
", ",
|
|
187
|
+
)}. Run the @xexr/blog-platform schema migration/setup for the current package version.`,
|
|
188
|
+
);
|
|
189
|
+
this.name = "BlogPlatformSchemaError";
|
|
190
|
+
this.missingTables = missingTables;
|
|
191
|
+
this.setupSql = missingTables.map(getCreateTableSqlForName);
|
|
192
|
+
if (options.cause !== undefined) {
|
|
193
|
+
this.cause = options.cause;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
131
198
|
/**
|
|
132
199
|
* Drizzle-backed durable storage adapter.
|
|
133
200
|
* Pass a Drizzle Database instance created with drizzle-orm/libsql.
|
|
@@ -374,18 +441,81 @@ export class DrizzleStorage implements BlogPlatformStorage {
|
|
|
374
441
|
})
|
|
375
442
|
.run();
|
|
376
443
|
}
|
|
444
|
+
|
|
445
|
+
async setMetadata(key: string, value: unknown): Promise<void> {
|
|
446
|
+
const now = new Date();
|
|
447
|
+
try {
|
|
448
|
+
await this.db.delete(bpMetadata).where(eq(bpMetadata.key, key)).run();
|
|
449
|
+
await this.db
|
|
450
|
+
.insert(bpMetadata)
|
|
451
|
+
.values({
|
|
452
|
+
key,
|
|
453
|
+
valueJson: JSON.stringify(value),
|
|
454
|
+
updatedAt: now,
|
|
455
|
+
})
|
|
456
|
+
.run();
|
|
457
|
+
} catch (error) {
|
|
458
|
+
throwSchemaErrorForMissingTable(error, "bp_metadata");
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
async getMetadata<T = unknown>(key: string): Promise<T | null> {
|
|
463
|
+
let rows: { valueJson: string }[];
|
|
464
|
+
try {
|
|
465
|
+
rows = await this.db
|
|
466
|
+
.select({ valueJson: bpMetadata.valueJson })
|
|
467
|
+
.from(bpMetadata)
|
|
468
|
+
.where(eq(bpMetadata.key, key))
|
|
469
|
+
.limit(1);
|
|
470
|
+
} catch (error) {
|
|
471
|
+
throwSchemaErrorForMissingTable(error, "bp_metadata");
|
|
472
|
+
}
|
|
473
|
+
const row = rows[0];
|
|
474
|
+
if (!row) return null;
|
|
475
|
+
try {
|
|
476
|
+
return JSON.parse(row.valueJson) as T;
|
|
477
|
+
} catch {
|
|
478
|
+
return null;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
377
481
|
}
|
|
378
482
|
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
483
|
+
export async function checkDrizzleAdapterSchema(
|
|
484
|
+
db: LibSQLDatabase,
|
|
485
|
+
): Promise<BlogPlatformSchemaCheckResult> {
|
|
486
|
+
const rows = await db.all<{ name: string }>(sql`
|
|
487
|
+
SELECT name
|
|
488
|
+
FROM sqlite_master
|
|
489
|
+
WHERE type = 'table'
|
|
490
|
+
`);
|
|
491
|
+
const presentTableNames = new Set(rows.map((row) => row.name));
|
|
492
|
+
const presentTables = BLOG_PLATFORM_TABLE_NAMES.filter((tableName) =>
|
|
493
|
+
presentTableNames.has(tableName),
|
|
494
|
+
);
|
|
495
|
+
const missingTables = BLOG_PLATFORM_TABLE_NAMES.filter(
|
|
496
|
+
(tableName) => !presentTableNames.has(tableName),
|
|
497
|
+
);
|
|
498
|
+
|
|
387
499
|
return {
|
|
388
|
-
|
|
500
|
+
ok: missingTables.length === 0,
|
|
501
|
+
schemaVersion: BLOG_PLATFORM_SCHEMA_VERSION,
|
|
502
|
+
requiredTables: BLOG_PLATFORM_TABLE_NAMES,
|
|
503
|
+
presentTables,
|
|
504
|
+
missingTables,
|
|
505
|
+
setupSql: missingTables.map(getCreateTableSqlForName),
|
|
506
|
+
};
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
export async function ensureDrizzleAdapterSchema(
|
|
510
|
+
db: LibSQLDatabase,
|
|
511
|
+
): Promise<void> {
|
|
512
|
+
const result = await checkDrizzleAdapterSchema(db);
|
|
513
|
+
if (!result.ok) {
|
|
514
|
+
throw new BlogPlatformSchemaError(result.missingTables);
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
|
|
518
|
+
const CREATE_ARTICLES_TABLE_SQL = `CREATE TABLE IF NOT EXISTS bp_articles (
|
|
389
519
|
id TEXT PRIMARY KEY,
|
|
390
520
|
slug TEXT NOT NULL UNIQUE,
|
|
391
521
|
title TEXT NOT NULL,
|
|
@@ -404,12 +534,93 @@ export function getDrizzleAdapterSql(): {
|
|
|
404
534
|
site_json TEXT NOT NULL,
|
|
405
535
|
meta_json TEXT NOT NULL,
|
|
406
536
|
version INTEGER NOT NULL
|
|
407
|
-
)
|
|
408
|
-
|
|
537
|
+
);`;
|
|
538
|
+
|
|
539
|
+
const CREATE_OPERATIONS_TABLE_SQL = `CREATE TABLE IF NOT EXISTS bp_operations (
|
|
409
540
|
key TEXT PRIMARY KEY,
|
|
410
541
|
result_json TEXT NOT NULL,
|
|
411
542
|
created_at INTEGER NOT NULL,
|
|
412
543
|
expires_at INTEGER
|
|
413
|
-
)
|
|
544
|
+
);`;
|
|
545
|
+
|
|
546
|
+
const CREATE_METADATA_TABLE_SQL = `CREATE TABLE IF NOT EXISTS bp_metadata (
|
|
547
|
+
key TEXT PRIMARY KEY,
|
|
548
|
+
value_json TEXT NOT NULL,
|
|
549
|
+
updated_at INTEGER NOT NULL
|
|
550
|
+
);`;
|
|
551
|
+
|
|
552
|
+
function getCreateTableSqlForName(tableName: BlogPlatformTableName): string {
|
|
553
|
+
switch (tableName) {
|
|
554
|
+
case "bp_articles":
|
|
555
|
+
return CREATE_ARTICLES_TABLE_SQL;
|
|
556
|
+
case "bp_operations":
|
|
557
|
+
return CREATE_OPERATIONS_TABLE_SQL;
|
|
558
|
+
case "bp_metadata":
|
|
559
|
+
return CREATE_METADATA_TABLE_SQL;
|
|
560
|
+
}
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
function throwSchemaErrorForMissingTable(
|
|
564
|
+
error: unknown,
|
|
565
|
+
tableName: BlogPlatformTableName,
|
|
566
|
+
): never {
|
|
567
|
+
if (isMissingTableError(error, tableName)) {
|
|
568
|
+
throw new BlogPlatformSchemaError([tableName], { cause: error });
|
|
569
|
+
}
|
|
570
|
+
throw error;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
function isMissingTableError(
|
|
574
|
+
error: unknown,
|
|
575
|
+
tableName: BlogPlatformTableName,
|
|
576
|
+
): boolean {
|
|
577
|
+
return getErrorMessages(error).some(
|
|
578
|
+
(message) =>
|
|
579
|
+
message.includes(`no such table: ${tableName}`) ||
|
|
580
|
+
message.includes(`no such table: main.${tableName}`),
|
|
581
|
+
);
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function getErrorMessages(error: unknown): string[] {
|
|
585
|
+
if (!error || typeof error !== "object") {
|
|
586
|
+
return [String(error)];
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
const messages: string[] = [];
|
|
590
|
+
if (error instanceof Error) {
|
|
591
|
+
messages.push(error.message);
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
const cause = (error as { cause?: unknown }).cause;
|
|
595
|
+
if (cause !== undefined) {
|
|
596
|
+
messages.push(...getErrorMessages(cause));
|
|
597
|
+
}
|
|
598
|
+
|
|
599
|
+
return messages;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
/**
|
|
603
|
+
* Helper: Return SQL strings for creating required tables.
|
|
604
|
+
* Consumers can embed these in their migrations.
|
|
605
|
+
*/
|
|
606
|
+
export function getDrizzleAdapterSql(): {
|
|
607
|
+
schemaVersion: number;
|
|
608
|
+
requiredTableNames: readonly BlogPlatformTableName[];
|
|
609
|
+
createAll: string[];
|
|
610
|
+
createArticles: string;
|
|
611
|
+
createOperations: string;
|
|
612
|
+
createMetadata: string;
|
|
613
|
+
} {
|
|
614
|
+
return {
|
|
615
|
+
schemaVersion: BLOG_PLATFORM_SCHEMA_VERSION,
|
|
616
|
+
requiredTableNames: BLOG_PLATFORM_TABLE_NAMES,
|
|
617
|
+
createAll: [
|
|
618
|
+
CREATE_ARTICLES_TABLE_SQL,
|
|
619
|
+
CREATE_OPERATIONS_TABLE_SQL,
|
|
620
|
+
CREATE_METADATA_TABLE_SQL,
|
|
621
|
+
],
|
|
622
|
+
createArticles: CREATE_ARTICLES_TABLE_SQL,
|
|
623
|
+
createOperations: CREATE_OPERATIONS_TABLE_SQL,
|
|
624
|
+
createMetadata: CREATE_METADATA_TABLE_SQL,
|
|
414
625
|
};
|
|
415
626
|
}
|
|
@@ -522,7 +522,12 @@ export function validateStructuredData(schema: BaseStructuredData): {
|
|
|
522
522
|
export function structuredDataToJsonLd(
|
|
523
523
|
schema: BaseStructuredData | BaseStructuredData[],
|
|
524
524
|
): string {
|
|
525
|
-
return JSON.stringify(schema, null, 0)
|
|
525
|
+
return JSON.stringify(schema, null, 0)
|
|
526
|
+
.replace(/</g, "\\u003c")
|
|
527
|
+
.replace(/>/g, "\\u003e")
|
|
528
|
+
.replace(/&/g, "\\u0026")
|
|
529
|
+
.replace(/\u2028/g, "\\u2028")
|
|
530
|
+
.replace(/\u2029/g, "\\u2029");
|
|
526
531
|
}
|
|
527
532
|
|
|
528
533
|
/**
|
package/dist/sitemap.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
'use strict';function
|
|
2
|
-
`)});return [t,r,...
|
|
3
|
-
`)}function J(e,t){let r='<?xml version="1.0" encoding="UTF-8"?>',i='<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
4
|
-
`)});return [r,i,...n,
|
|
5
|
-
`)}function Q(e){let t=[],r=[];e.entries.length===0?t.push("Sitemap must contain at least one URL"):e.entries.length>5e4&&t.push("Sitemap contains more than 50,000 URLs (Google limit)"),e.truncated&&r.push("Sitemap was truncated due to size limits"),e.entries.forEach((
|
|
1
|
+
'use strict';function T(e){let t=new Map;for(let r of e)r.author?.slug&&t.set(r.author.slug,q(t.get(r.author.slug),r.author));return Array.from(t.values()).sort((r,i)=>r.name.localeCompare(i.name))}function q(e,t){return e?{...e,bio:e.bio??t.bio,avatarUrl:e.avatarUrl??t.avatarUrl,pronouns:e.pronouns??t.pronouns,socialLinks:e.socialLinks??t.socialLinks,toneAdjectives:e.toneAdjectives.length>0?e.toneAdjectives:t.toneAdjectives}:t}function w(e,t=true){let r=e.trim();return r=r.replace(/\/+$/,""),/^https?:\/\//.exec(r)||(r=`${t?"https":"http"}://${r}`),t&&r.startsWith("http://")&&(r=r.replace("http://","https://")),r}function F(e){let t=e.trim();if(!t)throw new Error("Slug cannot be empty");if(!/^[a-z0-9-]+$/.test(t))throw new Error("Slug can only contain lowercase letters, numbers, and hyphens");let i=t.replace(/-+/g,"-").replace(/^-+|-+$/g,"");if(!i)throw new Error("Slug cannot consist only of hyphens");return i}function O(e,t={}){let{forceHttps:r=true,removeTrailingSlash:i=true,domain:s=e.site.domain,pathPrefix:n="/blog"}=t;if(!s)throw new Error("Domain is required for canonical URL generation");if(!e.slug)throw new Error("Article slug is required for canonical URL generation");let l=w(s,r),u=F(e.slug),c=n.replace(/^(?!\/)/,"/").replace(/\/+$/,""),a=`${l}${c}/${u}`;return i&&(a=a.replace(/\/+$/,"")),a}function _(e){let t=new Map;for(let r of e)for(let i of r.topics??[])t.has(i.slug)||t.set(i.slug,i);return Array.from(t.values()).sort((r,i)=>r.name.localeCompare(i.name))}function M(e="/blog"){let t=e.trim();return !t||t==="/"?"":t.replace(/^(?!\/)/,"/").replace(/\/+$/,"")}function b(e,t){let r=M(e),i=t?.replace(/^\/+/,"").replace(/\/+$/,"");return i?`${r}/${i}`.replace(/\/+/g,"/"):r||"/"}function v(e,t){return b(e,`authors/${t}`)}async function U(e,t,r=[],i={}){let{includeAuthorPages:s=true,includeTopicPages:n=true,includePaginationPages:l=true,articlesPerPage:u=10,maxEntries:c=5e4,changeFrequency:a={},priority:g={}}=i,o=[],m=w(i.canonical?.domain??e.domain,i.canonical?.forceHttps??true),f=i.canonical?.pathPrefix??"/blog",R=r.length>0?r:T(t),y=_([...t]);if(o.push({url:`${m}${f}`,lastModified:P(t),changeFrequency:a.homePage??"daily",priority:g.homePage??1}),t.forEach(p=>{if(o.length>=c)return;let d=O(p,i.canonical);o.push({url:d,lastModified:p.updatedAt,changeFrequency:a.articles??"monthly",priority:g.articles??.8});}),n&&y.length>0&&(o.length<c&&o.push({url:`${m}${b(f,"topics")}`,lastModified:P(t),changeFrequency:a.topicPages??"weekly",priority:g.topicPages??.6}),y.forEach(p=>{if(o.length>=c)return;let d=t.filter(S=>(S.topics??[]).some(h=>h.slug===p.slug));o.push({url:`${m}${b(f,`topics/${p.slug}`)}`,lastModified:P(d),changeFrequency:a.topicPages??"weekly",priority:g.topicPages??.6});})),s&&R.length>0&&(o.length<c&&o.push({url:`${m}${b(f,"authors")}`,lastModified:P(t),changeFrequency:a.authorPages??"weekly",priority:g.authorPages??.6}),R.forEach(p=>{if(o.length>=c)return;let d=`${m}${v(f,p.slug)}`,S=t.filter(A=>A.author?.id===p.id),h=P(S);o.push({url:d,lastModified:h,changeFrequency:a.authorPages??"weekly",priority:g.authorPages??.6});})),l&&t.length>u){let p=Math.ceil(t.length/u);for(let d=2;d<=p&&!(o.length>=c);d++)o.push({url:`${m}${f}?page=${d}`,lastModified:P(t),changeFrequency:a.paginationPages??"daily",priority:g.paginationPages??.5});}return o}function X(e,t,r=[],i={}){let{includeAuthorPages:s=true,includeTopicPages:n=true,includePaginationPages:l=true,articlesPerPage:u=10,maxEntries:c=5e4,changeFrequency:a={},priority:g={}}=i,o=[],m=w(i.canonical?.domain??e.domain,i.canonical?.forceHttps??true),f=i.canonical?.pathPrefix??"/blog",R=new Date().toISOString(),y=r.length>0?r:T(t),p=_([...t]),d=1;if(d+=t.length,n&&p.length>0&&(d+=p.length+1),s&&(d+=y.length>0?y.length+1:0),l&&t.length>u){let h=Math.ceil(t.length/u);d+=h-1;}let S=d>c;if(o.push({loc:`${m}${f}`,lastmod:P(t),changefreq:a.homePage??"daily",priority:g.homePage??1}),t.forEach(h=>{if(o.length>=c)return;let A=O(h,i.canonical);o.push({loc:A,lastmod:h.updatedAt,changefreq:a.articles??"monthly",priority:g.articles??.8});}),n&&p.length>0&&(o.length<c&&o.push({loc:`${m}${b(f,"topics")}`,lastmod:P(t),changefreq:a.topicPages??"weekly",priority:g.topicPages??.6}),p.forEach(h=>{if(o.length>=c)return;let A=t.filter($=>($.topics??[]).some(E=>E.slug===h.slug));o.push({loc:`${m}${b(f,`topics/${h.slug}`)}`,lastmod:P(A),changefreq:a.topicPages??"weekly",priority:g.topicPages??.6});})),s&&y.length>0&&(o.length<c&&o.push({loc:`${m}${b(f,"authors")}`,lastmod:P(t),changefreq:a.authorPages??"weekly",priority:g.authorPages??.6}),y.forEach(h=>{if(o.length>=c)return;let A=`${m}${v(f,h.slug)}`,$=t.filter(B=>B.author?.id===h.id),E=P($);o.push({loc:A,lastmod:E,changefreq:a.authorPages??"weekly",priority:g.authorPages??.6});})),l&&t.length>u){let h=Math.ceil(t.length/u);for(let A=2;A<=h&&!(o.length>=c);A++)o.push({loc:`${m}${f}?page=${A}`,lastmod:P(t),changefreq:a.paginationPages??"daily",priority:g.paginationPages??.5});}return {entries:o,totalEntries:o.length,truncated:S,generatedAt:R,baseUrl:m}}function L(e){let t='<?xml version="1.0" encoding="UTF-8"?>',r='<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',i="</urlset>",s=e.entries.map(n=>{let l=[];return l.push(" <url>"),l.push(` <loc>${x(n.loc)}</loc>`),n.lastmod&&l.push(` <lastmod>${x(n.lastmod)}</lastmod>`),n.changefreq&&l.push(` <changefreq>${x(n.changefreq)}</changefreq>`),n.priority!==void 0&&l.push(` <priority>${n.priority.toFixed(1)}</priority>`),l.push(" </url>"),l.join(`
|
|
2
|
+
`)});return [t,r,...s,i].join(`
|
|
3
|
+
`)}function J(e,t){let r='<?xml version="1.0" encoding="UTF-8"?>',i='<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',s="</sitemapindex>",n=e.map(l=>{let u=[];return u.push(" <sitemap>"),u.push(` <loc>${x(l.url)}</loc>`),l.lastmod&&u.push(` <lastmod>${x(l.lastmod)}</lastmod>`),u.push(" </sitemap>"),u.join(`
|
|
4
|
+
`)});return [r,i,...n,s].join(`
|
|
5
|
+
`)}function Q(e){let t=[],r=[];e.entries.length===0?t.push("Sitemap must contain at least one URL"):e.entries.length>5e4&&t.push("Sitemap contains more than 50,000 URLs (Google limit)"),e.truncated&&r.push("Sitemap was truncated due to size limits"),e.entries.forEach((s,n)=>{try{new URL(s.loc);}catch{t.push(`Invalid URL at entry ${n+1}: ${s.loc}`);}s.priority!==void 0&&(s.priority<0||s.priority>1)&&t.push(`Invalid priority at entry ${n+1}: must be between 0.0 and 1.0`),s.lastmod&&!C(s.lastmod)&&t.push(`Invalid lastmod date at entry ${n+1}: ${s.lastmod}`),s.loc.length>2048&&r.push(`URL at entry ${n+1} exceeds 2048 character limit`);});let i=new Map;return e.entries.forEach(s=>{let n=i.get(s.loc)??0;i.set(s.loc,n+1);}),i.forEach((s,n)=>{s>1&&t.push(`Duplicate URL found: ${n} (appears ${s} times)`);}),{valid:t.length===0,errors:t,warnings:r}}function P(e){return e.length===0?new Date().toISOString():e.reduce((r,i)=>{let s=new Date(i.updatedAt);return s>r?s:r},new Date(e[0].updatedAt)).toISOString()}function Y(e,t,r=[],i={}){return async function(){let l=(await U(e,t,r,i)).map(a=>({loc:a.url,lastmod:typeof a.lastModified=="string"?a.lastModified:a.lastModified?.toISOString(),changefreq:a.changeFrequency,priority:a.priority})),u={entries:l,totalEntries:l.length,generatedAt:new Date().toISOString(),baseUrl:w(i.canonical?.domain??e.domain,i.canonical?.forceHttps??true)},c=L(u);return new Response(c,{headers:{"Content-Type":"application/xml","Cache-Control":"public, max-age=3600, s-maxage=3600"}})}}function K(e){return async function(){let r=[...e.staticRoutes??[],...e.getStaticRoutes?await e.getStaticRoutes():[]],i=await e.getSite(),s=await e.getArticles(),n=e.getAuthors?await e.getAuthors():[];return [...r,...await U(i,s,n,e.options)]}}function x(e){let t={"&":"&","<":"<",">":">",'"':""","'":"'"};return e.replace(/[&<>"']/g,r=>t[r]??r)}function C(e){try{let t=new Date(e),r=t.toISOString(),i=e.endsWith("Z")&&!e.includes(".")?e.replace("Z",".000Z"):e;return r===i&&!isNaN(t.getTime())}catch{return false}}exports.createBlogSitemapRoute=Y;exports.createDynamicBlogSitemap=K;exports.generateBlogSitemapEntries=U;exports.generateSitemap=X;exports.generateSitemapIndex=J;exports.sitemapToXml=L;exports.validateSitemap=Q;//# sourceMappingURL=sitemap.js.map
|
|
6
6
|
//# sourceMappingURL=sitemap.js.map
|
package/dist/sitemap.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
function
|
|
2
|
-
`)});return [t,r,...
|
|
3
|
-
`)}function J(e,t){let r='<?xml version="1.0" encoding="UTF-8"?>',i='<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',
|
|
4
|
-
`)});return [r,i,...n,
|
|
5
|
-
`)}function Q(e){let t=[],r=[];e.entries.length===0?t.push("Sitemap must contain at least one URL"):e.entries.length>5e4&&t.push("Sitemap contains more than 50,000 URLs (Google limit)"),e.truncated&&r.push("Sitemap was truncated due to size limits"),e.entries.forEach((
|
|
1
|
+
function T(e){let t=new Map;for(let r of e)r.author?.slug&&t.set(r.author.slug,q(t.get(r.author.slug),r.author));return Array.from(t.values()).sort((r,i)=>r.name.localeCompare(i.name))}function q(e,t){return e?{...e,bio:e.bio??t.bio,avatarUrl:e.avatarUrl??t.avatarUrl,pronouns:e.pronouns??t.pronouns,socialLinks:e.socialLinks??t.socialLinks,toneAdjectives:e.toneAdjectives.length>0?e.toneAdjectives:t.toneAdjectives}:t}function w(e,t=true){let r=e.trim();return r=r.replace(/\/+$/,""),/^https?:\/\//.exec(r)||(r=`${t?"https":"http"}://${r}`),t&&r.startsWith("http://")&&(r=r.replace("http://","https://")),r}function F(e){let t=e.trim();if(!t)throw new Error("Slug cannot be empty");if(!/^[a-z0-9-]+$/.test(t))throw new Error("Slug can only contain lowercase letters, numbers, and hyphens");let i=t.replace(/-+/g,"-").replace(/^-+|-+$/g,"");if(!i)throw new Error("Slug cannot consist only of hyphens");return i}function O(e,t={}){let{forceHttps:r=true,removeTrailingSlash:i=true,domain:s=e.site.domain,pathPrefix:n="/blog"}=t;if(!s)throw new Error("Domain is required for canonical URL generation");if(!e.slug)throw new Error("Article slug is required for canonical URL generation");let l=w(s,r),u=F(e.slug),c=n.replace(/^(?!\/)/,"/").replace(/\/+$/,""),a=`${l}${c}/${u}`;return i&&(a=a.replace(/\/+$/,"")),a}function _(e){let t=new Map;for(let r of e)for(let i of r.topics??[])t.has(i.slug)||t.set(i.slug,i);return Array.from(t.values()).sort((r,i)=>r.name.localeCompare(i.name))}function M(e="/blog"){let t=e.trim();return !t||t==="/"?"":t.replace(/^(?!\/)/,"/").replace(/\/+$/,"")}function b(e,t){let r=M(e),i=t?.replace(/^\/+/,"").replace(/\/+$/,"");return i?`${r}/${i}`.replace(/\/+/g,"/"):r||"/"}function v(e,t){return b(e,`authors/${t}`)}async function U(e,t,r=[],i={}){let{includeAuthorPages:s=true,includeTopicPages:n=true,includePaginationPages:l=true,articlesPerPage:u=10,maxEntries:c=5e4,changeFrequency:a={},priority:g={}}=i,o=[],m=w(i.canonical?.domain??e.domain,i.canonical?.forceHttps??true),f=i.canonical?.pathPrefix??"/blog",R=r.length>0?r:T(t),y=_([...t]);if(o.push({url:`${m}${f}`,lastModified:P(t),changeFrequency:a.homePage??"daily",priority:g.homePage??1}),t.forEach(p=>{if(o.length>=c)return;let d=O(p,i.canonical);o.push({url:d,lastModified:p.updatedAt,changeFrequency:a.articles??"monthly",priority:g.articles??.8});}),n&&y.length>0&&(o.length<c&&o.push({url:`${m}${b(f,"topics")}`,lastModified:P(t),changeFrequency:a.topicPages??"weekly",priority:g.topicPages??.6}),y.forEach(p=>{if(o.length>=c)return;let d=t.filter(S=>(S.topics??[]).some(h=>h.slug===p.slug));o.push({url:`${m}${b(f,`topics/${p.slug}`)}`,lastModified:P(d),changeFrequency:a.topicPages??"weekly",priority:g.topicPages??.6});})),s&&R.length>0&&(o.length<c&&o.push({url:`${m}${b(f,"authors")}`,lastModified:P(t),changeFrequency:a.authorPages??"weekly",priority:g.authorPages??.6}),R.forEach(p=>{if(o.length>=c)return;let d=`${m}${v(f,p.slug)}`,S=t.filter(A=>A.author?.id===p.id),h=P(S);o.push({url:d,lastModified:h,changeFrequency:a.authorPages??"weekly",priority:g.authorPages??.6});})),l&&t.length>u){let p=Math.ceil(t.length/u);for(let d=2;d<=p&&!(o.length>=c);d++)o.push({url:`${m}${f}?page=${d}`,lastModified:P(t),changeFrequency:a.paginationPages??"daily",priority:g.paginationPages??.5});}return o}function X(e,t,r=[],i={}){let{includeAuthorPages:s=true,includeTopicPages:n=true,includePaginationPages:l=true,articlesPerPage:u=10,maxEntries:c=5e4,changeFrequency:a={},priority:g={}}=i,o=[],m=w(i.canonical?.domain??e.domain,i.canonical?.forceHttps??true),f=i.canonical?.pathPrefix??"/blog",R=new Date().toISOString(),y=r.length>0?r:T(t),p=_([...t]),d=1;if(d+=t.length,n&&p.length>0&&(d+=p.length+1),s&&(d+=y.length>0?y.length+1:0),l&&t.length>u){let h=Math.ceil(t.length/u);d+=h-1;}let S=d>c;if(o.push({loc:`${m}${f}`,lastmod:P(t),changefreq:a.homePage??"daily",priority:g.homePage??1}),t.forEach(h=>{if(o.length>=c)return;let A=O(h,i.canonical);o.push({loc:A,lastmod:h.updatedAt,changefreq:a.articles??"monthly",priority:g.articles??.8});}),n&&p.length>0&&(o.length<c&&o.push({loc:`${m}${b(f,"topics")}`,lastmod:P(t),changefreq:a.topicPages??"weekly",priority:g.topicPages??.6}),p.forEach(h=>{if(o.length>=c)return;let A=t.filter($=>($.topics??[]).some(E=>E.slug===h.slug));o.push({loc:`${m}${b(f,`topics/${h.slug}`)}`,lastmod:P(A),changefreq:a.topicPages??"weekly",priority:g.topicPages??.6});})),s&&y.length>0&&(o.length<c&&o.push({loc:`${m}${b(f,"authors")}`,lastmod:P(t),changefreq:a.authorPages??"weekly",priority:g.authorPages??.6}),y.forEach(h=>{if(o.length>=c)return;let A=`${m}${v(f,h.slug)}`,$=t.filter(B=>B.author?.id===h.id),E=P($);o.push({loc:A,lastmod:E,changefreq:a.authorPages??"weekly",priority:g.authorPages??.6});})),l&&t.length>u){let h=Math.ceil(t.length/u);for(let A=2;A<=h&&!(o.length>=c);A++)o.push({loc:`${m}${f}?page=${A}`,lastmod:P(t),changefreq:a.paginationPages??"daily",priority:g.paginationPages??.5});}return {entries:o,totalEntries:o.length,truncated:S,generatedAt:R,baseUrl:m}}function L(e){let t='<?xml version="1.0" encoding="UTF-8"?>',r='<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',i="</urlset>",s=e.entries.map(n=>{let l=[];return l.push(" <url>"),l.push(` <loc>${x(n.loc)}</loc>`),n.lastmod&&l.push(` <lastmod>${x(n.lastmod)}</lastmod>`),n.changefreq&&l.push(` <changefreq>${x(n.changefreq)}</changefreq>`),n.priority!==void 0&&l.push(` <priority>${n.priority.toFixed(1)}</priority>`),l.push(" </url>"),l.join(`
|
|
2
|
+
`)});return [t,r,...s,i].join(`
|
|
3
|
+
`)}function J(e,t){let r='<?xml version="1.0" encoding="UTF-8"?>',i='<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">',s="</sitemapindex>",n=e.map(l=>{let u=[];return u.push(" <sitemap>"),u.push(` <loc>${x(l.url)}</loc>`),l.lastmod&&u.push(` <lastmod>${x(l.lastmod)}</lastmod>`),u.push(" </sitemap>"),u.join(`
|
|
4
|
+
`)});return [r,i,...n,s].join(`
|
|
5
|
+
`)}function Q(e){let t=[],r=[];e.entries.length===0?t.push("Sitemap must contain at least one URL"):e.entries.length>5e4&&t.push("Sitemap contains more than 50,000 URLs (Google limit)"),e.truncated&&r.push("Sitemap was truncated due to size limits"),e.entries.forEach((s,n)=>{try{new URL(s.loc);}catch{t.push(`Invalid URL at entry ${n+1}: ${s.loc}`);}s.priority!==void 0&&(s.priority<0||s.priority>1)&&t.push(`Invalid priority at entry ${n+1}: must be between 0.0 and 1.0`),s.lastmod&&!C(s.lastmod)&&t.push(`Invalid lastmod date at entry ${n+1}: ${s.lastmod}`),s.loc.length>2048&&r.push(`URL at entry ${n+1} exceeds 2048 character limit`);});let i=new Map;return e.entries.forEach(s=>{let n=i.get(s.loc)??0;i.set(s.loc,n+1);}),i.forEach((s,n)=>{s>1&&t.push(`Duplicate URL found: ${n} (appears ${s} times)`);}),{valid:t.length===0,errors:t,warnings:r}}function P(e){return e.length===0?new Date().toISOString():e.reduce((r,i)=>{let s=new Date(i.updatedAt);return s>r?s:r},new Date(e[0].updatedAt)).toISOString()}function Y(e,t,r=[],i={}){return async function(){let l=(await U(e,t,r,i)).map(a=>({loc:a.url,lastmod:typeof a.lastModified=="string"?a.lastModified:a.lastModified?.toISOString(),changefreq:a.changeFrequency,priority:a.priority})),u={entries:l,totalEntries:l.length,generatedAt:new Date().toISOString(),baseUrl:w(i.canonical?.domain??e.domain,i.canonical?.forceHttps??true)},c=L(u);return new Response(c,{headers:{"Content-Type":"application/xml","Cache-Control":"public, max-age=3600, s-maxage=3600"}})}}function K(e){return async function(){let r=[...e.staticRoutes??[],...e.getStaticRoutes?await e.getStaticRoutes():[]],i=await e.getSite(),s=await e.getArticles(),n=e.getAuthors?await e.getAuthors():[];return [...r,...await U(i,s,n,e.options)]}}function x(e){let t={"&":"&","<":"<",">":">",'"':""","'":"'"};return e.replace(/[&<>"']/g,r=>t[r]??r)}function C(e){try{let t=new Date(e),r=t.toISOString(),i=e.endsWith("Z")&&!e.includes(".")?e.replace("Z",".000Z"):e;return r===i&&!isNaN(t.getTime())}catch{return false}}export{Y as createBlogSitemapRoute,K as createDynamicBlogSitemap,U as generateBlogSitemapEntries,X as generateSitemap,J as generateSitemapIndex,L as sitemapToXml,Q as validateSitemap};//# sourceMappingURL=sitemap.mjs.map
|
|
6
6
|
//# sourceMappingURL=sitemap.mjs.map
|
package/dist/types/contracts.ts
CHANGED
|
@@ -323,3 +323,24 @@ export interface UnpublishPayload {
|
|
|
323
323
|
/** Whether this is a dry run (validation only) */
|
|
324
324
|
readonly dryRun?: boolean;
|
|
325
325
|
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Search-engine submission settings synced from Bloggy core to the downstream
|
|
329
|
+
* blog platform.
|
|
330
|
+
*/
|
|
331
|
+
export interface SearchSubmissionConfig {
|
|
332
|
+
readonly indexNow?: {
|
|
333
|
+
readonly enabled: boolean;
|
|
334
|
+
readonly key: string;
|
|
335
|
+
readonly keyLocation: string;
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Versioned platform configuration payload accepted by the blog platform
|
|
341
|
+
* configuration endpoint.
|
|
342
|
+
*/
|
|
343
|
+
export interface BlogPlatformConfigPayload {
|
|
344
|
+
readonly version: 1;
|
|
345
|
+
readonly searchSubmission?: SearchSubmissionConfig;
|
|
346
|
+
}
|