mdorigin 0.1.1 → 0.1.2

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.
@@ -0,0 +1,160 @@
1
+ export async function handleApiRoute(pathname, searchParams, options) {
2
+ if (pathname === '/api/openapi.json') {
3
+ return json(200, buildOpenApiDocument(options));
4
+ }
5
+ if (pathname === '/api/search') {
6
+ if (!options.searchApi) {
7
+ return json(404, { error: 'search is not enabled for this site' });
8
+ }
9
+ const query = searchParams?.get('q')?.trim() ?? '';
10
+ if (query === '') {
11
+ return json(400, {
12
+ error: 'missing required query parameter: q',
13
+ });
14
+ }
15
+ const topK = normalizePositiveInteger(searchParams?.get('topK')) ?? 10;
16
+ const hits = await options.searchApi.search(query, { topK });
17
+ return json(200, {
18
+ query,
19
+ topK,
20
+ count: hits.length,
21
+ hits: hits.map(serializeSearchHit),
22
+ });
23
+ }
24
+ return null;
25
+ }
26
+ function buildOpenApiDocument(options) {
27
+ return {
28
+ openapi: '3.1.0',
29
+ info: {
30
+ title: `${options.siteConfig.siteTitle} API`,
31
+ version: '1.0.0',
32
+ description: 'Search API for a mdorigin site.',
33
+ },
34
+ servers: options.siteConfig.siteUrl
35
+ ? [{ url: options.siteConfig.siteUrl }]
36
+ : options.requestUrl
37
+ ? [{ url: new URL(options.requestUrl).origin }]
38
+ : undefined,
39
+ paths: {
40
+ '/api/search': {
41
+ get: {
42
+ operationId: 'searchSite',
43
+ summary: 'Search published site content',
44
+ parameters: [
45
+ {
46
+ name: 'q',
47
+ in: 'query',
48
+ required: true,
49
+ schema: { type: 'string' },
50
+ description: 'Search query string.',
51
+ },
52
+ {
53
+ name: 'topK',
54
+ in: 'query',
55
+ required: false,
56
+ schema: { type: 'integer', minimum: 1, default: 10 },
57
+ description: 'Maximum number of hits to return.',
58
+ },
59
+ ],
60
+ responses: {
61
+ '200': {
62
+ description: 'Search results.',
63
+ content: {
64
+ 'application/json': {
65
+ schema: {
66
+ type: 'object',
67
+ required: ['query', 'topK', 'count', 'hits'],
68
+ properties: {
69
+ query: { type: 'string' },
70
+ topK: { type: 'integer' },
71
+ count: { type: 'integer' },
72
+ hits: {
73
+ type: 'array',
74
+ items: {
75
+ type: 'object',
76
+ required: [
77
+ 'docId',
78
+ 'relativePath',
79
+ 'score',
80
+ 'metadata',
81
+ 'bestMatch',
82
+ ],
83
+ properties: {
84
+ docId: { type: 'string' },
85
+ relativePath: { type: 'string' },
86
+ canonicalUrl: { type: 'string' },
87
+ title: { type: 'string' },
88
+ summary: { type: 'string' },
89
+ score: { type: 'number' },
90
+ metadata: { type: 'object', additionalProperties: true },
91
+ bestMatch: {
92
+ type: 'object',
93
+ required: [
94
+ 'chunkId',
95
+ 'excerpt',
96
+ 'headingPath',
97
+ 'charStart',
98
+ 'charEnd',
99
+ 'score',
100
+ ],
101
+ properties: {
102
+ chunkId: { type: 'number' },
103
+ excerpt: { type: 'string' },
104
+ headingPath: {
105
+ type: 'array',
106
+ items: { type: 'string' },
107
+ },
108
+ charStart: { type: 'integer' },
109
+ charEnd: { type: 'integer' },
110
+ score: { type: 'number' },
111
+ },
112
+ },
113
+ },
114
+ },
115
+ },
116
+ },
117
+ },
118
+ },
119
+ },
120
+ },
121
+ '400': {
122
+ description: 'Invalid request.',
123
+ },
124
+ '404': {
125
+ description: 'Search API not enabled.',
126
+ },
127
+ },
128
+ },
129
+ },
130
+ },
131
+ };
132
+ }
133
+ function serializeSearchHit(hit) {
134
+ return {
135
+ docId: hit.docId,
136
+ relativePath: hit.relativePath,
137
+ canonicalUrl: hit.canonicalUrl,
138
+ title: hit.title,
139
+ summary: hit.summary,
140
+ metadata: hit.metadata,
141
+ score: hit.score,
142
+ bestMatch: hit.bestMatch,
143
+ };
144
+ }
145
+ function normalizePositiveInteger(value) {
146
+ if (!value) {
147
+ return null;
148
+ }
149
+ const parsed = Number.parseInt(value, 10);
150
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
151
+ }
152
+ function json(status, body) {
153
+ return {
154
+ status,
155
+ headers: {
156
+ 'content-type': 'application/json; charset=utf-8',
157
+ },
158
+ body: JSON.stringify(body, null, 2),
159
+ };
160
+ }
@@ -54,10 +54,15 @@ const MEDIA_TYPES = new Map([
54
54
  ['.json', 'application/json; charset=utf-8'],
55
55
  ['.md', 'text/markdown; charset=utf-8'],
56
56
  ['.pdf', 'application/pdf'],
57
+ ['.py', 'text/plain; charset=utf-8'],
57
58
  ['.png', 'image/png'],
59
+ ['.sh', 'text/plain; charset=utf-8'],
58
60
  ['.svg', 'image/svg+xml'],
59
61
  ['.txt', 'text/plain; charset=utf-8'],
62
+ ['.toml', 'text/plain; charset=utf-8'],
60
63
  ['.webp', 'image/webp'],
64
+ ['.yaml', 'text/plain; charset=utf-8'],
65
+ ['.yml', 'text/plain; charset=utf-8'],
61
66
  ]);
62
67
  export function getMediaTypeForPath(contentPath) {
63
68
  const extension = path.posix.extname(contentPath).toLowerCase();
@@ -1,6 +1,7 @@
1
1
  import type { ParsedDocumentMeta } from './markdown.js';
2
2
  export type ContentType = 'page' | 'post';
3
3
  export interface DirectoryShape {
4
+ hasSkillIndex: boolean;
4
5
  hasChildDirectories: boolean;
5
6
  hasExtraMarkdownFiles: boolean;
6
7
  hasAssetFiles: boolean;
@@ -9,6 +9,9 @@ export function inferDirectoryContentType(meta, shape) {
9
9
  if (typeof meta.date === 'string' && meta.date !== '') {
10
10
  return 'post';
11
11
  }
12
+ if (shape.hasSkillIndex) {
13
+ return 'post';
14
+ }
12
15
  if (shape.hasChildDirectories || shape.hasExtraMarkdownFiles) {
13
16
  return 'page';
14
17
  }
@@ -1,2 +1,2 @@
1
- export declare const DIRECTORY_INDEX_FILENAMES: readonly ["index.md", "README.md"];
1
+ export declare const DIRECTORY_INDEX_FILENAMES: readonly ["index.md", "README.md", "SKILL.md"];
2
2
  export declare function getDirectoryIndexCandidates(directoryPath: string): string[];
@@ -1,5 +1,9 @@
1
1
  import path from 'node:path';
2
- export const DIRECTORY_INDEX_FILENAMES = ['index.md', 'README.md'];
2
+ export const DIRECTORY_INDEX_FILENAMES = [
3
+ 'index.md',
4
+ 'README.md',
5
+ 'SKILL.md',
6
+ ];
3
7
  export function getDirectoryIndexCandidates(directoryPath) {
4
8
  return DIRECTORY_INDEX_FILENAMES.map((filename) => directoryPath === '' ? filename : path.posix.join(directoryPath, filename));
5
9
  }
@@ -1,7 +1,9 @@
1
1
  export interface ParsedDocumentMeta {
2
2
  title?: string;
3
+ name?: string;
3
4
  date?: string;
4
5
  summary?: string;
6
+ description?: string;
5
7
  draft?: boolean;
6
8
  type?: string;
7
9
  order?: number;
@@ -20,6 +22,8 @@ export interface ManagedIndexEntry {
20
22
  href: string;
21
23
  detail?: string;
22
24
  }
25
+ export declare function getDocumentTitle(meta: ParsedDocumentMeta, body: string, fallback: string): string;
26
+ export declare function getDocumentSummary(meta: ParsedDocumentMeta, body: string): string | undefined;
23
27
  export declare function parseMarkdownDocument(sourcePath: string, markdown: string): Promise<ParsedDocument>;
24
28
  export declare function renderMarkdown(markdown: string): Promise<string>;
25
29
  export declare function rewriteMarkdownLinksInHtml(html: string): string;
@@ -2,6 +2,15 @@ import matter from 'gray-matter';
2
2
  import { remark } from 'remark';
3
3
  import remarkGfm from 'remark-gfm';
4
4
  import remarkHtml from 'remark-html';
5
+ export function getDocumentTitle(meta, body, fallback) {
6
+ return (firstNonEmptyString(meta.title, meta.name) ??
7
+ extractFirstHeading(body) ??
8
+ fallback);
9
+ }
10
+ export function getDocumentSummary(meta, body) {
11
+ return (firstNonEmptyString(meta.summary, meta.description) ??
12
+ extractFirstParagraph(body));
13
+ }
5
14
  export async function parseMarkdownDocument(sourcePath, markdown) {
6
15
  const parsed = matter(markdown);
7
16
  const html = rewriteMarkdownLinksInHtml(await renderMarkdown(parsed.content));
@@ -83,6 +92,9 @@ function normalizeMeta(data) {
83
92
  if (typeof data.title === 'string') {
84
93
  meta.title = data.title;
85
94
  }
95
+ if (typeof data.name === 'string') {
96
+ meta.name = data.name;
97
+ }
86
98
  if (typeof data.date === 'string') {
87
99
  meta.date = data.date;
88
100
  }
@@ -92,6 +104,9 @@ function normalizeMeta(data) {
92
104
  if (typeof data.summary === 'string') {
93
105
  meta.summary = data.summary;
94
106
  }
107
+ if (typeof data.description === 'string') {
108
+ meta.description = data.description;
109
+ }
95
110
  if (typeof data.draft === 'boolean') {
96
111
  meta.draft = data.draft;
97
112
  }
@@ -143,6 +158,12 @@ function rewriteMarkdownPath(pathname) {
143
158
  if (pathname.toLowerCase() === 'readme.md') {
144
159
  return './';
145
160
  }
161
+ if (pathname.toLowerCase().endsWith('/skill.md')) {
162
+ return pathname.slice(0, -'SKILL.md'.length);
163
+ }
164
+ if (pathname.toLowerCase() === 'skill.md') {
165
+ return './';
166
+ }
146
167
  return pathname.slice(0, -'.md'.length);
147
168
  }
148
169
  function shouldPreserveHref(href) {
@@ -168,3 +189,35 @@ function normalizeManagedIndexHref(href) {
168
189
  }
169
190
  return href;
170
191
  }
192
+ function extractFirstHeading(markdown) {
193
+ for (const line of markdown.split('\n')) {
194
+ const trimmed = line.trim();
195
+ if (!trimmed.startsWith('#')) {
196
+ continue;
197
+ }
198
+ const heading = trimmed.replace(/^#+\s*/, '').trim();
199
+ if (heading !== '') {
200
+ return heading;
201
+ }
202
+ }
203
+ return undefined;
204
+ }
205
+ function extractFirstParagraph(markdown) {
206
+ const paragraphs = markdown
207
+ .split(/\n\s*\n/g)
208
+ .map((paragraph) => paragraph.trim())
209
+ .filter((paragraph) => paragraph !== '');
210
+ for (const paragraph of paragraphs) {
211
+ if (paragraph.startsWith('#') ||
212
+ paragraph.startsWith('<!--') ||
213
+ paragraph.startsWith('- ') ||
214
+ paragraph.startsWith('* ')) {
215
+ continue;
216
+ }
217
+ return paragraph.replace(/\s+/g, ' ');
218
+ }
219
+ return undefined;
220
+ }
221
+ function firstNonEmptyString(...values) {
222
+ return values.find((value) => typeof value === 'string' && value !== '');
223
+ }
@@ -1,9 +1,13 @@
1
1
  import type { ContentStore } from './content-store.js';
2
2
  import type { ResolvedSiteConfig } from './site-config.js';
3
+ import type { SearchApi } from '../search.js';
3
4
  export interface HandleSiteRequestOptions {
4
5
  draftMode: 'include' | 'exclude';
5
6
  siteConfig: ResolvedSiteConfig;
6
7
  acceptHeader?: string;
8
+ searchParams?: URLSearchParams;
9
+ requestUrl?: string;
10
+ searchApi?: SearchApi;
7
11
  }
8
12
  export interface SiteResponse {
9
13
  status: number;
@@ -1,14 +1,25 @@
1
1
  import path from 'node:path';
2
2
  import { inferDirectoryContentType } from './content-type.js';
3
3
  import { getDirectoryIndexCandidates } from './directory-index.js';
4
- import { extractManagedIndexEntries, parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, } from './markdown.js';
4
+ import { extractManagedIndexEntries, getDocumentSummary, getDocumentTitle as getParsedDocumentTitle, parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, } from './markdown.js';
5
+ import { handleApiRoute } from './api.js';
5
6
  import { normalizeRequestPath, resolveRequest } from './router.js';
6
- import { escapeHtml, renderDocument } from '../html/template.js';
7
+ import { escapeHtml, renderCatalogArticleItems, renderDocument, } from '../html/template.js';
7
8
  export async function handleSiteRequest(store, pathname, options) {
9
+ const searchEnabled = options.searchApi !== undefined;
10
+ const apiRoute = await handleApiRoute(pathname, options.searchParams, {
11
+ searchApi: options.searchApi,
12
+ siteConfig: options.siteConfig,
13
+ requestUrl: options.requestUrl,
14
+ });
15
+ if (apiRoute !== null) {
16
+ return apiRoute;
17
+ }
8
18
  if (pathname === '/sitemap.xml') {
9
19
  return renderSitemap(store, options);
10
20
  }
11
21
  const resolved = resolveRequest(pathname);
22
+ const catalogFragmentRequest = getCatalogFragmentRequest(options.searchParams);
12
23
  const negotiatedMarkdown = shouldServeMarkdownForRequest(resolved, options.acceptHeader);
13
24
  if (resolved.kind === 'not-found' || !resolved.sourcePath) {
14
25
  const aliasRedirect = await tryRedirectAlias(store, pathname, options);
@@ -36,7 +47,7 @@ export async function handleSiteRequest(store, pathname, options) {
36
47
  if (directoryIndexResponse !== null) {
37
48
  return directoryIndexResponse;
38
49
  }
39
- return renderDirectoryListing(store, resolved.requestPath, options.siteConfig);
50
+ return renderDirectoryListing(store, resolved.requestPath, options.siteConfig, searchEnabled);
40
51
  }
41
52
  return notFound();
42
53
  }
@@ -72,6 +83,10 @@ export async function handleSiteRequest(store, pathname, options) {
72
83
  const catalogEntries = options.siteConfig.template === 'catalog'
73
84
  ? extractManagedIndexEntries(renderedBody)
74
85
  : [];
86
+ if (catalogFragmentRequest !== null &&
87
+ options.siteConfig.template === 'catalog') {
88
+ return renderCatalogPostsFragment(catalogEntries, catalogFragmentRequest);
89
+ }
75
90
  const documentBody = options.siteConfig.template === 'catalog'
76
91
  ? stripManagedIndexBlock(renderedBody)
77
92
  : renderedBody;
@@ -91,7 +106,9 @@ export async function handleSiteRequest(store, pathname, options) {
91
106
  logo: options.siteConfig.logo,
92
107
  title: getDocumentTitle(parsed),
93
108
  body: renderedParsed.html,
94
- summary: options.siteConfig.showSummary === false ? undefined : parsed.meta.summary,
109
+ summary: options.siteConfig.showSummary === false
110
+ ? undefined
111
+ : getDocumentSummary(parsed.meta, parsed.body),
95
112
  date: options.siteConfig.showDate === false ? undefined : parsed.meta.date,
96
113
  showSummary: options.siteConfig.showSummary,
97
114
  showDate: options.siteConfig.showDate,
@@ -106,6 +123,51 @@ export async function handleSiteRequest(store, pathname, options) {
106
123
  canonicalPath: getCanonicalHtmlPathForContentPath(resolved.sourcePath),
107
124
  alternateMarkdownPath: getMarkdownRequestPathForContentPath(resolved.sourcePath),
108
125
  catalogEntries,
126
+ catalogRequestPath: resolved.requestPath,
127
+ catalogInitialPostCount: options.siteConfig.catalogInitialPostCount,
128
+ catalogLoadMoreStep: options.siteConfig.catalogLoadMoreStep,
129
+ searchEnabled,
130
+ }),
131
+ };
132
+ }
133
+ function getCatalogFragmentRequest(searchParams) {
134
+ if (searchParams?.get('catalog-format') !== 'posts') {
135
+ return null;
136
+ }
137
+ const offset = normalizeNonNegativeInteger(searchParams.get('catalog-offset'));
138
+ const limit = normalizePositiveInteger(searchParams.get('catalog-limit'));
139
+ if (offset === null || limit === null) {
140
+ return null;
141
+ }
142
+ return { offset, limit };
143
+ }
144
+ function normalizeNonNegativeInteger(value) {
145
+ if (value === null) {
146
+ return 0;
147
+ }
148
+ const parsed = Number.parseInt(value, 10);
149
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : null;
150
+ }
151
+ function normalizePositiveInteger(value) {
152
+ if (value === null) {
153
+ return null;
154
+ }
155
+ const parsed = Number.parseInt(value, 10);
156
+ return Number.isInteger(parsed) && parsed > 0 ? parsed : null;
157
+ }
158
+ function renderCatalogPostsFragment(entries, request) {
159
+ const articles = entries.filter((entry) => entry.kind === 'article');
160
+ const visibleArticles = articles.slice(request.offset, request.offset + request.limit);
161
+ const nextOffset = request.offset + visibleArticles.length;
162
+ return {
163
+ status: 200,
164
+ headers: {
165
+ 'content-type': 'application/json; charset=utf-8',
166
+ },
167
+ body: JSON.stringify({
168
+ itemsHtml: renderCatalogArticleItems(visibleArticles),
169
+ hasMore: nextOffset < articles.length,
170
+ nextOffset,
109
171
  }),
110
172
  };
111
173
  }
@@ -195,13 +257,11 @@ function appendVary(existing, value) {
195
257
  return `${existing}, ${value}`;
196
258
  }
197
259
  function getDocumentTitle(parsed) {
198
- if (parsed.meta.title) {
199
- return parsed.meta.title;
200
- }
201
260
  const basename = path.posix.basename(parsed.sourcePath, '.md');
202
- return basename === 'index'
261
+ const fallback = basename === 'index' || basename === 'README' || basename === 'SKILL'
203
262
  ? path.posix.basename(path.posix.dirname(parsed.sourcePath)) || 'mdorigin'
204
263
  : basename;
264
+ return getParsedDocumentTitle(parsed.meta, parsed.body, fallback);
205
265
  }
206
266
  async function collectSitemapEntries(store, directoryPath, options) {
207
267
  const entries = await store.listDirectory(directoryPath);
@@ -247,7 +307,7 @@ function dedupeSitemapEntries(entries) {
247
307
  }
248
308
  return Array.from(deduped.values());
249
309
  }
250
- async function renderDirectoryListing(store, requestPath, siteConfig) {
310
+ async function renderDirectoryListing(store, requestPath, siteConfig, searchEnabled) {
251
311
  const directoryPath = requestPath === '/' ? '' : requestPath.slice(1).replace(/\/$/, '');
252
312
  const entries = await store.listDirectory(directoryPath);
253
313
  if (entries === null) {
@@ -286,6 +346,7 @@ async function renderDirectoryListing(store, requestPath, siteConfig) {
286
346
  stylesheetContent: siteConfig.stylesheetContent,
287
347
  canonicalPath: requestPath,
288
348
  alternateMarkdownPath: getMarkdownRequestPathForContentPath(getDirectoryIndexContentPathForRequestPath(requestPath)),
349
+ searchEnabled,
289
350
  }),
290
351
  };
291
352
  }
@@ -338,6 +399,11 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
338
399
  const catalogEntries = options.siteConfig.template === 'catalog'
339
400
  ? extractManagedIndexEntries(renderedBody)
340
401
  : [];
402
+ const catalogFragmentRequest = getCatalogFragmentRequest(options.searchParams);
403
+ if (catalogFragmentRequest !== null &&
404
+ options.siteConfig.template === 'catalog') {
405
+ return renderCatalogPostsFragment(catalogEntries, catalogFragmentRequest);
406
+ }
341
407
  const documentBody = options.siteConfig.template === 'catalog'
342
408
  ? stripManagedIndexBlock(renderedBody)
343
409
  : renderedBody;
@@ -357,7 +423,9 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
357
423
  logo: options.siteConfig.logo,
358
424
  title: getDocumentTitle(parsed),
359
425
  body: renderedParsed.html,
360
- summary: options.siteConfig.showSummary === false ? undefined : parsed.meta.summary,
426
+ summary: options.siteConfig.showSummary === false
427
+ ? undefined
428
+ : getDocumentSummary(parsed.meta, parsed.body),
361
429
  date: options.siteConfig.showDate === false ? undefined : parsed.meta.date,
362
430
  showSummary: options.siteConfig.showSummary,
363
431
  showDate: options.siteConfig.showDate,
@@ -372,6 +440,10 @@ async function tryRenderAlternateDirectoryIndex(store, requestPath, options) {
372
440
  canonicalPath: requestPath,
373
441
  alternateMarkdownPath: getMarkdownRequestPathForContentPath(candidatePath),
374
442
  catalogEntries,
443
+ catalogRequestPath: requestPath,
444
+ catalogInitialPostCount: options.siteConfig.catalogInitialPostCount,
445
+ catalogLoadMoreStep: options.siteConfig.catalogLoadMoreStep,
446
+ searchEnabled: options.searchApi !== undefined,
375
447
  }),
376
448
  };
377
449
  }
@@ -585,9 +657,7 @@ async function resolveDirectoryNav(store, entry) {
585
657
  const parsed = await parseMarkdownDocument(candidatePath, contentEntry.text);
586
658
  const shape = await inspectDirectoryShape(store, entry.path);
587
659
  return {
588
- title: typeof parsed.meta.title === 'string' && parsed.meta.title !== ''
589
- ? parsed.meta.title
590
- : entry.name,
660
+ title: getParsedDocumentTitle(parsed.meta, parsed.body, entry.name),
591
661
  type: inferDirectoryContentType(parsed.meta, shape),
592
662
  order: parsed.meta.order,
593
663
  };
@@ -601,11 +671,13 @@ async function inspectDirectoryShape(store, directoryPath) {
601
671
  const entries = await store.listDirectory(directoryPath);
602
672
  if (entries === null) {
603
673
  return {
674
+ hasSkillIndex: false,
604
675
  hasChildDirectories: false,
605
676
  hasExtraMarkdownFiles: false,
606
677
  hasAssetFiles: false,
607
678
  };
608
679
  }
680
+ let hasSkillIndex = false;
609
681
  let hasChildDirectories = false;
610
682
  let hasExtraMarkdownFiles = false;
611
683
  let hasAssetFiles = false;
@@ -619,7 +691,10 @@ async function inspectDirectoryShape(store, directoryPath) {
619
691
  }
620
692
  const extension = path.posix.extname(entry.name).toLowerCase();
621
693
  if (extension === '.md') {
622
- if (entry.name !== 'index.md' && entry.name !== 'README.md') {
694
+ if (entry.name === 'SKILL.md') {
695
+ hasSkillIndex = true;
696
+ }
697
+ else if (entry.name !== 'index.md' && entry.name !== 'README.md') {
623
698
  hasExtraMarkdownFiles = true;
624
699
  }
625
700
  continue;
@@ -627,6 +702,7 @@ async function inspectDirectoryShape(store, directoryPath) {
627
702
  hasAssetFiles = true;
628
703
  }
629
704
  return {
705
+ hasSkillIndex,
630
706
  hasChildDirectories,
631
707
  hasExtraMarkdownFiles,
632
708
  hasAssetFiles,
@@ -35,6 +35,8 @@ export interface SiteConfig {
35
35
  socialLinks?: SiteSocialLink[];
36
36
  editLink?: EditLinkConfig;
37
37
  showHomeIndex?: boolean;
38
+ catalogInitialPostCount?: number;
39
+ catalogLoadMoreStep?: number;
38
40
  }
39
41
  export interface ResolvedSiteConfig {
40
42
  siteTitle: string;
@@ -52,6 +54,8 @@ export interface ResolvedSiteConfig {
52
54
  socialLinks: SiteSocialLink[];
53
55
  editLink?: EditLinkConfig;
54
56
  showHomeIndex: boolean;
57
+ catalogInitialPostCount: number;
58
+ catalogLoadMoreStep: number;
55
59
  stylesheetContent?: string;
56
60
  siteTitleConfigured: boolean;
57
61
  siteDescriptionConfigured: boolean;
@@ -49,6 +49,8 @@ export async function loadSiteConfig(options = {}) {
49
49
  showHomeIndex: typeof parsedConfig.showHomeIndex === 'boolean'
50
50
  ? parsedConfig.showHomeIndex
51
51
  : normalizeTopNav(parsedConfig.topNav).length === 0,
52
+ catalogInitialPostCount: normalizePositiveInteger(parsedConfig.catalogInitialPostCount, 10),
53
+ catalogLoadMoreStep: normalizePositiveInteger(parsedConfig.catalogLoadMoreStep, 10),
52
54
  stylesheetContent,
53
55
  siteTitleConfigured: typeof parsedConfig.siteTitle === 'string' && parsedConfig.siteTitle !== '',
54
56
  siteDescriptionConfigured: typeof parsedConfig.siteDescription === 'string' &&
@@ -131,6 +133,18 @@ function normalizeTopNav(value) {
131
133
  return [];
132
134
  });
133
135
  }
136
+ function normalizePositiveInteger(value, fallback) {
137
+ if (typeof value === 'number' && Number.isInteger(value) && value > 0) {
138
+ return value;
139
+ }
140
+ if (typeof value === 'string') {
141
+ const parsed = Number.parseInt(value, 10);
142
+ if (Number.isInteger(parsed) && parsed > 0) {
143
+ return parsed;
144
+ }
145
+ }
146
+ return fallback;
147
+ }
134
148
  function normalizeLogo(value) {
135
149
  if (typeof value !== 'object' ||
136
150
  value === null ||
@@ -25,6 +25,11 @@ export interface RenderDocumentOptions {
25
25
  canonicalPath?: string;
26
26
  alternateMarkdownPath?: string;
27
27
  catalogEntries?: ManagedIndexEntry[];
28
+ catalogRequestPath?: string;
29
+ catalogInitialPostCount?: number;
30
+ catalogLoadMoreStep?: number;
31
+ searchEnabled?: boolean;
28
32
  }
29
33
  export declare function renderDocument(options: RenderDocumentOptions): string;
30
34
  export declare function escapeHtml(value: string): string;
35
+ export declare function renderCatalogArticleItems(entries: readonly ManagedIndexEntry[]): string;