next-blogpanel 1.0.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.
Files changed (127) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +211 -0
  3. package/dist/admin/index.cjs +3062 -0
  4. package/dist/admin/index.cjs.map +1 -0
  5. package/dist/admin/index.d.cts +51 -0
  6. package/dist/admin/index.d.ts +51 -0
  7. package/dist/admin/index.js +3029 -0
  8. package/dist/admin/index.js.map +1 -0
  9. package/dist/api/categories.cjs +82 -0
  10. package/dist/api/categories.cjs.map +1 -0
  11. package/dist/api/categories.d.cts +27 -0
  12. package/dist/api/categories.d.ts +27 -0
  13. package/dist/api/categories.js +77 -0
  14. package/dist/api/categories.js.map +1 -0
  15. package/dist/api/media-file.cjs +46 -0
  16. package/dist/api/media-file.cjs.map +1 -0
  17. package/dist/api/media-file.d.cts +7 -0
  18. package/dist/api/media-file.d.ts +7 -0
  19. package/dist/api/media-file.js +44 -0
  20. package/dist/api/media-file.js.map +1 -0
  21. package/dist/api/media.cjs +180 -0
  22. package/dist/api/media.cjs.map +1 -0
  23. package/dist/api/media.d.cts +22 -0
  24. package/dist/api/media.d.ts +22 -0
  25. package/dist/api/media.js +176 -0
  26. package/dist/api/media.js.map +1 -0
  27. package/dist/api/posts.cjs +103 -0
  28. package/dist/api/posts.cjs.map +1 -0
  29. package/dist/api/posts.d.cts +27 -0
  30. package/dist/api/posts.d.ts +27 -0
  31. package/dist/api/posts.js +98 -0
  32. package/dist/api/posts.js.map +1 -0
  33. package/dist/api/rss.cjs +25 -0
  34. package/dist/api/rss.cjs.map +1 -0
  35. package/dist/api/rss.d.cts +5 -0
  36. package/dist/api/rss.d.ts +5 -0
  37. package/dist/api/rss.js +23 -0
  38. package/dist/api/rss.js.map +1 -0
  39. package/dist/api/settings.cjs +40 -0
  40. package/dist/api/settings.cjs.map +1 -0
  41. package/dist/api/settings.d.cts +17 -0
  42. package/dist/api/settings.d.ts +17 -0
  43. package/dist/api/settings.js +37 -0
  44. package/dist/api/settings.js.map +1 -0
  45. package/dist/api/sitemap.cjs +25 -0
  46. package/dist/api/sitemap.cjs.map +1 -0
  47. package/dist/api/sitemap.d.cts +5 -0
  48. package/dist/api/sitemap.d.ts +5 -0
  49. package/dist/api/sitemap.js +23 -0
  50. package/dist/api/sitemap.js.map +1 -0
  51. package/dist/api/tokens.cjs +56 -0
  52. package/dist/api/tokens.cjs.map +1 -0
  53. package/dist/api/tokens.d.cts +22 -0
  54. package/dist/api/tokens.d.ts +22 -0
  55. package/dist/api/tokens.js +52 -0
  56. package/dist/api/tokens.js.map +1 -0
  57. package/dist/chunk-2M46MMWN.cjs +76 -0
  58. package/dist/chunk-2M46MMWN.cjs.map +1 -0
  59. package/dist/chunk-4GK22Q3R.cjs +229 -0
  60. package/dist/chunk-4GK22Q3R.cjs.map +1 -0
  61. package/dist/chunk-6PZ772NQ.js +595 -0
  62. package/dist/chunk-6PZ772NQ.js.map +1 -0
  63. package/dist/chunk-BZXPRGLN.cjs +351 -0
  64. package/dist/chunk-BZXPRGLN.cjs.map +1 -0
  65. package/dist/chunk-DAX5C6SC.cjs +86 -0
  66. package/dist/chunk-DAX5C6SC.cjs.map +1 -0
  67. package/dist/chunk-DWNMHQ4I.cjs +57 -0
  68. package/dist/chunk-DWNMHQ4I.cjs.map +1 -0
  69. package/dist/chunk-FPGVR2BX.cjs +121 -0
  70. package/dist/chunk-FPGVR2BX.cjs.map +1 -0
  71. package/dist/chunk-JBP3G4QJ.js +50 -0
  72. package/dist/chunk-JBP3G4QJ.js.map +1 -0
  73. package/dist/chunk-JDP4DIHI.js +215 -0
  74. package/dist/chunk-JDP4DIHI.js.map +1 -0
  75. package/dist/chunk-LIZOVA4W.js +118 -0
  76. package/dist/chunk-LIZOVA4W.js.map +1 -0
  77. package/dist/chunk-QLBTOCT3.js +118 -0
  78. package/dist/chunk-QLBTOCT3.js.map +1 -0
  79. package/dist/chunk-UI2KDZ5H.js +344 -0
  80. package/dist/chunk-UI2KDZ5H.js.map +1 -0
  81. package/dist/chunk-V7QPRX4A.cjs +125 -0
  82. package/dist/chunk-V7QPRX4A.cjs.map +1 -0
  83. package/dist/chunk-VEJFN65Y.js +82 -0
  84. package/dist/chunk-VEJFN65Y.js.map +1 -0
  85. package/dist/chunk-WYZFAI6O.js +74 -0
  86. package/dist/chunk-WYZFAI6O.js.map +1 -0
  87. package/dist/chunk-XLYTATRQ.cjs +637 -0
  88. package/dist/chunk-XLYTATRQ.cjs.map +1 -0
  89. package/dist/cli/index.cjs +2036 -0
  90. package/dist/components/index.cjs +647 -0
  91. package/dist/components/index.cjs.map +1 -0
  92. package/dist/components/index.d.cts +187 -0
  93. package/dist/components/index.d.ts +187 -0
  94. package/dist/components/index.js +633 -0
  95. package/dist/components/index.js.map +1 -0
  96. package/dist/db-M3KXLBUP.js +3 -0
  97. package/dist/db-M3KXLBUP.js.map +1 -0
  98. package/dist/db-TDCZNY3Q.cjs +112 -0
  99. package/dist/db-TDCZNY3Q.cjs.map +1 -0
  100. package/dist/editor/index.cjs +1126 -0
  101. package/dist/editor/index.cjs.map +1 -0
  102. package/dist/editor/index.d.cts +137 -0
  103. package/dist/editor/index.d.ts +137 -0
  104. package/dist/editor/index.js +1093 -0
  105. package/dist/editor/index.js.map +1 -0
  106. package/dist/index-BcNka7gK.d.cts +475 -0
  107. package/dist/index-DPRgaNEE.d.ts +475 -0
  108. package/dist/index.cjs +433 -0
  109. package/dist/index.cjs.map +1 -0
  110. package/dist/index.d.cts +27 -0
  111. package/dist/index.d.ts +27 -0
  112. package/dist/index.js +209 -0
  113. package/dist/index.js.map +1 -0
  114. package/dist/lib/index.cjs +193 -0
  115. package/dist/lib/index.cjs.map +1 -0
  116. package/dist/lib/index.d.cts +4 -0
  117. package/dist/lib/index.d.ts +4 -0
  118. package/dist/lib/index.js +8 -0
  119. package/dist/lib/index.js.map +1 -0
  120. package/dist/styles/admin.css +667 -0
  121. package/dist/styles/blog.css +851 -0
  122. package/dist/styles/editor.css +452 -0
  123. package/dist/styles/globals.css +270 -0
  124. package/dist/styles/prose.css +299 -0
  125. package/dist/types-CgrBuig-.d.cts +778 -0
  126. package/dist/types-CgrBuig-.d.ts +778 -0
  127. package/package.json +140 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/search.ts","../src/lib/seo.ts","../src/lib/seo-scorer.ts"],"names":[],"mappings":"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","file":"chunk-UI2KDZ5H.js","sourcesContent":["import type { Collection, Document } from 'mongodb';\n\nexport interface SearchResult {\n slug: string;\n title: string;\n excerpt: string;\n score: number;\n}\n\nexport async function searchPosts(\n collection: Collection,\n query: string,\n limit: number = 10\n): Promise<SearchResult[]> {\n if (!query.trim()) return [];\n\n const results = await collection\n .find(\n {\n $text: { $search: query },\n status: 'published',\n },\n {\n projection: {\n slug: 1,\n title: 1,\n excerpt: 1,\n score: { $meta: 'textScore' },\n },\n }\n )\n .sort({ score: { $meta: 'textScore' } })\n .limit(limit)\n .toArray();\n\n return results.map((doc: Document) => ({\n slug: doc.slug as string,\n title: doc.title as string,\n excerpt: doc.excerpt as string,\n score: doc.score as number,\n }));\n}\n","import type { BlogPost } from './types';\nimport { getEnvConfig, getConfig } from './config';\nimport { absoluteUrl } from './url';\n\nexport interface MetaTags {\n title: string;\n description: string;\n canonical: string;\n openGraph: {\n title: string;\n description: string;\n url: string;\n siteName: string;\n type: string;\n images: { url: string; width?: number; height?: number; alt?: string }[];\n article?: {\n publishedTime: string;\n modifiedTime: string;\n section?: string;\n tags?: string[];\n };\n };\n twitter: {\n card: string;\n title: string;\n description: string;\n images: string[];\n };\n robots?: string;\n}\n\nexport function generateMetaTags(post: BlogPost): MetaTags {\n const env = getEnvConfig();\n const config = getConfig();\n const postUrl = `${env.BLOGPANEL_SITE_URL}${config.basePath}/${post.slug}`;\n const title = post.seo?.metaTitle || post.title;\n const description = post.seo?.metaDescription || post.excerpt;\n const canonical = post.seo?.canonicalUrl || postUrl;\n\n // og:image and twitter:image must be absolute — crawlers fetch them out of\n // band, with no document to resolve a relative path against. The GridFS\n // default stores root-relative URLs (`/api/blog/media/file/…`), so without\n // this every social preview on a default install would break silently;\n // R2's already-absolute URLs pass through untouched.\n const ogImage = absoluteUrl(\n post.seo?.ogImage || post.coverImage?.url,\n env.BLOGPANEL_SITE_URL,\n 'the og:image/twitter:image for a post'\n );\n\n return {\n title: `${title} | ${env.BLOGPANEL_SITE_NAME}`,\n description,\n canonical,\n openGraph: {\n title,\n description,\n url: postUrl,\n siteName: env.BLOGPANEL_SITE_NAME,\n type: post.seo?.ogType || 'article',\n images: ogImage\n ? [\n {\n url: ogImage,\n width: 1200,\n height: 630,\n alt: title,\n },\n ]\n : [],\n article: {\n publishedTime: post.publishedAt?.toISOString() || '',\n modifiedTime: post.updatedAt.toISOString(),\n section: post.categories[0],\n tags: post.tags,\n },\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n images: ogImage ? [ogImage] : [],\n },\n robots: post.seo?.noIndex ? 'noindex, nofollow' : undefined,\n };\n}\n\nexport interface ArticleStructuredData {\n '@context': string;\n '@type': string;\n headline: string;\n description: string;\n image?: string;\n datePublished?: string;\n dateModified: string;\n author: {\n '@type': string;\n name: string;\n url?: string;\n };\n publisher: {\n '@type': string;\n name: string;\n };\n mainEntityOfPage: {\n '@type': string;\n '@id': string;\n };\n wordCount: number;\n articleSection?: string;\n}\n\nexport function generateStructuredData(post: BlogPost): ArticleStructuredData {\n const env = getEnvConfig();\n const config = getConfig();\n const postUrl = `${env.BLOGPANEL_SITE_URL}${config.basePath}/${post.slug}`;\n\n return {\n '@context': 'https://schema.org',\n '@type': 'BlogPosting',\n headline: post.title,\n description: post.excerpt,\n // Same requirement as og:image: schema.org consumers fetch the image\n // directly, so a relative path is not usable.\n image: absoluteUrl(\n post.coverImage?.url || post.seo?.ogImage,\n env.BLOGPANEL_SITE_URL,\n 'the JSON-LD image for a post'\n ),\n datePublished: post.publishedAt?.toISOString(),\n dateModified: post.updatedAt.toISOString(),\n author: {\n '@type': 'Person',\n name: post.author.name,\n url: post.author.url,\n },\n publisher: {\n '@type': 'Organization',\n name: env.BLOGPANEL_SITE_NAME,\n },\n mainEntityOfPage: {\n '@type': 'WebPage',\n '@id': postUrl,\n },\n wordCount: post.wordCount,\n articleSection: post.categories[0],\n };\n}\n\nexport interface FAQStructuredData {\n '@context': string;\n '@type': string;\n mainEntity: {\n '@type': string;\n name: string;\n acceptedAnswer: {\n '@type': string;\n text: string;\n };\n }[];\n}\n\nexport function generateFAQStructuredData(\n faqItems: { question: string; answer: string }[]\n): FAQStructuredData | null {\n if (!faqItems.length) return null;\n\n return {\n '@context': 'https://schema.org',\n '@type': 'FAQPage',\n mainEntity: faqItems.map((item) => ({\n '@type': 'Question',\n name: item.question,\n acceptedAnswer: {\n '@type': 'Answer',\n text: item.answer,\n },\n })),\n };\n}\n\nexport interface BreadcrumbStructuredData {\n '@context': string;\n '@type': string;\n itemListElement: {\n '@type': string;\n position: number;\n name: string;\n item?: string;\n }[];\n}\n\nexport function generateBreadcrumbs(\n post: BlogPost,\n categoryName?: string\n): BreadcrumbStructuredData {\n const env = getEnvConfig();\n const config = getConfig();\n const items: BreadcrumbStructuredData['itemListElement'] = [\n {\n '@type': 'ListItem',\n position: 1,\n name: 'Home',\n item: env.BLOGPANEL_SITE_URL,\n },\n {\n '@type': 'ListItem',\n position: 2,\n name: 'Blog',\n item: `${env.BLOGPANEL_SITE_URL}${config.basePath}`,\n },\n ];\n\n if (categoryName && post.categories[0]) {\n items.push({\n '@type': 'ListItem',\n position: 3,\n name: categoryName,\n item: `${env.BLOGPANEL_SITE_URL}${config.basePath}/category/${post.categories[0]}`,\n });\n items.push({\n '@type': 'ListItem',\n position: 4,\n name: post.title,\n });\n } else {\n items.push({\n '@type': 'ListItem',\n position: 3,\n name: post.title,\n });\n }\n\n return {\n '@context': 'https://schema.org',\n '@type': 'BreadcrumbList',\n itemListElement: items,\n };\n}\n","import type { BlogPost, SEOScore, SEOCheck, SEOCheckStatus } from './types';\n\nfunction check(id: string, status: SEOCheckStatus, message: string): SEOCheck {\n return { id, status, message };\n}\n\nexport function calculateSEOScore(post: BlogPost): SEOScore {\n const checks: SEOCheck[] = [];\n const keyword = post.seo?.focusKeyword?.toLowerCase() || '';\n const title = post.title.toLowerCase();\n const slug = post.slug.toLowerCase();\n const excerpt = (post.seo?.metaDescription || post.excerpt || '').toLowerCase();\n const contentText = post.contentText?.toLowerCase() || '';\n const contentHTML = post.contentHTML || '';\n\n // 1. Focus keyword in title\n if (!keyword) {\n checks.push(check('focus-keyword-in-title', 'warn', 'No focus keyword set'));\n } else if (title.includes(keyword)) {\n checks.push(check('focus-keyword-in-title', 'pass', 'Focus keyword appears in title'));\n } else {\n checks.push(check('focus-keyword-in-title', 'fail', 'Focus keyword not found in title'));\n }\n\n // 2. Focus keyword in slug\n if (!keyword) {\n checks.push(check('focus-keyword-in-slug', 'warn', 'No focus keyword set'));\n } else if (slug.includes(keyword.replace(/\\s+/g, '-'))) {\n checks.push(check('focus-keyword-in-slug', 'pass', 'Focus keyword appears in URL slug'));\n } else {\n checks.push(check('focus-keyword-in-slug', 'fail', 'Focus keyword not found in URL slug'));\n }\n\n // 3. Focus keyword in meta description\n if (!keyword) {\n checks.push(check('focus-keyword-in-excerpt', 'warn', 'No focus keyword set'));\n } else if (excerpt.includes(keyword)) {\n checks.push(check('focus-keyword-in-excerpt', 'pass', 'Focus keyword appears in meta description'));\n } else {\n checks.push(check('focus-keyword-in-excerpt', 'fail', 'Focus keyword not found in meta description'));\n }\n\n // 4. Focus keyword in H2\n if (keyword) {\n const h2Regex = /<h2[^>]*>(.*?)<\\/h2>/gi;\n const h2s = contentHTML.match(h2Regex) || [];\n const keywordInH2 = h2s.some((h2) => h2.toLowerCase().includes(keyword));\n if (keywordInH2) {\n checks.push(check('focus-keyword-in-h2', 'pass', 'Focus keyword found in a subheading'));\n } else {\n checks.push(check('focus-keyword-in-h2', 'warn', 'Focus keyword not found in any H2 subheading'));\n }\n }\n\n // 5. Focus keyword in first paragraph\n if (keyword && contentText) {\n const first150Words = contentText.split(/\\s+/).slice(0, 150).join(' ');\n if (first150Words.includes(keyword)) {\n checks.push(check('focus-keyword-in-first-paragraph', 'pass', 'Focus keyword appears early in content'));\n } else {\n checks.push(check('focus-keyword-in-first-paragraph', 'warn', 'Focus keyword not found in the first paragraph'));\n }\n }\n\n // 6. Keyword density\n if (keyword && contentText) {\n const words = contentText.split(/\\s+/).length;\n const keywordCount = (contentText.match(new RegExp(keyword, 'g')) || []).length;\n const density = (keywordCount / words) * 100;\n if (density >= 0.5 && density <= 2.5) {\n checks.push(check('focus-keyword-density', 'pass', `Keyword density is ${density.toFixed(1)}% (ideal)`));\n } else if (density < 0.5) {\n checks.push(check('focus-keyword-density', 'warn', `Keyword density is ${density.toFixed(1)}% (too low, aim for 0.5-2.5%)`));\n } else {\n checks.push(check('focus-keyword-density', 'warn', `Keyword density is ${density.toFixed(1)}% (too high, aim for 0.5-2.5%)`));\n }\n }\n\n // 7. Title length\n const metaTitle = post.seo?.metaTitle || post.title;\n if (metaTitle.length >= 50 && metaTitle.length <= 60) {\n checks.push(check('title-length', 'pass', `Title is ${metaTitle.length} characters (ideal)`));\n } else if (metaTitle.length < 30) {\n checks.push(check('title-length', 'fail', `Title is ${metaTitle.length} characters (too short, aim for 50-60)`));\n } else if (metaTitle.length > 70) {\n checks.push(check('title-length', 'warn', `Title is ${metaTitle.length} characters (too long, aim for 50-60)`));\n } else {\n checks.push(check('title-length', 'warn', `Title is ${metaTitle.length} characters (aim for 50-60)`));\n }\n\n // 8. Meta description length\n const metaDesc = post.seo?.metaDescription || post.excerpt || '';\n if (metaDesc.length >= 150 && metaDesc.length <= 160) {\n checks.push(check('meta-description-length', 'pass', `Meta description is ${metaDesc.length} characters (ideal)`));\n } else if (metaDesc.length < 120) {\n checks.push(check('meta-description-length', 'warn', `Meta description is ${metaDesc.length} characters (too short, aim for 150-160)`));\n } else if (metaDesc.length > 170) {\n checks.push(check('meta-description-length', 'warn', `Meta description is ${metaDesc.length} characters (too long, may be truncated)`));\n } else {\n checks.push(check('meta-description-length', 'pass', `Meta description is ${metaDesc.length} characters`));\n }\n\n // 9. Slug length\n if (post.slug.length <= 75) {\n checks.push(check('slug-length', 'pass', `URL slug is ${post.slug.length} characters`));\n } else {\n checks.push(check('slug-length', 'warn', `URL slug is ${post.slug.length} characters (should be under 75)`));\n }\n\n // 10. Content length\n if (post.wordCount >= 300) {\n checks.push(check('content-length', 'pass', `Content is ${post.wordCount} words`));\n } else {\n checks.push(check('content-length', 'fail', `Content is only ${post.wordCount} words (aim for 300+)`));\n }\n\n // 11. Heading hierarchy\n const headingRegex = /<(h[2-6])[^>]*>/gi;\n const headings = [...contentHTML.matchAll(headingRegex)].map((m) => parseInt(m[1][1]));\n let hierarchyOk = true;\n for (let i = 1; i < headings.length; i++) {\n if (headings[i] > headings[i - 1] + 1) {\n hierarchyOk = false;\n break;\n }\n }\n if (headings.length === 0) {\n checks.push(check('heading-hierarchy', 'warn', 'No subheadings found — add H2s to structure content'));\n } else if (hierarchyOk) {\n checks.push(check('heading-hierarchy', 'pass', 'Heading hierarchy is correct'));\n } else {\n checks.push(check('heading-hierarchy', 'warn', 'Heading levels are skipped (e.g., H2 → H4)'));\n }\n\n // 12. Image alt text\n const imgRegex = /<img[^>]*>/gi;\n const images = contentHTML.match(imgRegex) || [];\n const missingAlt = images.filter((img) => !img.includes('alt=') || img.includes('alt=\"\"'));\n if (images.length === 0) {\n checks.push(check('image-alt-text', 'warn', 'No images found in content'));\n } else if (missingAlt.length === 0) {\n checks.push(check('image-alt-text', 'pass', 'All images have alt text'));\n } else {\n checks.push(check('image-alt-text', 'fail', `${missingAlt.length} image(s) missing alt text`));\n }\n\n // 13. Internal links\n const internalLinkRegex = /href=[\"']\\/[^\"']*[\"']/gi;\n const internalLinks = contentHTML.match(internalLinkRegex) || [];\n if (internalLinks.length > 0) {\n checks.push(check('internal-links', 'pass', `${internalLinks.length} internal link(s) found`));\n } else {\n checks.push(check('internal-links', 'warn', 'No internal links found — add links to related content'));\n }\n\n // 14. External links\n const externalLinkRegex = /href=[\"']https?:\\/\\/[^\"']*[\"']/gi;\n const externalLinks = contentHTML.match(externalLinkRegex) || [];\n if (externalLinks.length > 0) {\n checks.push(check('external-links', 'pass', `${externalLinks.length} external link(s) found`));\n } else {\n checks.push(check('external-links', 'warn', 'No external links found'));\n }\n\n // 15. Paragraph length\n const paragraphs = contentHTML.split(/<\\/p>/i).filter((p) => p.trim());\n const longParagraphs = paragraphs.filter((p) => {\n const text = p.replace(/<[^>]+>/g, '');\n return text.split(/\\s+/).length > 300;\n });\n if (longParagraphs.length === 0) {\n checks.push(check('paragraph-length', 'pass', 'All paragraphs are a reasonable length'));\n } else {\n checks.push(check('paragraph-length', 'warn', `${longParagraphs.length} paragraph(s) exceed 300 words`));\n }\n\n // 16. Cover image\n if (post.coverImage?.url) {\n checks.push(check('cover-image', 'pass', 'Post has a cover image'));\n } else {\n checks.push(check('cover-image', 'warn', 'No cover image set — social shares may look plain'));\n }\n\n // 17. Readability (simplified Flesch-like check)\n if (contentText) {\n const sentences = contentText.split(/[.!?]+/).filter((s) => s.trim().length > 0);\n const words = contentText.split(/\\s+/).length;\n const avgWordsPerSentence = sentences.length > 0 ? words / sentences.length : 0;\n if (avgWordsPerSentence <= 20) {\n checks.push(check('readability-score', 'pass', `Average sentence length: ${avgWordsPerSentence.toFixed(0)} words`));\n } else if (avgWordsPerSentence <= 25) {\n checks.push(check('readability-score', 'warn', `Average sentence length: ${avgWordsPerSentence.toFixed(0)} words (try to keep under 20)`));\n } else {\n checks.push(check('readability-score', 'fail', `Average sentence length: ${avgWordsPerSentence.toFixed(0)} words (too long, aim for under 20)`));\n }\n }\n\n // Calculate overall score\n const fails = checks.filter((c) => c.status === 'fail').length;\n const warns = checks.filter((c) => c.status === 'warn').length;\n\n let overall: SEOScore['overall'];\n if (fails >= 3) {\n overall = 'poor';\n } else if (fails >= 1 || warns >= 5) {\n overall = 'ok';\n } else {\n overall = 'good';\n }\n\n return { overall, checks };\n}\n"]}
@@ -0,0 +1,125 @@
1
+ 'use strict';
2
+
3
+ var zod = require('zod');
4
+
5
+ // src/lib/types.ts
6
+ var mediaUrlSchema = zod.z.string().min(1).refine(
7
+ (value) => /^https?:\/\//i.test(value) || value.startsWith("/") && !value.startsWith("//"),
8
+ {
9
+ message: 'must be an http:// or https:// URL, or a root-relative path beginning with a single "/"'
10
+ }
11
+ );
12
+ zod.z.enum([
13
+ "paragraph",
14
+ "heading",
15
+ "image",
16
+ "codeBlock",
17
+ "blockquote",
18
+ "bulletList",
19
+ "orderedList",
20
+ "taskList",
21
+ "table",
22
+ "embed",
23
+ "horizontalRule",
24
+ "callout",
25
+ "tableOfContents",
26
+ "faq",
27
+ "html"
28
+ ]);
29
+ zod.z.object({
30
+ filename: zod.z.string(),
31
+ originalName: zod.z.string(),
32
+ mimeType: zod.z.string(),
33
+ size: zod.z.number(),
34
+ width: zod.z.number().optional(),
35
+ height: zod.z.number().optional(),
36
+ storageKey: zod.z.string(),
37
+ // Widened from z.enum(['gridfs','r2']) so a registered custom backend can
38
+ // record itself here. Purely additive: 'gridfs' and 'r2' still validate,
39
+ // and no stored document changes shape, so there is nothing to migrate.
40
+ // Non-empty is still enforced — an empty provider is unresolvable, and
41
+ // DELETE would have to fall back to the configured backend to act on it.
42
+ provider: zod.z.string().min(1),
43
+ url: mediaUrlSchema,
44
+ alt: zod.z.string().optional(),
45
+ caption: zod.z.string().optional(),
46
+ createdAt: zod.z.date()
47
+ });
48
+ var AuthorSchema = zod.z.object({
49
+ name: zod.z.string().min(1),
50
+ avatar: mediaUrlSchema.optional(),
51
+ bio: zod.z.string().optional(),
52
+ url: zod.z.string().url().optional()
53
+ });
54
+ var PostSEOSchema = zod.z.object({
55
+ metaTitle: zod.z.string().optional(),
56
+ metaDescription: zod.z.string().optional(),
57
+ canonicalUrl: zod.z.string().url().optional(),
58
+ ogImage: mediaUrlSchema.optional(),
59
+ ogType: zod.z.string().default("article"),
60
+ noIndex: zod.z.boolean().default(false),
61
+ structuredData: zod.z.record(zod.z.unknown()).optional(),
62
+ focusKeyword: zod.z.string().optional()
63
+ });
64
+ var PostStatusSchema = zod.z.enum(["draft", "published", "scheduled", "archived"]);
65
+ var CreatePostSchema = zod.z.object({
66
+ title: zod.z.string().min(1, "Title is required"),
67
+ slug: zod.z.string().optional(),
68
+ excerpt: zod.z.string().optional(),
69
+ content: zod.z.any(),
70
+ // BlockContent[] — validated structurally
71
+ contentHTML: zod.z.string().optional(),
72
+ contentText: zod.z.string().optional(),
73
+ coverImage: zod.z.object({
74
+ _id: zod.z.string(),
75
+ url: mediaUrlSchema,
76
+ alt: zod.z.string().optional(),
77
+ caption: zod.z.string().optional(),
78
+ width: zod.z.number().optional(),
79
+ height: zod.z.number().optional()
80
+ }).optional(),
81
+ categories: zod.z.array(zod.z.string()).default([]),
82
+ tags: zod.z.array(zod.z.string()).default([]),
83
+ author: AuthorSchema.optional(),
84
+ seo: PostSEOSchema.optional(),
85
+ status: PostStatusSchema.default("draft"),
86
+ publishedAt: zod.z.coerce.date().optional(),
87
+ scheduledAt: zod.z.coerce.date().optional()
88
+ });
89
+ var UpdatePostSchema = CreatePostSchema.partial();
90
+ var CreateCategorySchema = zod.z.object({
91
+ name: zod.z.string().min(1, "Category name is required"),
92
+ slug: zod.z.string().optional(),
93
+ description: zod.z.string().optional(),
94
+ seo: zod.z.object({
95
+ metaTitle: zod.z.string().optional(),
96
+ metaDescription: zod.z.string().optional()
97
+ }).optional(),
98
+ order: zod.z.number().default(0),
99
+ parentId: zod.z.string().optional()
100
+ });
101
+ var UpdateCategorySchema = CreateCategorySchema.partial();
102
+ var BlogSettingsSchema = zod.z.object({
103
+ postsPerPage: zod.z.number().min(1).max(100).default(10),
104
+ defaultAuthor: AuthorSchema.optional(),
105
+ defaultOgImage: mediaUrlSchema.optional(),
106
+ commentSystem: zod.z.enum(["none", "giscus", "disqus"]).default("none"),
107
+ commentConfig: zod.z.record(zod.z.unknown()).optional(),
108
+ customCSS: zod.z.string().optional(),
109
+ analytics: zod.z.object({
110
+ gaId: zod.z.string().optional(),
111
+ plausibleDomain: zod.z.string().optional()
112
+ }).optional()
113
+ });
114
+ var CreateApiTokenSchema = zod.z.object({
115
+ name: zod.z.string().min(1, "Token name is required").max(100)
116
+ });
117
+
118
+ exports.BlogSettingsSchema = BlogSettingsSchema;
119
+ exports.CreateApiTokenSchema = CreateApiTokenSchema;
120
+ exports.CreateCategorySchema = CreateCategorySchema;
121
+ exports.CreatePostSchema = CreatePostSchema;
122
+ exports.UpdateCategorySchema = UpdateCategorySchema;
123
+ exports.UpdatePostSchema = UpdatePostSchema;
124
+ //# sourceMappingURL=chunk-V7QPRX4A.cjs.map
125
+ //# sourceMappingURL=chunk-V7QPRX4A.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/types.ts"],"names":["z"],"mappings":";;;;;AA6BA,IAAM,iBAAiBA,KAAA,CACpB,MAAA,EAAO,CACP,GAAA,CAAI,CAAC,CAAA,CACL,MAAA;AAAA,EACC,CAAC,KAAA,KAAU,eAAA,CAAgB,IAAA,CAAK,KAAK,CAAA,IAAM,KAAA,CAAM,UAAA,CAAW,GAAG,CAAA,IAAK,CAAC,KAAA,CAAM,WAAW,IAAI,CAAA;AAAA,EAC1F;AAAA,IACE,OAAA,EAAS;AAAA;AAEb,CAAA;AAM6BA,MAAE,IAAA,CAAK;AAAA,EACpC,WAAA;AAAA,EACA,SAAA;AAAA,EACA,OAAA;AAAA,EACA,WAAA;AAAA,EACA,YAAA;AAAA,EACA,YAAA;AAAA,EACA,aAAA;AAAA,EACA,UAAA;AAAA,EACA,OAAA;AAAA,EACA,OAAA;AAAA,EACA,gBAAA;AAAA,EACA,SAAA;AAAA,EACA,iBAAA;AAAA,EACA,KAAA;AAAA,EACA;AACF,CAAC;AAyB0BA,MAAE,MAAA,CAAO;AAAA,EAClC,QAAA,EAAUA,MAAE,MAAA,EAAO;AAAA,EACnB,YAAA,EAAcA,MAAE,MAAA,EAAO;AAAA,EACvB,QAAA,EAAUA,MAAE,MAAA,EAAO;AAAA,EACnB,IAAA,EAAMA,MAAE,MAAA,EAAO;AAAA,EACf,KAAA,EAAOA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC3B,MAAA,EAAQA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC5B,UAAA,EAAYA,MAAE,MAAA,EAAO;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,EAMrB,QAAA,EAAUA,KAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EAC1B,GAAA,EAAK,cAAA;AAAA,EACL,GAAA,EAAKA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EACzB,OAAA,EAASA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC7B,SAAA,EAAWA,MAAE,IAAA;AACf,CAAC;AA6BM,IAAM,YAAA,GAAeA,MAAE,MAAA,CAAO;AAAA,EACnC,IAAA,EAAMA,KAAA,CAAE,MAAA,EAAO,CAAE,IAAI,CAAC,CAAA;AAAA,EACtB,MAAA,EAAQ,eAAe,QAAA,EAAS;AAAA,EAChC,GAAA,EAAKA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EACzB,KAAKA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,GAAM,QAAA;AACxB,CAAC,CAAA;AAaM,IAAM,aAAA,GAAgBA,MAAE,MAAA,CAAO;AAAA,EACpC,SAAA,EAAWA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC/B,eAAA,EAAiBA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EACrC,cAAcA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,GAAM,QAAA,EAAS;AAAA,EACxC,OAAA,EAAS,eAAe,QAAA,EAAS;AAAA,EACjC,MAAA,EAAQA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAQ,SAAS,CAAA;AAAA,EACpC,OAAA,EAASA,KAAA,CAAE,OAAA,EAAQ,CAAE,QAAQ,KAAK,CAAA;AAAA,EAClC,gBAAgBA,KAAA,CAAE,MAAA,CAAOA,MAAE,OAAA,EAAS,EAAE,QAAA,EAAS;AAAA,EAC/C,YAAA,EAAcA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA;AAC3B,CAAC,CAAA;AA6BM,IAAM,gBAAA,GAAmBA,MAAE,IAAA,CAAK,CAAC,SAAS,WAAA,EAAa,WAAA,EAAa,UAAU,CAAC,CAAA;AAG/E,IAAM,gBAAA,GAAmBA,MAAE,MAAA,CAAO;AAAA,EACvC,OAAOA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,GAAG,mBAAmB,CAAA;AAAA,EAC5C,IAAA,EAAMA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC1B,OAAA,EAASA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC7B,OAAA,EAASA,MAAE,GAAA,EAAI;AAAA;AAAA,EACf,WAAA,EAAaA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EACjC,WAAA,EAAaA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EACjC,UAAA,EAAYA,MACT,MAAA,CAAO;AAAA,IACN,GAAA,EAAKA,MAAE,MAAA,EAAO;AAAA,IACd,GAAA,EAAK,cAAA;AAAA,IACL,GAAA,EAAKA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,IACzB,OAAA,EAASA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,IAC7B,KAAA,EAAOA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,IAC3B,MAAA,EAAQA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA;AAAS,GAC7B,EACA,QAAA,EAAS;AAAA,EACZ,UAAA,EAAYA,MAAE,KAAA,CAAMA,KAAA,CAAE,QAAQ,CAAA,CAAE,OAAA,CAAQ,EAAE,CAAA;AAAA,EAC1C,IAAA,EAAMA,MAAE,KAAA,CAAMA,KAAA,CAAE,QAAQ,CAAA,CAAE,OAAA,CAAQ,EAAE,CAAA;AAAA,EACpC,MAAA,EAAQ,aAAa,QAAA,EAAS;AAAA,EAC9B,GAAA,EAAK,cAAc,QAAA,EAAS;AAAA,EAC5B,MAAA,EAAQ,gBAAA,CAAiB,OAAA,CAAQ,OAAO,CAAA;AAAA,EACxC,WAAA,EAAaA,KAAA,CAAE,MAAA,CAAO,IAAA,GAAO,QAAA,EAAS;AAAA,EACtC,WAAA,EAAaA,KAAA,CAAE,MAAA,CAAO,IAAA,GAAO,QAAA;AAC/B,CAAC;AAEM,IAAM,gBAAA,GAAmB,iBAAiB,OAAA;AAiC1C,IAAM,oBAAA,GAAuBA,MAAE,MAAA,CAAO;AAAA,EAC3C,MAAMA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,GAAG,2BAA2B,CAAA;AAAA,EACnD,IAAA,EAAMA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC1B,WAAA,EAAaA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EACjC,GAAA,EAAKA,MACF,MAAA,CAAO;AAAA,IACN,SAAA,EAAWA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,IAC/B,eAAA,EAAiBA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA;AAAS,GACtC,EACA,QAAA,EAAS;AAAA,EACZ,KAAA,EAAOA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAQ,CAAC,CAAA;AAAA,EAC3B,QAAA,EAAUA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA;AACvB,CAAC;AAEM,IAAM,oBAAA,GAAuB,qBAAqB,OAAA;AAuBlD,IAAM,kBAAA,GAAqBA,MAAE,MAAA,CAAO;AAAA,EACzC,YAAA,EAAcA,KAAA,CAAE,MAAA,EAAO,CAAE,GAAA,CAAI,CAAC,CAAA,CAAE,GAAA,CAAI,GAAG,CAAA,CAAE,OAAA,CAAQ,EAAE,CAAA;AAAA,EACnD,aAAA,EAAe,aAAa,QAAA,EAAS;AAAA,EACrC,cAAA,EAAgB,eAAe,QAAA,EAAS;AAAA,EACxC,aAAA,EAAeA,KAAA,CAAE,IAAA,CAAK,CAAC,MAAA,EAAQ,UAAU,QAAQ,CAAC,CAAA,CAAE,OAAA,CAAQ,MAAM,CAAA;AAAA,EAClE,eAAeA,KAAA,CAAE,MAAA,CAAOA,MAAE,OAAA,EAAS,EAAE,QAAA,EAAS;AAAA,EAC9C,SAAA,EAAWA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,EAC/B,SAAA,EAAWA,MACR,MAAA,CAAO;AAAA,IACN,IAAA,EAAMA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA,EAAS;AAAA,IAC1B,eAAA,EAAiBA,KAAA,CAAE,MAAA,EAAO,CAAE,QAAA;AAAS,GACtC,EACA,QAAA;AACL,CAAC;AAoBM,IAAM,oBAAA,GAAuBA,MAAE,MAAA,CAAO;AAAA,EAC3C,IAAA,EAAMA,MAAE,MAAA,EAAO,CAAE,IAAI,CAAA,EAAG,wBAAwB,CAAA,CAAE,GAAA,CAAI,GAAG;AAC3D,CAAC","file":"chunk-V7QPRX4A.cjs","sourcesContent":["import { z } from 'zod';\nimport type { ObjectId } from 'mongodb';\n\n/**\n * A URL that points at an image this install stores.\n *\n * Not `z.string().url()`: GridFS is the default backend and it returns\n * root-relative URLs (`/api/blog/media/file/…`), which `.url()` rejects. With\n * the strict validator, an image uploaded through the media library could not\n * be attached as a cover image, an `ogImage`, or an avatar on a default\n * install — only R2 URLs got through. Both shapes are accepted here and the\n * consumers that require an absolute URL (Open Graph, JSON-LD, RSS) prefix\n * `BLOGPANEL_SITE_URL` at the point of use.\n *\n * Deliberately narrower than `isAbsoluteUrl()` from `./url`, which is right\n * for its own job (recognising a value that is *already* resolvable, whatever\n * its scheme, so it can be passed through untouched) but wrong for this one:\n * this schema validates a value a user is submitting to be *stored* as a\n * cover image, `ogImage`, or avatar and later rendered into `<img src>`,\n * `og:image`, and JSON-LD. `isAbsoluteUrl()` also treats protocol-relative\n * `//host/path` as already-absolute, and any `scheme:` — including\n * `javascript:` and `data:`, which `z.string().url()` accepted before this\n * release too, via `new URL()` — as acceptable. Accepting `//evil.example.com/x`\n * here would let an off-origin image be set as `og:image` or a post's cover\n * image with no scheme ever pinned down: the browser or crawler fills in\n * whichever scheme the current page happens to be on. So this schema\n * allowlists `http:`/`https:` plus a genuinely root-relative path — a single\n * leading `/`, not `//` — instead.\n */\nconst mediaUrlSchema = z\n .string()\n .min(1)\n .refine(\n (value) => /^https?:\\/\\//i.test(value) || (value.startsWith('/') && !value.startsWith('//')),\n {\n message: 'must be an http:// or https:// URL, or a root-relative path beginning with a single \"/\"',\n }\n );\n\n// ============================================================\n// Block Editor Types\n// ============================================================\n\nexport const BlockTypeSchema = z.enum([\n 'paragraph',\n 'heading',\n 'image',\n 'codeBlock',\n 'blockquote',\n 'bulletList',\n 'orderedList',\n 'taskList',\n 'table',\n 'embed',\n 'horizontalRule',\n 'callout',\n 'tableOfContents',\n 'faq',\n 'html',\n]);\n\nexport type BlockType = z.infer<typeof BlockTypeSchema>;\n\nexport interface BlockContent {\n type: string;\n attrs?: Record<string, unknown>;\n content?: BlockContent[];\n text?: string;\n marks?: { type: string; attrs?: Record<string, unknown> }[];\n}\n\n// ============================================================\n// Media\n// ============================================================\n\nexport interface MediaReference {\n _id: string;\n url: string;\n alt?: string;\n caption?: string;\n width?: number;\n height?: number;\n}\n\nexport const MediaSchema = z.object({\n filename: z.string(),\n originalName: z.string(),\n mimeType: z.string(),\n size: z.number(),\n width: z.number().optional(),\n height: z.number().optional(),\n storageKey: z.string(),\n // Widened from z.enum(['gridfs','r2']) so a registered custom backend can\n // record itself here. Purely additive: 'gridfs' and 'r2' still validate,\n // and no stored document changes shape, so there is nothing to migrate.\n // Non-empty is still enforced — an empty provider is unresolvable, and\n // DELETE would have to fall back to the configured backend to act on it.\n provider: z.string().min(1),\n url: mediaUrlSchema,\n alt: z.string().optional(),\n caption: z.string().optional(),\n createdAt: z.date(),\n});\n\nexport interface Media {\n _id: ObjectId;\n filename: string;\n originalName: string;\n mimeType: string;\n size: number;\n width?: number;\n height?: number;\n storageKey: string;\n /**\n * Name of the backend that actually holds this object — `'gridfs'`, `'r2'`,\n * or any name passed to `registerStorageProvider()`. Read back by DELETE to\n * route the removal to the right backend rather than to whichever one is\n * configured today. A `string`, not a union of the built-ins: a third-party\n * backend that cannot be recorded here cannot have its objects deleted.\n */\n provider: string;\n url: string;\n alt?: string;\n caption?: string;\n createdAt: Date;\n}\n\n// ============================================================\n// Author\n// ============================================================\n\nexport const AuthorSchema = z.object({\n name: z.string().min(1),\n avatar: mediaUrlSchema.optional(),\n bio: z.string().optional(),\n url: z.string().url().optional(),\n});\n\nexport interface Author {\n name: string;\n avatar?: string;\n bio?: string;\n url?: string;\n}\n\n// ============================================================\n// SEO\n// ============================================================\n\nexport const PostSEOSchema = z.object({\n metaTitle: z.string().optional(),\n metaDescription: z.string().optional(),\n canonicalUrl: z.string().url().optional(),\n ogImage: mediaUrlSchema.optional(),\n ogType: z.string().default('article'),\n noIndex: z.boolean().default(false),\n structuredData: z.record(z.unknown()).optional(),\n focusKeyword: z.string().optional(),\n});\n\nexport interface PostSEO {\n metaTitle?: string;\n metaDescription?: string;\n canonicalUrl?: string;\n ogImage?: string;\n ogType: string;\n noIndex: boolean;\n structuredData?: Record<string, unknown>;\n focusKeyword?: string;\n}\n\n// ============================================================\n// Revision\n// ============================================================\n\nexport interface Revision {\n version: number;\n title: string;\n content: BlockContent[];\n contentHTML: string;\n savedAt: Date;\n}\n\n// ============================================================\n// Post\n// ============================================================\n\nexport const PostStatusSchema = z.enum(['draft', 'published', 'scheduled', 'archived']);\nexport type PostStatus = z.infer<typeof PostStatusSchema>;\n\nexport const CreatePostSchema = z.object({\n title: z.string().min(1, 'Title is required'),\n slug: z.string().optional(),\n excerpt: z.string().optional(),\n content: z.any(), // BlockContent[] — validated structurally\n contentHTML: z.string().optional(),\n contentText: z.string().optional(),\n coverImage: z\n .object({\n _id: z.string(),\n url: mediaUrlSchema,\n alt: z.string().optional(),\n caption: z.string().optional(),\n width: z.number().optional(),\n height: z.number().optional(),\n })\n .optional(),\n categories: z.array(z.string()).default([]),\n tags: z.array(z.string()).default([]),\n author: AuthorSchema.optional(),\n seo: PostSEOSchema.optional(),\n status: PostStatusSchema.default('draft'),\n publishedAt: z.coerce.date().optional(),\n scheduledAt: z.coerce.date().optional(),\n});\n\nexport const UpdatePostSchema = CreatePostSchema.partial();\n\nexport type CreatePostInput = z.infer<typeof CreatePostSchema>;\nexport type UpdatePostInput = z.infer<typeof UpdatePostSchema>;\n\nexport interface BlogPost {\n _id: ObjectId;\n title: string;\n slug: string;\n excerpt: string;\n content: BlockContent[];\n contentHTML: string;\n contentText: string;\n coverImage?: MediaReference;\n categories: string[];\n tags: string[];\n author: Author;\n seo: PostSEO;\n status: PostStatus;\n publishedAt?: Date;\n scheduledAt?: Date;\n readingTime: number;\n wordCount: number;\n version: number;\n revisions: Revision[];\n createdAt: Date;\n updatedAt: Date;\n}\n\n// ============================================================\n// Category\n// ============================================================\n\nexport const CreateCategorySchema = z.object({\n name: z.string().min(1, 'Category name is required'),\n slug: z.string().optional(),\n description: z.string().optional(),\n seo: z\n .object({\n metaTitle: z.string().optional(),\n metaDescription: z.string().optional(),\n })\n .optional(),\n order: z.number().default(0),\n parentId: z.string().optional(),\n});\n\nexport const UpdateCategorySchema = CreateCategorySchema.partial();\n\nexport type CreateCategoryInput = z.infer<typeof CreateCategorySchema>;\nexport type UpdateCategoryInput = z.infer<typeof UpdateCategorySchema>;\n\nexport interface Category {\n _id: ObjectId;\n name: string;\n slug: string;\n description?: string;\n seo?: {\n metaTitle?: string;\n metaDescription?: string;\n };\n order: number;\n parentId?: ObjectId;\n postCount: number;\n}\n\n// ============================================================\n// Settings\n// ============================================================\n\nexport const BlogSettingsSchema = z.object({\n postsPerPage: z.number().min(1).max(100).default(10),\n defaultAuthor: AuthorSchema.optional(),\n defaultOgImage: mediaUrlSchema.optional(),\n commentSystem: z.enum(['none', 'giscus', 'disqus']).default('none'),\n commentConfig: z.record(z.unknown()).optional(),\n customCSS: z.string().optional(),\n analytics: z\n .object({\n gaId: z.string().optional(),\n plausibleDomain: z.string().optional(),\n })\n .optional(),\n});\n\nexport interface BlogSettings {\n _id: string;\n postsPerPage: number;\n defaultAuthor?: Author;\n defaultOgImage?: string;\n commentSystem: 'none' | 'giscus' | 'disqus';\n commentConfig?: Record<string, unknown>;\n customCSS?: string;\n analytics?: {\n gaId?: string;\n plausibleDomain?: string;\n };\n}\n\n// ============================================================\n// API Tokens\n// ============================================================\n\nexport const CreateApiTokenSchema = z.object({\n name: z.string().min(1, 'Token name is required').max(100),\n});\n\nexport interface ApiToken {\n _id: ObjectId;\n name: string;\n tokenHash: string;\n prefix: string;\n lastUsedAt?: Date;\n createdAt: Date;\n}\n\n// ============================================================\n// API Response Types\n// ============================================================\n\nexport interface ApiSuccessResponse<T = unknown> {\n success: true;\n data: T;\n meta?: {\n page: number;\n limit: number;\n total: number;\n totalPages: number;\n };\n}\n\nexport interface ApiErrorResponse {\n success: false;\n error: {\n code: string;\n message: string;\n };\n}\n\nexport type ApiResponse<T = unknown> = ApiSuccessResponse<T> | ApiErrorResponse;\n\n// ============================================================\n// SEO Score Types\n// ============================================================\n\nexport type SEOCheckStatus = 'pass' | 'warn' | 'fail';\nexport type SEOOverallScore = 'good' | 'ok' | 'poor';\n\nexport interface SEOCheck {\n id: string;\n status: SEOCheckStatus;\n message: string;\n}\n\nexport interface SEOScore {\n overall: SEOOverallScore;\n checks: SEOCheck[];\n}\n\n// ============================================================\n// Config Types\n// ============================================================\n\nexport interface BlogPanelConfig {\n basePath: string;\n adminPath: string;\n apiPath: string;\n postsPerPage: number;\n excerptLength: number;\n codeHighlighter: 'shiki' | 'prism';\n editor: {\n blocks: BlockType[];\n /** Ceiling on the STORED result. Enforced by compression, never by rejection. */\n maxImageSize: number;\n /** Ceiling on the RAW upload. Enforced by rejection, before buffering. */\n maxUploadSize: number;\n imageFormats: string[];\n autosaveInterval: number;\n };\n seo: {\n titleTemplate: string;\n defaultOgImage?: string;\n generateRSS: boolean;\n generateSitemap: boolean;\n structuredData: boolean;\n minContentLength: number;\n };\n auth: {\n strategy: 'api-key' | 'custom' | 'credentials';\n verify?: (request: Request) => Promise<boolean>;\n admins?: string[];\n };\n features: {\n search: boolean;\n relatedPosts: boolean;\n readingProgress: boolean;\n tableOfContents: boolean;\n shareButtons: boolean;\n darkMode: boolean;\n scheduling: boolean;\n revisionHistory: boolean;\n imageOptimization: boolean;\n };\n theme: {\n variables?: Record<string, string>;\n darkMode?: boolean;\n components?: Record<string, React.ComponentType<unknown>>;\n };\n hooks: {\n beforePublish?: (post: BlogPost) => Promise<void>;\n afterPublish?: (post: BlogPost) => Promise<void>;\n beforeDelete?: (post: BlogPost) => Promise<void>;\n onMediaUpload?: (media: Media) => Promise<void>;\n };\n}\n\n/**\n * What a user's `next-blogpanel.config.ts` is allowed to contain.\n *\n * A plain `Partial<BlogPanelConfig>` only makes the top-level keys optional:\n * it would demand all five `editor` keys from anyone who wanted to change\n * `maxImageSize` alone. The merge in `getConfig()`/`setConfig()` has always\n * been a per-key merge of the nested groups, so the type is what was wrong,\n * not the behaviour.\n */\nexport type BlogPanelUserConfig = Partial<\n Omit<BlogPanelConfig, 'editor' | 'seo' | 'auth' | 'features' | 'theme' | 'hooks'>\n> & {\n editor?: Partial<BlogPanelConfig['editor']>;\n seo?: Partial<BlogPanelConfig['seo']>;\n auth?: Partial<BlogPanelConfig['auth']>;\n features?: Partial<BlogPanelConfig['features']>;\n theme?: Partial<BlogPanelConfig['theme']>;\n hooks?: Partial<BlogPanelConfig['hooks']>;\n};\n\n// ============================================================\n// Query Types\n// ============================================================\n\nexport interface PostListQuery {\n page?: number;\n limit?: number;\n category?: string;\n tag?: string;\n status?: PostStatus;\n search?: string;\n sortBy?: 'publishedAt' | 'createdAt' | 'title';\n sortOrder?: 'asc' | 'desc';\n}\n\nexport interface MediaListQuery {\n page?: number;\n limit?: number;\n mimeType?: string;\n}\n"]}
@@ -0,0 +1,82 @@
1
+ import { getEnvConfig, getConfig, getCollection } from './chunk-6PZ772NQ.js';
2
+
3
+ // src/lib/url.ts
4
+ function isAbsoluteUrl(url) {
5
+ return /^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith("//");
6
+ }
7
+ var warned = /* @__PURE__ */ Symbol.for("next-blogpanel.url.warned");
8
+ function warnOnce(context, url) {
9
+ const store = globalThis;
10
+ const seen = store[warned] || (store[warned] = /* @__PURE__ */ new Set());
11
+ if (seen.has(context)) return;
12
+ seen.add(context);
13
+ console.warn(
14
+ `[blogpanel] BLOGPANEL_SITE_URL is not set, so ${context} was omitted rather than emitted as the relative URL "${url}". Open Graph, Twitter cards, schema.org and RSS all require absolute URLs; a relative one is not a smaller version of the right answer, it is invalid. Set BLOGPANEL_SITE_URL to your site's origin (no trailing slash).`
15
+ );
16
+ }
17
+ function absoluteUrl(url, siteUrl, context) {
18
+ if (!url) return void 0;
19
+ if (isAbsoluteUrl(url)) return url;
20
+ const base = (siteUrl || "").trim().replace(/\/+$/, "");
21
+ if (!base) {
22
+ if (context) warnOnce(context, url);
23
+ return void 0;
24
+ }
25
+ return url.startsWith("/") ? `${base}${url}` : `${base}/${url}`;
26
+ }
27
+
28
+ // src/lib/rss.ts
29
+ async function generateRSSFeed(fullContent = false) {
30
+ const env = getEnvConfig();
31
+ const config = getConfig();
32
+ if (!env.BLOGPANEL_SITE_URL) {
33
+ console.warn(
34
+ "[blogpanel] BLOGPANEL_SITE_URL is not set \u2014 the RSS feed's <link> and <atom:link> entries will be relative URLs, which most feed readers reject. Set BLOGPANEL_SITE_URL to your site's real domain before deploying."
35
+ );
36
+ }
37
+ const posts = await getCollection("nbk_posts");
38
+ const publishedPosts = await posts.find({ status: "published" }).sort({ publishedAt: -1 }).limit(50).toArray();
39
+ const items = publishedPosts.map((post) => {
40
+ const postUrl = `${env.BLOGPANEL_SITE_URL}${config.basePath}/${post.slug}`;
41
+ const content = fullContent ? post.contentHTML : post.excerpt;
42
+ const pubDate = post.publishedAt ? new Date(post.publishedAt).toUTCString() : new Date(post.createdAt).toUTCString();
43
+ let enclosure = "";
44
+ const enclosureUrl = absoluteUrl(
45
+ post.coverImage?.url,
46
+ env.BLOGPANEL_SITE_URL,
47
+ "an RSS <enclosure> image"
48
+ );
49
+ if (enclosureUrl) {
50
+ enclosure = `
51
+ <enclosure url="${escapeXml(enclosureUrl)}" type="image/jpeg" />`;
52
+ }
53
+ return ` <item>
54
+ <title>${escapeXml(post.title)}</title>
55
+ <link>${escapeXml(postUrl)}</link>
56
+ <guid isPermaLink="true">${escapeXml(postUrl)}</guid>
57
+ <description><![CDATA[${content || ""}]]></description>
58
+ <pubDate>${pubDate}</pubDate>${enclosure}${post.categories?.length ? post.categories.map((c) => `
59
+ <category>${escapeXml(c)}</category>`).join("") : ""}
60
+ </item>`;
61
+ }).join("\n");
62
+ const buildDate = (/* @__PURE__ */ new Date()).toUTCString();
63
+ return `<?xml version="1.0" encoding="UTF-8"?>
64
+ <rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:content="http://purl.org/rss/1.0/modules/content/">
65
+ <channel>
66
+ <title>${escapeXml(env.BLOGPANEL_SITE_NAME)} Blog</title>
67
+ <link>${escapeXml(env.BLOGPANEL_SITE_URL)}${config.basePath}</link>
68
+ <description>Latest posts from ${escapeXml(env.BLOGPANEL_SITE_NAME)}</description>
69
+ <language>en</language>
70
+ <lastBuildDate>${buildDate}</lastBuildDate>
71
+ <atom:link href="${escapeXml(env.BLOGPANEL_SITE_URL)}${escapeXml(config.apiPath)}/rss.xml" rel="self" type="application/rss+xml" />
72
+ ${items}
73
+ </channel>
74
+ </rss>`;
75
+ }
76
+ function escapeXml(str) {
77
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
78
+ }
79
+
80
+ export { absoluteUrl, generateRSSFeed, isAbsoluteUrl };
81
+ //# sourceMappingURL=chunk-VEJFN65Y.js.map
82
+ //# sourceMappingURL=chunk-VEJFN65Y.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/url.ts","../src/lib/rss.ts"],"names":[],"mappings":";;;AAqBO,SAAS,cAAc,GAAA,EAAsB;AAClD,EAAA,OAAO,uBAAuB,IAAA,CAAK,GAAG,CAAA,IAAK,GAAA,CAAI,WAAW,IAAI,CAAA;AAChE;AAEA,IAAM,MAAA,mBAAS,MAAA,CAAO,GAAA,CAAI,2BAA2B,CAAA;AAErD,SAAS,QAAA,CAAS,SAAiB,GAAA,EAAmB;AACpD,EAAA,MAAM,KAAA,GAAQ,UAAA;AACd,EAAA,MAAM,IAAA,GAAQ,KAAA,CAAA,MAAA,CAAA,KAAA,KAAA,CAAA,MAAA,CAAA,mBAAkB,IAAI,GAAA,EAAY,CAAA;AAChD,EAAA,IAAI,IAAA,CAAK,GAAA,CAAI,OAAO,CAAA,EAAG;AACvB,EAAA,IAAA,CAAK,IAAI,OAAO,CAAA;AAChB,EAAA,OAAA,CAAQ,IAAA;AAAA,IACN,CAAA,8CAAA,EAAiD,OAAO,CAAA,sDAAA,EAAyD,GAAG,CAAA,yNAAA;AAAA,GAGtH;AACF;AAeO,SAAS,WAAA,CACd,GAAA,EACA,OAAA,EACA,OAAA,EACoB;AACpB,EAAA,IAAI,CAAC,KAAK,OAAO,MAAA;AACjB,EAAA,IAAI,aAAA,CAAc,GAAG,CAAA,EAAG,OAAO,GAAA;AAE/B,EAAA,MAAM,QAAQ,OAAA,IAAW,EAAA,EAAI,MAAK,CAAE,OAAA,CAAQ,QAAQ,EAAE,CAAA;AACtD,EAAA,IAAI,CAAC,IAAA,EAAM;AACT,IAAA,IAAI,OAAA,EAAS,QAAA,CAAS,OAAA,EAAS,GAAG,CAAA;AAClC,IAAA,OAAO,MAAA;AAAA,EACT;AAEA,EAAA,OAAO,GAAA,CAAI,UAAA,CAAW,GAAG,CAAA,GAAI,CAAA,EAAG,IAAI,CAAA,EAAG,GAAG,CAAA,CAAA,GAAK,CAAA,EAAG,IAAI,CAAA,CAAA,EAAI,GAAG,CAAA,CAAA;AAC/D;;;AC/DA,eAAsB,eAAA,CAAgB,cAAc,KAAA,EAAwB;AAC1E,EAAA,MAAM,MAAM,YAAA,EAAa;AACzB,EAAA,MAAM,SAAS,SAAA,EAAU;AAOzB,EAAA,IAAI,CAAC,IAAI,kBAAA,EAAoB;AAC3B,IAAA,OAAA,CAAQ,IAAA;AAAA,MACN;AAAA,KACF;AAAA,EACF;AAEA,EAAA,MAAM,KAAA,GAAQ,MAAM,aAAA,CAAc,WAAW,CAAA;AAE7C,EAAA,MAAM,iBAAiB,MAAM,KAAA,CAC1B,KAAK,EAAE,MAAA,EAAQ,aAAa,CAAA,CAC5B,IAAA,CAAK,EAAE,aAAa,EAAA,EAAI,EACxB,KAAA,CAAM,EAAE,EACR,OAAA,EAAQ;AAEX,EAAA,MAAM,KAAA,GAAQ,cAAA,CACX,GAAA,CAAI,CAAC,IAAA,KAAS;AACb,IAAA,MAAM,OAAA,GAAU,GAAG,GAAA,CAAI,kBAAkB,GAAG,MAAA,CAAO,QAAQ,CAAA,CAAA,EAAI,IAAA,CAAK,IAAI,CAAA,CAAA;AACxE,IAAA,MAAM,OAAA,GAAU,WAAA,GAAc,IAAA,CAAK,WAAA,GAAc,IAAA,CAAK,OAAA;AACtD,IAAA,MAAM,OAAA,GAAU,IAAA,CAAK,WAAA,GACjB,IAAI,KAAK,IAAA,CAAK,WAAW,CAAA,CAAE,WAAA,KAC3B,IAAI,IAAA,CAAK,IAAA,CAAK,SAAS,EAAE,WAAA,EAAY;AAOzC,IAAA,IAAI,SAAA,GAAY,EAAA;AAChB,IAAA,MAAM,YAAA,GAAe,WAAA;AAAA,MACnB,KAAK,UAAA,EAAY,GAAA;AAAA,MACjB,GAAA,CAAI,kBAAA;AAAA,MACJ;AAAA,KACF;AACA,IAAA,IAAI,YAAA,EAAc;AAChB,MAAA,SAAA,GAAY;AAAA,sBAAA,EAA2B,SAAA,CAAU,YAAY,CAAC,CAAA,sBAAA,CAAA;AAAA,IAChE;AAEA,IAAA,OAAO,CAAA;AAAA,aAAA,EACE,SAAA,CAAU,IAAA,CAAK,KAAK,CAAC,CAAA;AAAA,YAAA,EACtB,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA,+BAAA,EACC,SAAA,CAAU,OAAO,CAAC,CAAA;AAAA,4BAAA,EACrB,WAAW,EAAE,CAAA;AAAA,eAAA,EAC1B,OAAO,CAAA,UAAA,EAAa,SAAS,CAAA,EACtC,IAAA,CAAK,UAAA,EAAY,MAAA,GACb,IAAA,CAAK,UAAA,CAAW,GAAA,CAAI,CAAC,CAAA,KAAc;AAAA,gBAAA,EAAqB,SAAA,CAAU,CAAC,CAAC,CAAA,WAAA,CAAa,EAAE,IAAA,CAAK,EAAE,IAC1F,EACN;AAAA,WAAA,CAAA;AAAA,EAEF,CAAC,CAAA,CACA,IAAA,CAAK,IAAI,CAAA;AAEZ,EAAA,MAAM,SAAA,GAAA,iBAAY,IAAI,IAAA,EAAK,EAAE,WAAA,EAAY;AAEzC,EAAA,OAAO,CAAA;AAAA;AAAA;AAAA,WAAA,EAGI,SAAA,CAAU,GAAA,CAAI,mBAAmB,CAAC,CAAA;AAAA,UAAA,EACnC,UAAU,GAAA,CAAI,kBAAkB,CAAC,CAAA,EAAG,OAAO,QAAQ,CAAA;AAAA,mCAAA,EAC1B,SAAA,CAAU,GAAA,CAAI,mBAAmB,CAAC,CAAA;AAAA;AAAA,mBAAA,EAElD,SAAS,CAAA;AAAA,qBAAA,EACP,SAAA,CAAU,IAAI,kBAAkB,CAAC,GAAG,SAAA,CAAU,MAAA,CAAO,OAAO,CAAC,CAAA;AAAA,EAClF,KAAK;AAAA;AAAA,MAAA,CAAA;AAGP;AAEA,SAAS,UAAU,GAAA,EAAqB;AACtC,EAAA,OAAO,IACJ,OAAA,CAAQ,IAAA,EAAM,OAAO,CAAA,CACrB,OAAA,CAAQ,MAAM,MAAM,CAAA,CACpB,QAAQ,IAAA,EAAM,MAAM,EACpB,OAAA,CAAQ,IAAA,EAAM,QAAQ,CAAA,CACtB,OAAA,CAAQ,MAAM,QAAQ,CAAA;AAC3B","file":"chunk-VEJFN65Y.js","sourcesContent":["/**\n * URL absolutisation for the contexts that require it.\n *\n * Deliberately free of any environment or configuration access, and free of\n * any Node built-in, so it can be imported from client components as well as\n * from server code. Callers supply the site URL; on the server that is\n * `getEnvConfig().BLOGPANEL_SITE_URL`, in a component it is a prop.\n *\n * Why absolutise at the point of consumption rather than at write time: the\n * stored URL has to survive the site moving. GridFS media URLs are persisted\n * into `nbk_media` documents and into the body of every post that embeds the\n * image, so anything baked into them is permanent — a localhost or staging\n * origin captured at upload time would follow the content to production with\n * no way back short of a database rewrite. `BLOGPANEL_SITE_URL` legitimately\n * differs between environments; the storage key does not. Keeping stored URLs\n * origin-relative and prefixing them in the handful of places that require an\n * absolute URL (Open Graph, Twitter cards, JSON-LD, RSS enclosures) keeps the\n * mutable part mutable.\n */\n\n/** True for `https://…`, `data:…`, `//cdn…` — anything already resolvable without a base. */\nexport function isAbsoluteUrl(url: string): boolean {\n return /^[a-z][a-z0-9+.-]*:/i.test(url) || url.startsWith('//');\n}\n\nconst warned = Symbol.for('next-blogpanel.url.warned');\n\nfunction warnOnce(context: string, url: string): void {\n const store = globalThis as unknown as Record<symbol, Set<string> | undefined>;\n const seen = (store[warned] ||= new Set<string>());\n if (seen.has(context)) return;\n seen.add(context);\n console.warn(\n `[blogpanel] BLOGPANEL_SITE_URL is not set, so ${context} was omitted rather than emitted as the relative URL \"${url}\". ` +\n `Open Graph, Twitter cards, schema.org and RSS all require absolute URLs; a relative one is not a smaller version of the right answer, it is invalid. ` +\n `Set BLOGPANEL_SITE_URL to your site's origin (no trailing slash).`\n );\n}\n\n/**\n * Returns `url` as an absolute URL, or `undefined` when it cannot be made one.\n *\n * Already-absolute inputs (R2 public URLs, `data:` URIs, protocol-relative\n * hosts) are returned untouched. Root-relative inputs (the GridFS default,\n * `/api/blog/media/file/…`) are prefixed with `siteUrl`. When `siteUrl` is\n * empty the result is `undefined` — callers must then omit the field, which\n * is the only honest option: emitting the bare relative path produces a\n * malformed `og:image`/`<enclosure url>` that consumers resolve against their\n * own origin, and emitting a half-formed absolute URL is worse still.\n *\n * Pass `context` to have the omission reported once per process.\n */\nexport function absoluteUrl(\n url: string | null | undefined,\n siteUrl: string | null | undefined,\n context?: string\n): string | undefined {\n if (!url) return undefined;\n if (isAbsoluteUrl(url)) return url;\n\n const base = (siteUrl || '').trim().replace(/\\/+$/, '');\n if (!base) {\n if (context) warnOnce(context, url);\n return undefined;\n }\n\n return url.startsWith('/') ? `${base}${url}` : `${base}/${url}`;\n}\n","import { getCollection } from './db';\nimport { getEnvConfig, getConfig } from './config';\nimport { absoluteUrl } from './url';\n\nexport async function generateRSSFeed(fullContent = false): Promise<string> {\n const env = getEnvConfig();\n const config = getConfig();\n\n // Same reasoning as generateSitemap(): RSS <link>/<atom:link> must be\n // absolute per the RSS spec, and most feed readers silently drop or\n // reject relative ones. Warn rather than throw — the blog itself still\n // renders fine without BLOGPANEL_SITE_URL set, only the feed's external\n // validity suffers.\n if (!env.BLOGPANEL_SITE_URL) {\n console.warn(\n '[blogpanel] BLOGPANEL_SITE_URL is not set — the RSS feed\\'s <link> and <atom:link> entries will be relative URLs, which most feed readers reject. Set BLOGPANEL_SITE_URL to your site\\'s real domain before deploying.'\n );\n }\n\n const posts = await getCollection('nbk_posts');\n\n const publishedPosts = await posts\n .find({ status: 'published' })\n .sort({ publishedAt: -1 })\n .limit(50)\n .toArray();\n\n const items = publishedPosts\n .map((post) => {\n const postUrl = `${env.BLOGPANEL_SITE_URL}${config.basePath}/${post.slug}`;\n const content = fullContent ? post.contentHTML : post.excerpt;\n const pubDate = post.publishedAt\n ? new Date(post.publishedAt).toUTCString()\n : new Date(post.createdAt).toUTCString();\n\n // RSS 2.0 requires <enclosure url> to be an absolute http(s) URL —\n // readers fetch it with no document base to resolve against. The GridFS\n // default produces root-relative URLs, so prefix them here; R2 URLs are\n // already absolute and pass through. Without a site URL the element is\n // dropped rather than emitted with a relative href.\n let enclosure = '';\n const enclosureUrl = absoluteUrl(\n post.coverImage?.url,\n env.BLOGPANEL_SITE_URL,\n 'an RSS <enclosure> image'\n );\n if (enclosureUrl) {\n enclosure = `\\n <enclosure url=\"${escapeXml(enclosureUrl)}\" type=\"image/jpeg\" />`;\n }\n\n return ` <item>\n <title>${escapeXml(post.title)}</title>\n <link>${escapeXml(postUrl)}</link>\n <guid isPermaLink=\"true\">${escapeXml(postUrl)}</guid>\n <description><![CDATA[${content || ''}]]></description>\n <pubDate>${pubDate}</pubDate>${enclosure}${\n post.categories?.length\n ? post.categories.map((c: string) => `\\n <category>${escapeXml(c)}</category>`).join('')\n : ''\n }\n </item>`;\n })\n .join('\\n');\n\n const buildDate = new Date().toUTCString();\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<rss version=\"2.0\" xmlns:atom=\"http://www.w3.org/2005/Atom\" xmlns:content=\"http://purl.org/rss/1.0/modules/content/\">\n <channel>\n <title>${escapeXml(env.BLOGPANEL_SITE_NAME)} Blog</title>\n <link>${escapeXml(env.BLOGPANEL_SITE_URL)}${config.basePath}</link>\n <description>Latest posts from ${escapeXml(env.BLOGPANEL_SITE_NAME)}</description>\n <language>en</language>\n <lastBuildDate>${buildDate}</lastBuildDate>\n <atom:link href=\"${escapeXml(env.BLOGPANEL_SITE_URL)}${escapeXml(config.apiPath)}/rss.xml\" rel=\"self\" type=\"application/rss+xml\" />\n${items}\n </channel>\n</rss>`;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n"]}
@@ -0,0 +1,74 @@
1
+ import { getEnvConfig, getConfig, getCollection } from './chunk-6PZ772NQ.js';
2
+
3
+ // src/lib/sitemap.ts
4
+ async function generateSitemap() {
5
+ const env = getEnvConfig();
6
+ const config = getConfig();
7
+ if (!env.BLOGPANEL_SITE_URL) {
8
+ console.warn(
9
+ "[blogpanel] BLOGPANEL_SITE_URL is not set \u2014 sitemap.xml <loc> entries will be relative URLs, which violates the sitemap protocol. Set BLOGPANEL_SITE_URL to your site's real domain before deploying."
10
+ );
11
+ }
12
+ const posts = await getCollection("nbk_posts");
13
+ const categories = await getCollection("nbk_categories");
14
+ const entries = [];
15
+ entries.push({
16
+ loc: `${env.BLOGPANEL_SITE_URL}${config.basePath}`,
17
+ changefreq: "daily",
18
+ priority: "0.9"
19
+ });
20
+ const publishedPosts = await posts.find({ status: "published" }).sort({ publishedAt: -1 }).project({ slug: 1, updatedAt: 1, publishedAt: 1 }).toArray();
21
+ for (const post of publishedPosts) {
22
+ const lastmod = post.updatedAt || post.publishedAt;
23
+ const daysSincePublish = lastmod ? Math.floor((Date.now() - new Date(lastmod).getTime()) / (1e3 * 60 * 60 * 24)) : 0;
24
+ let changefreq = "monthly";
25
+ if (daysSincePublish < 7) changefreq = "daily";
26
+ else if (daysSincePublish < 30) changefreq = "weekly";
27
+ entries.push({
28
+ loc: `${env.BLOGPANEL_SITE_URL}${config.basePath}/${post.slug}`,
29
+ lastmod: lastmod ? new Date(lastmod).toISOString().split("T")[0] : void 0,
30
+ changefreq,
31
+ priority: "0.8"
32
+ });
33
+ }
34
+ const allCategories = await categories.find({}).sort({ order: 1 }).project({ slug: 1 }).toArray();
35
+ for (const cat of allCategories) {
36
+ entries.push({
37
+ loc: `${env.BLOGPANEL_SITE_URL}${config.basePath}/category/${cat.slug}`,
38
+ changefreq: "weekly",
39
+ priority: "0.6"
40
+ });
41
+ }
42
+ const totalPosts = publishedPosts.length;
43
+ const postsPerPage = 10;
44
+ const totalPages = Math.ceil(totalPosts / postsPerPage);
45
+ for (let page = 2; page <= totalPages; page++) {
46
+ entries.push({
47
+ loc: `${env.BLOGPANEL_SITE_URL}${config.basePath}?page=${page}`,
48
+ changefreq: "weekly",
49
+ priority: "0.5"
50
+ });
51
+ }
52
+ return buildXML(entries);
53
+ }
54
+ function buildXML(entries) {
55
+ const urls = entries.map(
56
+ (entry) => ` <url>
57
+ <loc>${escapeXml(entry.loc)}</loc>${entry.lastmod ? `
58
+ <lastmod>${entry.lastmod}</lastmod>` : ""}${entry.changefreq ? `
59
+ <changefreq>${entry.changefreq}</changefreq>` : ""}${entry.priority ? `
60
+ <priority>${entry.priority}</priority>` : ""}
61
+ </url>`
62
+ ).join("\n");
63
+ return `<?xml version="1.0" encoding="UTF-8"?>
64
+ <urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
65
+ ${urls}
66
+ </urlset>`;
67
+ }
68
+ function escapeXml(str) {
69
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&apos;");
70
+ }
71
+
72
+ export { generateSitemap };
73
+ //# sourceMappingURL=chunk-WYZFAI6O.js.map
74
+ //# sourceMappingURL=chunk-WYZFAI6O.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/lib/sitemap.ts"],"names":[],"mappings":"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","file":"chunk-WYZFAI6O.js","sourcesContent":["import { getCollection } from './db';\nimport { getEnvConfig, getConfig } from './config';\n\ninterface SitemapEntry {\n loc: string;\n lastmod?: string;\n changefreq?: string;\n priority?: string;\n}\n\nexport async function generateSitemap(): Promise<string> {\n const env = getEnvConfig();\n const config = getConfig();\n\n // Sitemap protocol requires absolute URLs in <loc>. Without\n // BLOGPANEL_SITE_URL every entry below silently becomes a relative path\n // instead — a spec violation search engines will flag, not a rendering\n // error, so it's easy to ship unnoticed. Warn loudly rather than fail\n // silently; this stays a warning (not a throw) because the site itself\n // still works fine without it — only the sitemap's external validity\n // suffers.\n if (!env.BLOGPANEL_SITE_URL) {\n console.warn(\n '[blogpanel] BLOGPANEL_SITE_URL is not set — sitemap.xml <loc> entries will be relative URLs, which violates the sitemap protocol. Set BLOGPANEL_SITE_URL to your site\\'s real domain before deploying.'\n );\n }\n\n const posts = await getCollection('nbk_posts');\n const categories = await getCollection('nbk_categories');\n\n const entries: SitemapEntry[] = [];\n\n // Blog listing page\n entries.push({\n loc: `${env.BLOGPANEL_SITE_URL}${config.basePath}`,\n changefreq: 'daily',\n priority: '0.9',\n });\n\n // Published posts\n const publishedPosts = await posts\n .find({ status: 'published' })\n .sort({ publishedAt: -1 })\n .project({ slug: 1, updatedAt: 1, publishedAt: 1 })\n .toArray();\n\n for (const post of publishedPosts) {\n const lastmod = post.updatedAt || post.publishedAt;\n const daysSincePublish = lastmod\n ? Math.floor((Date.now() - new Date(lastmod).getTime()) / (1000 * 60 * 60 * 24))\n : 0;\n\n let changefreq = 'monthly';\n if (daysSincePublish < 7) changefreq = 'daily';\n else if (daysSincePublish < 30) changefreq = 'weekly';\n\n entries.push({\n loc: `${env.BLOGPANEL_SITE_URL}${config.basePath}/${post.slug}`,\n lastmod: lastmod ? new Date(lastmod).toISOString().split('T')[0] : undefined,\n changefreq,\n priority: '0.8',\n });\n }\n\n // Category pages\n const allCategories = await categories\n .find({})\n .sort({ order: 1 })\n .project({ slug: 1 })\n .toArray();\n\n for (const cat of allCategories) {\n entries.push({\n loc: `${env.BLOGPANEL_SITE_URL}${config.basePath}/category/${cat.slug}`,\n changefreq: 'weekly',\n priority: '0.6',\n });\n }\n\n // Paginated listing pages\n const totalPosts = publishedPosts.length;\n const postsPerPage = 10;\n const totalPages = Math.ceil(totalPosts / postsPerPage);\n for (let page = 2; page <= totalPages; page++) {\n entries.push({\n loc: `${env.BLOGPANEL_SITE_URL}${config.basePath}?page=${page}`,\n changefreq: 'weekly',\n priority: '0.5',\n });\n }\n\n return buildXML(entries);\n}\n\nfunction buildXML(entries: SitemapEntry[]): string {\n const urls = entries\n .map(\n (entry) => ` <url>\n <loc>${escapeXml(entry.loc)}</loc>${entry.lastmod ? `\\n <lastmod>${entry.lastmod}</lastmod>` : ''}${entry.changefreq ? `\\n <changefreq>${entry.changefreq}</changefreq>` : ''}${entry.priority ? `\\n <priority>${entry.priority}</priority>` : ''}\n </url>`\n )\n .join('\\n');\n\n return `<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">\n${urls}\n</urlset>`;\n}\n\nfunction escapeXml(str: string): string {\n return str\n .replace(/&/g, '&amp;')\n .replace(/</g, '&lt;')\n .replace(/>/g, '&gt;')\n .replace(/\"/g, '&quot;')\n .replace(/'/g, '&apos;');\n}\n"]}