mdorigin 0.4.1 → 0.5.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.
@@ -1,8 +1,9 @@
1
1
  import path from 'node:path';
2
2
  import { isIgnoredContentName } from './content-store.js';
3
- import { inferDirectoryContentType } from './content-type.js';
3
+ import { inferDirectoryContentType, resolveContentType, } from './content-type.js';
4
4
  import { getDirectoryIndexCandidates } from './directory-index.js';
5
- import { extractManagedIndexEntries, getDocumentSummary, getDocumentTitle as getParsedDocumentTitle, parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, } from './markdown.js';
5
+ import { extractManagedIndexEntries, getDocumentSummary, getDocumentTitle as getParsedDocumentTitle, parseMarkdownDocument, stripManagedIndexBlock, stripManagedIndexLinks, stripMachineOnlyMarkdownComments, } from './markdown.js';
6
+ import { ensureTrailingSlash, trimLeadingSlash } from './site-url.js';
6
7
  import { applyIndexTransforms, renderFooterOverride, renderHeaderOverride, renderPageWithPlugins, transformHtmlWithPlugins, } from './extensions.js';
7
8
  import { handleApiRoute } from './api.js';
8
9
  import { normalizeRequestPath, resolveRequest } from './router.js';
@@ -21,6 +22,9 @@ export async function handleSiteRequest(store, pathname, options) {
21
22
  if (pathname === '/sitemap.xml') {
22
23
  return renderSitemap(store, options);
23
24
  }
25
+ if (pathname === '/feed.xml') {
26
+ return renderRssFeed(store, options);
27
+ }
24
28
  const resolved = resolveRequest(pathname);
25
29
  const listingFragmentRequest = getListingFragmentRequest(options.searchParams);
26
30
  const negotiatedMarkdown = shouldServeMarkdownForRequest(resolved, options.acceptHeader);
@@ -29,7 +33,7 @@ export async function handleSiteRequest(store, pathname, options) {
29
33
  if (aliasRedirect !== null) {
30
34
  return aliasRedirect;
31
35
  }
32
- return notFound();
36
+ return renderNotFoundForRequest(store, pathname, options);
33
37
  }
34
38
  const entry = await store.get(resolved.sourcePath);
35
39
  if (entry === null) {
@@ -45,6 +49,10 @@ export async function handleSiteRequest(store, pathname, options) {
45
49
  if (alternateMarkdownRedirect !== null) {
46
50
  return alternateMarkdownRedirect;
47
51
  }
52
+ const canonicalDirectoryRedirect = await tryRedirectCanonicalDirectoryPath(store, resolved);
53
+ if (canonicalDirectoryRedirect !== null) {
54
+ return canonicalDirectoryRedirect;
55
+ }
48
56
  if (resolved.kind === 'html' && resolved.requestPath.endsWith('/')) {
49
57
  const directoryIndexResponse = await tryRenderAlternateDirectoryIndex(store, resolved.requestPath, options);
50
58
  if (directoryIndexResponse !== null) {
@@ -52,18 +60,18 @@ export async function handleSiteRequest(store, pathname, options) {
52
60
  }
53
61
  return renderDirectoryListing(store, resolved.requestPath, options.siteConfig, searchEnabled);
54
62
  }
55
- return notFound();
63
+ return renderNotFoundForResolvedRequest(store, resolved, options, negotiatedMarkdown);
56
64
  }
57
65
  if (resolved.kind === 'asset') {
58
66
  return serveAsset(entry);
59
67
  }
60
68
  if (entry.kind !== 'text' || entry.text === undefined) {
61
- return notFound();
69
+ return renderNotFoundForResolvedRequest(store, resolved, options, negotiatedMarkdown);
62
70
  }
63
71
  if (resolved.kind === 'markdown' || negotiatedMarkdown) {
64
72
  const parsed = await parseMarkdownDocument(resolved.sourcePath, entry.text);
65
73
  if (parsed.meta.draft === true && options.draftMode === 'exclude') {
66
- return notFound();
74
+ return renderNotFoundForResolvedRequest(store, resolved, options, negotiatedMarkdown);
67
75
  }
68
76
  return {
69
77
  status: 200,
@@ -75,7 +83,7 @@ export async function handleSiteRequest(store, pathname, options) {
75
83
  }
76
84
  const parsed = await parseMarkdownDocument(resolved.sourcePath, entry.text);
77
85
  if (parsed.meta.draft === true && options.draftMode === 'exclude') {
78
- return notFound();
86
+ return renderNotFoundForResolvedRequest(store, resolved, options, negotiatedMarkdown);
79
87
  }
80
88
  const navigation = await resolveTopNav(store, options.siteConfig);
81
89
  const renderedBody = isRootHomeRequest(resolved.requestPath) && !options.siteConfig.showHomeIndex
@@ -214,6 +222,7 @@ async function renderStructuredPage(options) {
214
222
  stylesheetContent: currentPage.stylesheetContent,
215
223
  canonicalPath: currentPage.canonicalPath,
216
224
  alternateMarkdownPath: currentPage.alternateMarkdownPath,
225
+ rssFeedUrl: getRssFeedUrl(currentPage.siteUrl, options.siteConfig),
217
226
  listingEntries: currentPage.listingEntries,
218
227
  listingRequestPath: currentPage.listingRequestPath,
219
228
  listingInitialPostCount: currentPage.listingInitialPostCount,
@@ -281,6 +290,60 @@ function notFound() {
281
290
  body: 'Not Found',
282
291
  };
283
292
  }
293
+ async function renderNotFoundForRequest(store, pathname, options) {
294
+ const resolved = resolveRequest(pathname);
295
+ return renderNotFoundForResolvedRequest(store, resolved, options, false);
296
+ }
297
+ async function renderNotFoundForResolvedRequest(store, resolved, options, negotiatedMarkdown) {
298
+ const varyOnAccept = shouldVaryOnAccept(resolved);
299
+ if (resolved.kind !== 'html' || negotiatedMarkdown) {
300
+ return withNotFoundVary(varyOnAccept);
301
+ }
302
+ return renderHtmlNotFound(store, resolved.requestPath, options, varyOnAccept);
303
+ }
304
+ async function renderHtmlNotFound(store, requestPath, options, varyOnAccept) {
305
+ const navigation = await resolveTopNav(store, options.siteConfig);
306
+ const body = [
307
+ '<h1>Not Found</h1>',
308
+ `<p>No page was published at <code>${escapeHtml(requestPath)}</code>.</p>`,
309
+ ].join('');
310
+ return {
311
+ status: 404,
312
+ headers: withVaryAcceptIfNeeded({
313
+ 'content-type': 'text/html; charset=utf-8',
314
+ }, varyOnAccept),
315
+ body: renderDocument({
316
+ siteTitle: options.siteConfig.siteTitle,
317
+ siteDescription: options.siteConfig.siteDescription,
318
+ siteUrl: options.siteConfig.siteUrl,
319
+ favicon: options.siteConfig.favicon,
320
+ socialImage: options.siteConfig.socialImage,
321
+ logo: options.siteConfig.logo,
322
+ title: 'Not Found',
323
+ body,
324
+ showSummary: false,
325
+ showDate: false,
326
+ topNav: navigation.items,
327
+ footerNav: options.siteConfig.footerNav,
328
+ footerText: options.siteConfig.footerText,
329
+ socialLinks: options.siteConfig.socialLinks,
330
+ stylesheetContent: options.siteConfig.stylesheetContent,
331
+ rssFeedUrl: getRssFeedUrl(options.siteConfig.siteUrl, options.siteConfig),
332
+ searchEnabled: options.searchApi !== undefined,
333
+ }),
334
+ };
335
+ }
336
+ function withNotFoundVary(varyOnAccept) {
337
+ if (!varyOnAccept) {
338
+ return notFound();
339
+ }
340
+ return {
341
+ ...notFound(),
342
+ headers: withVaryAcceptIfNeeded({
343
+ 'content-type': 'text/plain; charset=utf-8',
344
+ }, true),
345
+ };
346
+ }
284
347
  function redirect(location) {
285
348
  return {
286
349
  status: 308,
@@ -317,6 +380,59 @@ async function renderSitemap(store, options) {
317
380
  body,
318
381
  };
319
382
  }
383
+ async function renderRssFeed(store, options) {
384
+ if (!isRssEnabled(options.siteConfig) || !options.siteConfig.siteUrl) {
385
+ return notFound();
386
+ }
387
+ const items = await collectRssFeedItems(store, '', options);
388
+ const limitedItems = items.slice(0, options.siteConfig.rss?.maxItems ?? 20);
389
+ const rssFeedUrl = getRssFeedUrl(options.siteConfig.siteUrl, options.siteConfig);
390
+ const title = options.siteConfig.rss?.title ?? options.siteConfig.siteTitle;
391
+ const description = options.siteConfig.rss?.description ?? options.siteConfig.siteDescription;
392
+ const lastBuildDate = limitedItems[0]?.pubDate.toUTCString();
393
+ const body = [
394
+ '<?xml version="1.0" encoding="UTF-8"?>',
395
+ '<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">',
396
+ '<channel>',
397
+ ` <title>${escapeHtml(title)}</title>`,
398
+ ` <link>${escapeHtml(options.siteConfig.siteUrl)}</link>`,
399
+ description
400
+ ? ` <description>${escapeHtml(description)}</description>`
401
+ : ' <description></description>',
402
+ ' <generator>mdorigin</generator>',
403
+ rssFeedUrl
404
+ ? ` <atom:link href="${escapeHtml(rssFeedUrl)}" rel="self" type="application/rss+xml" />`
405
+ : '',
406
+ options.siteConfig.rss?.author
407
+ ? ` <managingEditor>${escapeHtml(options.siteConfig.rss.author)}</managingEditor>`
408
+ : '',
409
+ lastBuildDate ? ` <lastBuildDate>${escapeHtml(lastBuildDate)}</lastBuildDate>` : '',
410
+ ...limitedItems.map((item) => [
411
+ ' <item>',
412
+ ` <title>${escapeHtml(item.title)}</title>`,
413
+ ` <link>${escapeHtml(item.absoluteUrl)}</link>`,
414
+ ` <guid isPermaLink="true">${escapeHtml(item.absoluteUrl)}</guid>`,
415
+ ` <pubDate>${escapeHtml(item.pubDate.toUTCString())}</pubDate>`,
416
+ item.summary
417
+ ? ` <description>${escapeHtml(item.summary)}</description>`
418
+ : '',
419
+ ' </item>',
420
+ ]
421
+ .filter((line) => line !== '')
422
+ .join('\n')),
423
+ '</channel>',
424
+ '</rss>',
425
+ ]
426
+ .filter((line) => line !== '')
427
+ .join('\n');
428
+ return {
429
+ status: 200,
430
+ headers: {
431
+ 'content-type': 'application/rss+xml; charset=utf-8',
432
+ },
433
+ body,
434
+ };
435
+ }
320
436
  function withVaryAcceptIfNeeded(headers, enabled) {
321
437
  if (!enabled) {
322
438
  return headers;
@@ -387,6 +503,74 @@ function dedupeSitemapEntries(entries) {
387
503
  }
388
504
  return Array.from(deduped.values());
389
505
  }
506
+ async function collectRssFeedItems(store, directoryPath, options) {
507
+ const entries = await store.listDirectory(directoryPath);
508
+ if (entries === null) {
509
+ return [];
510
+ }
511
+ const feedItems = [];
512
+ const directoryShape = inspectDirectoryShapeEntries(entries);
513
+ for (const entry of entries) {
514
+ if (entry.kind === 'directory') {
515
+ feedItems.push(...(await collectRssFeedItems(store, entry.path, options)));
516
+ continue;
517
+ }
518
+ if (!isMarkdownEntry(entry)) {
519
+ continue;
520
+ }
521
+ const document = await store.get(entry.path);
522
+ if (document === null || document.kind !== 'text' || document.text === undefined) {
523
+ continue;
524
+ }
525
+ const parsed = await parseMarkdownDocument(entry.path, document.text);
526
+ if (parsed.meta.draft === true && options.draftMode === 'exclude') {
527
+ continue;
528
+ }
529
+ const pubDate = parseFeedDate(parsed.meta.date);
530
+ if (pubDate === null) {
531
+ continue;
532
+ }
533
+ const contentType = inferFeedContentType(entry.path, parsed.meta, directoryShape);
534
+ if (contentType !== 'post') {
535
+ continue;
536
+ }
537
+ const canonicalPath = getCanonicalHtmlPathForContentPath(entry.path);
538
+ feedItems.push({
539
+ title: getDocumentTitle(parsed),
540
+ canonicalPath,
541
+ absoluteUrl: new URL(trimLeadingSlash(canonicalPath), ensureTrailingSlash(options.siteConfig.siteUrl ?? '')).toString(),
542
+ summary: getFeedSummary(parsed),
543
+ pubDate,
544
+ });
545
+ }
546
+ feedItems.sort((left, right) => {
547
+ const timeDelta = right.pubDate.getTime() - left.pubDate.getTime();
548
+ return timeDelta !== 0
549
+ ? timeDelta
550
+ : left.canonicalPath.localeCompare(right.canonicalPath);
551
+ });
552
+ return feedItems;
553
+ }
554
+ function inferFeedContentType(contentPath, meta, directoryShape) {
555
+ const explicitType = resolveContentType(meta);
556
+ if (explicitType) {
557
+ return explicitType;
558
+ }
559
+ if (isDirectoryIndexContentPath(contentPath)) {
560
+ return inferDirectoryContentType(meta, directoryShape);
561
+ }
562
+ return typeof meta.date === 'string' && meta.date !== '' ? 'post' : 'page';
563
+ }
564
+ function getFeedSummary(parsed) {
565
+ return getDocumentSummary(parsed.meta, stripMachineOnlyMarkdownComments(stripManagedIndexBlock(parsed.body)));
566
+ }
567
+ function parseFeedDate(value) {
568
+ if (typeof value !== 'string' || value.trim() === '') {
569
+ return null;
570
+ }
571
+ const parsed = new Date(value);
572
+ return Number.isNaN(parsed.getTime()) ? null : parsed;
573
+ }
390
574
  async function renderDirectoryListing(store, requestPath, siteConfig, searchEnabled) {
391
575
  const directoryPath = requestPath === '/' ? '' : requestPath.slice(1).replace(/\/$/, '');
392
576
  const entries = await store.listDirectory(directoryPath);
@@ -424,6 +608,7 @@ async function renderDirectoryListing(store, requestPath, siteConfig, searchEnab
424
608
  stylesheetContent: siteConfig.stylesheetContent,
425
609
  canonicalPath: requestPath,
426
610
  alternateMarkdownPath: getMarkdownRequestPathForContentPath(getDirectoryIndexContentPathForRequestPath(requestPath)),
611
+ rssFeedUrl: getRssFeedUrl(siteConfig.siteUrl, siteConfig),
427
612
  searchEnabled,
428
613
  }),
429
614
  };
@@ -558,6 +743,23 @@ async function tryRedirectAlternateDirectoryMarkdown(store, resolved, options) {
558
743
  }
559
744
  return null;
560
745
  }
746
+ async function tryRedirectCanonicalDirectoryPath(store, resolved) {
747
+ if (resolved.kind !== 'html' || resolved.requestPath.endsWith('/')) {
748
+ return null;
749
+ }
750
+ if (path.posix.extname(resolved.requestPath) !== '') {
751
+ return null;
752
+ }
753
+ const directoryPath = resolved.requestPath.slice(1);
754
+ if (directoryPath === '') {
755
+ return null;
756
+ }
757
+ const directoryEntries = await store.listDirectory(directoryPath);
758
+ if (directoryEntries === null) {
759
+ return null;
760
+ }
761
+ return redirect(`${resolved.requestPath}/`);
762
+ }
561
763
  function getMarkdownRequestPathForContentPath(contentPath) {
562
764
  return `/${contentPath}`;
563
765
  }
@@ -628,6 +830,10 @@ async function findAliasRedirectLocation(store, directoryPath, requestPath, opti
628
830
  function isMarkdownEntry(entry) {
629
831
  return path.posix.extname(entry.name).toLowerCase() === '.md';
630
832
  }
833
+ function isDirectoryIndexContentPath(contentPath) {
834
+ const basename = path.posix.basename(contentPath).toLowerCase();
835
+ return basename === 'index.md' || basename === 'readme.md' || basename === 'skill.md';
836
+ }
631
837
  function normalizeAliases(aliases) {
632
838
  if (!Array.isArray(aliases)) {
633
839
  return [];
@@ -642,12 +848,23 @@ function normalizeAliases(aliases) {
642
848
  }
643
849
  function getCanonicalHtmlPathForContentPath(contentPath) {
644
850
  const basename = path.posix.basename(contentPath).toLowerCase();
645
- if (basename === 'index.md' || basename === 'readme.md') {
851
+ if (basename === 'index.md' ||
852
+ basename === 'readme.md' ||
853
+ basename === 'skill.md') {
646
854
  const directory = path.posix.dirname(contentPath);
647
855
  return directory === '.' ? '/' : `/${directory}/`;
648
856
  }
649
857
  return `/${contentPath.slice(0, -'.md'.length)}`;
650
858
  }
859
+ function isRssEnabled(siteConfig) {
860
+ return Boolean(siteConfig.siteUrl) && (siteConfig.rss?.enabled ?? true);
861
+ }
862
+ function getRssFeedUrl(siteUrl, siteConfig) {
863
+ if (!siteUrl || !isRssEnabled(siteConfig)) {
864
+ return undefined;
865
+ }
866
+ return new URL('feed.xml', ensureTrailingSlash(siteUrl)).toString();
867
+ }
651
868
  function getEditLinkHref(siteConfig, sourcePath) {
652
869
  if (!siteConfig.editLink || !sourcePath) {
653
870
  return undefined;
@@ -731,6 +948,9 @@ async function inspectDirectoryShape(store, directoryPath) {
731
948
  hasAssetFiles: false,
732
949
  };
733
950
  }
951
+ return inspectDirectoryShapeEntries(entries);
952
+ }
953
+ function inspectDirectoryShapeEntries(entries) {
734
954
  let hasSkillIndex = false;
735
955
  let hasChildDirectories = false;
736
956
  let hasExtraMarkdownFiles = false;
@@ -17,6 +17,19 @@ export interface SiteSocialLink {
17
17
  export interface EditLinkConfig {
18
18
  baseUrl: string;
19
19
  }
20
+ export interface SiteRssConfigInput {
21
+ title?: string;
22
+ description?: string;
23
+ author?: string;
24
+ maxItems?: number;
25
+ }
26
+ export interface SiteRssConfig {
27
+ enabled: boolean;
28
+ title?: string;
29
+ description?: string;
30
+ author?: string;
31
+ maxItems: number;
32
+ }
20
33
  export interface SiteSearchRerankerConfig {
21
34
  kind?: 'embedding-v1' | 'heuristic-v1';
22
35
  candidatePoolSize?: number;
@@ -63,6 +76,7 @@ export interface SiteConfig {
63
76
  footerText?: string;
64
77
  socialLinks?: SiteSocialLink[];
65
78
  editLink?: EditLinkConfig;
79
+ rss?: false | SiteRssConfigInput;
66
80
  showHomeIndex?: boolean;
67
81
  listingInitialPostCount?: number;
68
82
  listingLoadMoreStep?: number;
@@ -85,6 +99,7 @@ export interface ResolvedSiteConfig {
85
99
  footerText?: string;
86
100
  socialLinks: SiteSocialLink[];
87
101
  editLink?: EditLinkConfig;
102
+ rss?: SiteRssConfig;
88
103
  showHomeIndex: boolean;
89
104
  listingInitialPostCount: number;
90
105
  listingLoadMoreStep: number;
@@ -42,6 +42,7 @@ export async function loadUserSiteConfig(options = {}) {
42
42
  : undefined,
43
43
  socialLinks: normalizeSocialLinks(parsedConfig.socialLinks),
44
44
  editLink: normalizeEditLink(parsedConfig.editLink),
45
+ rss: normalizeRssConfig(parsedConfig.rss),
45
46
  showHomeIndex: typeof parsedConfig.showHomeIndex === 'boolean'
46
47
  ? parsedConfig.showHomeIndex
47
48
  : normalizeTopNav(parsedConfig.topNav).length === 0,
@@ -222,6 +223,36 @@ function normalizeOptionalNumber(value) {
222
223
  }
223
224
  return undefined;
224
225
  }
226
+ function normalizeRssConfig(value) {
227
+ if (value === false) {
228
+ return {
229
+ enabled: false,
230
+ title: undefined,
231
+ description: undefined,
232
+ author: undefined,
233
+ maxItems: 20,
234
+ };
235
+ }
236
+ if (typeof value !== 'object' || value === null) {
237
+ return {
238
+ enabled: true,
239
+ title: undefined,
240
+ description: undefined,
241
+ author: undefined,
242
+ maxItems: 20,
243
+ };
244
+ }
245
+ const rss = value;
246
+ return {
247
+ enabled: true,
248
+ title: typeof rss.title === 'string' && rss.title !== '' ? rss.title : undefined,
249
+ description: typeof rss.description === 'string' && rss.description !== ''
250
+ ? rss.description
251
+ : undefined,
252
+ author: typeof rss.author === 'string' && rss.author !== '' ? rss.author : undefined,
253
+ maxItems: normalizePositiveInteger(rss.maxItems, 20),
254
+ };
255
+ }
225
256
  function normalizeSearchConfig(value, configFilePath) {
226
257
  if (typeof value !== 'object' || value === null) {
227
258
  return undefined;
@@ -0,0 +1,2 @@
1
+ export declare function trimLeadingSlash(value: string): string;
2
+ export declare function ensureTrailingSlash(value: string): string;
@@ -0,0 +1,6 @@
1
+ export function trimLeadingSlash(value) {
2
+ return value.startsWith('/') ? value.slice(1) : value;
3
+ }
4
+ export function ensureTrailingSlash(value) {
5
+ return value.endsWith('/') ? value : `${value}/`;
6
+ }
@@ -21,6 +21,7 @@ export interface RenderDocumentOptions {
21
21
  stylesheetContent?: string;
22
22
  canonicalPath?: string;
23
23
  alternateMarkdownPath?: string;
24
+ rssFeedUrl?: string;
24
25
  listingEntries?: ManagedIndexEntry[];
25
26
  listingRequestPath?: string;
26
27
  listingInitialPostCount?: number;
@@ -25,6 +25,9 @@ export function renderDocument(options) {
25
25
  const alternateMarkdownMeta = options.alternateMarkdownPath
26
26
  ? `<link rel="alternate" type="text/markdown" href="${escapeHtml(options.alternateMarkdownPath)}">`
27
27
  : '';
28
+ const rssMeta = options.rssFeedUrl
29
+ ? `<link rel="alternate" type="application/rss+xml" title="${siteTitle}" href="${escapeHtml(options.rssFeedUrl)}">`
30
+ : '';
28
31
  const stylesheetBlock = `<style>${getDefaultThemeStyles()}${options.stylesheetContent ? `\n${options.stylesheetContent}` : ''}</style>`;
29
32
  const navBlock = options.topNav && options.topNav.length > 0
30
33
  ? `<nav class="site-nav"><ul>${options.topNav
@@ -107,6 +110,7 @@ export function renderDocument(options) {
107
110
  faviconMeta,
108
111
  socialImageMeta,
109
112
  alternateMarkdownMeta,
113
+ rssMeta,
110
114
  stylesheetBlock,
111
115
  '</head>',
112
116
  '<body>',
package/dist/search.js CHANGED
@@ -4,6 +4,7 @@ import { inferDirectoryContentType } from './core/content-type.js';
4
4
  import { getDirectoryIndexCandidates } from './core/directory-index.js';
5
5
  import { getDocumentSummary, getDocumentTitle, parseMarkdownDocument, stripMachineOnlyMarkdownComments, stripManagedIndexBlock, } from './core/markdown.js';
6
6
  import { isIgnoredContentName } from './core/content-store.js';
7
+ import { ensureTrailingSlash, trimLeadingSlash } from './core/site-url.js';
7
8
  const OVERVIEW_CONTENT_FILENAMES = new Set(['readme.md', 'index.md', 'skill.md']);
8
9
  export async function buildSearchBundle(options) {
9
10
  const buildModule = await loadIndexbindBuildModule();
@@ -276,12 +277,6 @@ function getCanonicalHtmlPathForContentPath(contentPath) {
276
277
  }
277
278
  return `/${contentPath.slice(0, -'.md'.length)}`;
278
279
  }
279
- function trimLeadingSlash(value) {
280
- return value.startsWith('/') ? value.slice(1) : value;
281
- }
282
- function ensureTrailingSlash(value) {
283
- return value.endsWith('/') ? value : `${value}/`;
284
- }
285
280
  async function pathExists(filePath) {
286
281
  try {
287
282
  await stat(filePath);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mdorigin",
3
- "version": "0.4.1",
3
+ "version": "0.5.0",
4
4
  "type": "module",
5
5
  "description": "Markdown-first publishing for humans and agents.",
6
6
  "repository": {