@fullstackdatasolutions/articles 0.1.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 (53) hide show
  1. package/README.md +324 -0
  2. package/dist/index.cjs +1027 -0
  3. package/dist/index.cjs.map +1 -0
  4. package/dist/index.d.cts +232 -0
  5. package/dist/index.d.ts +232 -0
  6. package/dist/index.js +974 -0
  7. package/dist/index.js.map +1 -0
  8. package/dist/server.cjs +732 -0
  9. package/dist/server.cjs.map +1 -0
  10. package/dist/server.d.cts +127 -0
  11. package/dist/server.d.ts +127 -0
  12. package/dist/server.js +684 -0
  13. package/dist/server.js.map +1 -0
  14. package/package.json +82 -0
  15. package/src/ArticleCard.tsx +63 -0
  16. package/src/ArticleCategoryGrid.tsx +73 -0
  17. package/src/ArticleSchemas.tsx +82 -0
  18. package/src/ArticleSearchBar.tsx +50 -0
  19. package/src/ArticlesHero.tsx +33 -0
  20. package/src/ArticlesPage.tsx +167 -0
  21. package/src/CategoryArticlesPage.tsx +84 -0
  22. package/src/CommentForm.tsx +98 -0
  23. package/src/CommentItem.tsx +123 -0
  24. package/src/CommentThread.tsx +40 -0
  25. package/src/CommentsSection.tsx +84 -0
  26. package/src/FeaturedArticle.tsx +63 -0
  27. package/src/LatestArticles.tsx +42 -0
  28. package/src/LatestArticlesSection.tsx +68 -0
  29. package/src/__tests__/ArticleCard.test.tsx +78 -0
  30. package/src/__tests__/ArticleCategoryGrid.test.tsx +98 -0
  31. package/src/__tests__/ArticleSchemas.test.tsx +130 -0
  32. package/src/__tests__/ArticleSearchBar.test.tsx +79 -0
  33. package/src/__tests__/ArticlesHero.test.tsx +38 -0
  34. package/src/__tests__/ArticlesPage.test.tsx +155 -0
  35. package/src/__tests__/CategoryArticlesPage.test.tsx +156 -0
  36. package/src/__tests__/CommentForm.test.tsx +80 -0
  37. package/src/__tests__/CommentItem.test.tsx +149 -0
  38. package/src/__tests__/CommentsSection.test.tsx +118 -0
  39. package/src/__tests__/FeaturedArticle.test.tsx +85 -0
  40. package/src/__tests__/LatestArticles.test.tsx +157 -0
  41. package/src/__tests__/LatestArticlesSection.test.tsx +105 -0
  42. package/src/__tests__/jest-dom.d.ts +1 -0
  43. package/src/__tests__/seoUtils.test.ts +217 -0
  44. package/src/__tests__/useArticles.test.ts +207 -0
  45. package/src/articleTypes.ts +21 -0
  46. package/src/articlesConfig.ts +98 -0
  47. package/src/commentTypes.ts +14 -0
  48. package/src/index.ts +35 -0
  49. package/src/markdown.ts +314 -0
  50. package/src/seoUtils.ts +155 -0
  51. package/src/server-articles.ts +265 -0
  52. package/src/server.ts +25 -0
  53. package/src/useArticles.ts +93 -0
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/server.ts","../src/server-articles.ts","../src/markdown.ts","../src/seoUtils.ts"],"sourcesContent":["// Server-only exports — uses fs/path; never import this in a client bundle\nexport {\n getAllArticles,\n getArticleMetadata,\n getAvailableArticleSlugs,\n getAdjacentArticles,\n searchArticles,\n getAllCategories,\n getArticlesByCategory,\n categoryToSlug,\n sanitizeImagePath,\n} from './server-articles'\n\nexport {\n generateArticleStaticParams,\n generateCategoryStaticParams,\n generateArticleMetadata,\n generateCategoryMetadata,\n getArticleSitemapEntries,\n} from './seoUtils'\n\nexport { markdownToHtml } from './markdown'\n\nexport type { Article, CategoryInfo } from './articleTypes'\nexport type { ArticlesConfig } from './articlesConfig'\n","import { cache } from 'react'\nimport matter from 'gray-matter'\nimport fs from 'node:fs'\nimport path from 'node:path'\nimport readingTime from 'reading-time'\nimport { markdownToHtml } from './markdown'\nimport type { Article, CategoryInfo } from './articleTypes'\n\nconst articlesDirectory = path.join(process.cwd(), 'public/articles')\n\nfunction getReadingTime(content: string): string {\n return readingTime(content).text\n}\n\nfunction findArticleImage(slug: string): string | null {\n try {\n const articleDir = path.join(articlesDirectory, slug)\n const entries: (fs.Dirent | string)[] = fs.readdirSync(articleDir, { withFileTypes: true })\n const names = entries\n .map((entry) => {\n if (typeof entry === 'string') return entry\n if (entry && typeof entry.name === 'string') return entry.name\n return null\n })\n .filter((name): name is string => Boolean(name))\n const imageExtensions = ['.png', '.jpg', '.jpeg', '.gif', '.webp']\n const imageFileName = names.find((name) =>\n imageExtensions.some((ext) => name.toLowerCase().endsWith(ext))\n )\n return imageFileName ? sanitizeImagePath(imageFileName, slug) : null\n } catch {\n return null\n }\n}\n\nfunction sanitizeImagePath(rawPath: string, articleSlug: string): string | null {\n if (!rawPath || typeof rawPath !== 'string') return null\n const cleanPath = rawPath.replaceAll(/[\\x00-\\x1f\\x7f-\\x9f]/g, '')\n if (cleanPath.startsWith('http://') || cleanPath.startsWith('https://')) return cleanPath\n if (cleanPath.includes('..') || cleanPath.includes('\\\\') || cleanPath.startsWith('/')) {\n console.warn(`Potentially unsafe image path detected: ${cleanPath}`)\n return null\n }\n if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {\n console.warn(`Invalid characters in image path: ${cleanPath}`)\n return null\n }\n if (cleanPath.includes('/')) {\n const normalizedPath = path.normalize(cleanPath)\n if (normalizedPath.startsWith('..') || normalizedPath.includes('../')) {\n console.warn(`Path traversal attempt detected: ${cleanPath}`)\n return null\n }\n return `/articles/${articleSlug}/${cleanPath}`\n }\n return `/articles/${articleSlug}/${cleanPath}`\n}\n\nexport function getAvailableArticleSlugs(): string[] {\n try {\n if (!fs.existsSync(articlesDirectory)) return []\n const items = fs.readdirSync(articlesDirectory, { withFileTypes: true })\n return items\n .filter((item) => item.isDirectory())\n .filter((dir) => {\n const articleDir = path.join(articlesDirectory, dir.name)\n try {\n const entries: (fs.Dirent | string)[] = fs.readdirSync(articleDir, {\n withFileTypes: true,\n })\n return entries.some((entry) => {\n const name = typeof entry === 'string' ? entry : entry?.name\n if (name !== 'article.md') return false\n if (typeof entry === 'string') return true\n if (typeof entry?.isFile === 'function') return entry.isFile()\n return true\n })\n } catch {\n return false\n }\n })\n .map((dir) => dir.name)\n } catch (error) {\n console.error('Error reading articles directory:', error)\n return []\n }\n}\n\nasync function getArticleSummary(slug: string): Promise<Article | null> {\n try {\n const articlePath = path.join(articlesDirectory, slug, 'article.md')\n if (!fs.existsSync(articlePath)) return null\n const fileContent = fs.readFileSync(articlePath, 'utf8')\n const { data, content: markdownContent } = matter(fileContent)\n const readTime = getReadingTime(markdownContent)\n let featuredImage = data.featuredImage || ''\n if (featuredImage && !featuredImage.startsWith('http')) {\n const sanitizedPath = sanitizeImagePath(featuredImage, slug)\n featuredImage = sanitizedPath || '/placeholder-logo.png'\n } else if (!featuredImage) {\n featuredImage = findArticleImage(slug) || '/placeholder-logo.png'\n }\n let articleDate: string | undefined\n if (data.date) {\n try {\n const parsedDate = new Date(data.date)\n if (!Number.isNaN(parsedDate.getTime())) {\n articleDate = parsedDate.toISOString().split('T')[0]\n }\n } catch {\n console.warn(`Invalid date for article ${slug}: ${data.date}`)\n }\n }\n const allTags: string[] = Array.isArray(data.tags)\n ? data.tags.filter((t: unknown) => typeof t === 'string' && String(t).trim())\n : []\n const categories: string[] =\n allTags.length > 0 ? allTags.map((t: string) => t.replaceAll('-', ' ').trim()) : ['Campaigns']\n return {\n slug,\n title: data.title || slug.replaceAll('-', ' '),\n excerpt: data.excerpt || '',\n date: articleDate,\n author: data.author || 'Andrew Blase',\n category: categories[0],\n categories,\n readTime,\n featuredImage,\n tags: data.tags || [],\n }\n } catch (error) {\n console.error(`Error loading article ${slug}:`, error)\n return null\n }\n}\n\nexport const getArticleMetadata = cache(async (slug: string): Promise<Article | null> => {\n try {\n const articlePath = path.join(articlesDirectory, slug, 'article.md')\n if (!fs.existsSync(articlePath)) return null\n const fileContent = fs.readFileSync(articlePath, 'utf8')\n const { data, content: markdownContent } = matter(fileContent)\n const htmlContent = await markdownToHtml(markdownContent, slug)\n const readTime = getReadingTime(markdownContent)\n let featuredImage = data.featuredImage || ''\n if (featuredImage && !featuredImage.startsWith('http')) {\n const sanitizedPath = sanitizeImagePath(featuredImage, slug)\n featuredImage = sanitizedPath || '/placeholder-logo.png'\n } else if (!featuredImage) {\n featuredImage = findArticleImage(slug) || '/placeholder-logo.png'\n }\n let articleDate: string | undefined\n if (data.date) {\n try {\n const parsedDate = new Date(data.date)\n if (!Number.isNaN(parsedDate.getTime())) {\n articleDate = parsedDate.toISOString().split('T')[0]\n }\n } catch {\n console.warn(`Invalid date for article ${slug}: ${data.date}`)\n }\n }\n const allTags: string[] = Array.isArray(data.tags)\n ? data.tags.filter((t: unknown) => typeof t === 'string' && String(t).trim())\n : []\n const categories: string[] =\n allTags.length > 0 ? allTags.map((t: string) => t.replaceAll('-', ' ').trim()) : ['Campaigns']\n return {\n slug,\n title: data.title || slug.replaceAll('-', ' '),\n excerpt: data.excerpt || '',\n date: articleDate,\n author: data.author || 'Andrew Blase',\n category: categories[0],\n categories,\n readTime,\n featuredImage,\n tags: data.tags || [],\n content: markdownContent,\n htmlContent,\n }\n } catch (error) {\n console.error(`Error loading article ${slug}:`, error)\n return null\n }\n})\n\nexport const getAllArticles = cache(async (): Promise<Article[]> => {\n const slugs = getAvailableArticleSlugs()\n const articles = await Promise.all(slugs.map((slug) => getArticleSummary(slug)))\n const currentDate = new Date().toISOString().split('T')[0]\n return articles\n .filter((article): article is Article => article !== null)\n .filter((article) => !article.date || article.date <= currentDate)\n .sort((a, b) => {\n if (!a.date && !b.date) return 0\n if (!a.date) return 1\n if (!b.date) return -1\n return new Date(b.date).getTime() - new Date(a.date).getTime()\n })\n})\n\nexport async function getAdjacentArticles(\n currentSlug: string\n): Promise<{ previous: Article | null; next: Article | null }> {\n const allArticles = await getAllArticles()\n const currentIndex = allArticles.findIndex((article) => article.slug === currentSlug)\n if (currentIndex === -1) return { previous: null, next: null }\n const previous = currentIndex < allArticles.length - 1 ? allArticles[currentIndex + 1] : null\n const next = currentIndex > 0 ? allArticles[currentIndex - 1] : null\n return { previous, next }\n}\n\nexport async function searchArticles(query: string): Promise<Article[]> {\n if (!query?.trim()) return getAllArticles()\n const articles = await getAllArticles()\n const searchTerm = query.toLowerCase().trim()\n return articles.filter((article) => {\n const matchesTitle = article.title.toLowerCase().includes(searchTerm)\n const matchesExcerpt = article.excerpt.toLowerCase().includes(searchTerm)\n const matchesAuthor = article.author.toLowerCase().includes(searchTerm)\n const matchesCategory = article.categories.some((cat) => cat.toLowerCase().includes(searchTerm))\n const matchesTags = article.tags?.some((tag) => tag.toLowerCase().includes(searchTerm))\n return (\n matchesTitle || matchesExcerpt || matchesAuthor || matchesCategory || Boolean(matchesTags)\n )\n })\n}\n\nexport function categoryToSlug(category: string): string {\n return category\n .toLowerCase()\n .replaceAll(/\\s+/g, '-')\n .replaceAll(/[^a-z0-9-]/g, '')\n}\n\nexport async function getAllCategories(): Promise<CategoryInfo[]> {\n const articles = await getAllArticles()\n const categoryMap = new Map<string, { count: number; featuredImage: string }>()\n for (const article of articles) {\n for (const cat of article.categories) {\n if (!categoryMap.has(cat)) {\n categoryMap.set(cat, { count: 0, featuredImage: article.featuredImage })\n }\n categoryMap.get(cat)!.count++\n }\n }\n return Array.from(categoryMap.entries())\n .map(([name, { count, featuredImage }]) => ({\n name,\n slug: categoryToSlug(name),\n count,\n featuredImage,\n }))\n .sort((a, b) => b.count - a.count)\n}\n\nexport async function getArticlesByCategory(categorySlug: string): Promise<Article[]> {\n const articles = await getAllArticles()\n return articles.filter((article) =>\n article.categories.some((cat) => categoryToSlug(cat) === categorySlug)\n )\n}\n\nexport { sanitizeImagePath }\n","import type { Element, Root, ElementContent } from 'hast'\nimport rehypePrism from 'rehype-prism-plus'\nimport rehypeSanitize from 'rehype-sanitize'\nimport rehypeStringify from 'rehype-stringify'\nimport { remark } from 'remark'\nimport remarkGfm from 'remark-gfm'\nimport remarkParse from 'remark-parse'\nimport remarkRehype from 'remark-rehype'\nimport { Plugin } from 'unified'\nimport { visit } from 'unist-util-visit'\n\n// Import the sanitizeImagePath function\nfunction sanitizeImagePath(rawPath: string, articleSlug: string): string | null {\n if (!rawPath || typeof rawPath !== 'string') {\n return null\n }\n\n // Remove any null bytes or control characters\n const cleanPath = rawPath.replaceAll(/[\\x00-\\x1f\\x7f-\\x9f]/g, '')\n\n // Check for absolute URLs (http/https)\n if (cleanPath.startsWith('http://') || cleanPath.startsWith('https://')) {\n // For external URLs, just return as-is (they're safe)\n return cleanPath\n }\n\n // For relative paths, ensure they don't contain dangerous patterns\n if (cleanPath.includes('..') || cleanPath.includes('\\\\') || cleanPath.startsWith('/')) {\n console.warn(`Potentially unsafe image path detected: ${cleanPath}`)\n return null\n }\n\n // Only allow alphanumeric characters, hyphens, underscores, dots, and forward slashes\n if (!/^[a-zA-Z0-9._/-]+$/.test(cleanPath)) {\n console.warn(`Invalid characters in image path: ${cleanPath}`)\n return null\n }\n\n // Construct safe path within articles directory\n if (cleanPath.includes('/')) {\n // Relative path, ensure it's within the article directory\n const normalizedPath = cleanPath.replaceAll('\\\\', '/')\n if (normalizedPath.startsWith('..') || normalizedPath.includes('../')) {\n console.warn(`Path traversal attempt detected: ${cleanPath}`)\n return null\n }\n return `/articles/${articleSlug}/${normalizedPath}`\n } else {\n // Just a filename, construct full path\n return `/articles/${articleSlug}/${cleanPath}`\n }\n}\n\nconst customRenderer: Plugin<[], Root> = () => {\n return (tree: Root) => {\n // First pass: Apply general styles\n visit(tree, 'element', (node: Element) => {\n if (node.tagName) {\n const props = node.properties || {}\n\n switch (node.tagName) {\n case 'h1':\n props.className = 'text-3xl font-bold text-foreground mt-8 mb-4 scroll-mt-20'\n break\n case 'h2':\n props.className = 'text-2xl font-semibold text-foreground mt-6 mb-3 scroll-mt-20'\n break\n case 'h3':\n props.className = 'text-xl font-semibold text-foreground mt-4 mb-2 scroll-mt-20'\n break\n case 'h4':\n props.className = 'text-lg font-semibold text-foreground mt-3 mb-2 scroll-mt-20'\n break\n case 'p':\n props.className = 'text-muted-foreground leading-relaxed mb-4'\n break\n case 'a':\n props.className = 'text-primary hover:underline transition-colors duration-200'\n props.target = '_blank'\n props.rel = 'noopener noreferrer'\n break\n case 'ul':\n props.className = 'list-disc list-inside mb-4 space-y-2 ml-4'\n break\n case 'ol':\n props.className = 'list-decimal list-inside mb-4 space-y-2 ml-4'\n break\n case 'li':\n props.className = 'mb-1'\n break\n case 'blockquote':\n props.className = 'border-l-4 border-primary pl-4 italic my-4 text-muted-foreground'\n break\n case 'code':\n props.className =\n (props.className ? props.className + ' ' : '') +\n 'bg-muted px-1 py-0.5 rounded text-sm font-mono'\n break\n case 'pre':\n props.className = 'bg-muted rounded-lg p-4 overflow-x-auto my-4'\n break\n case 'img':\n props.className = 'rounded-lg my-6 w-full max-w-2xl mx-auto'\n break\n case 'table':\n props.className = 'border-collapse border border-border my-4 w-full'\n break\n case 'th':\n props.className = 'border border-border px-2 py-1 bg-muted font-semibold'\n break\n case 'td':\n props.className = 'border border-border px-2 py-1'\n break\n case 'hr':\n props.className = 'my-8 border-border'\n break\n default:\n break\n }\n\n node.properties = props\n }\n })\n\n // Second pass: Fix footnotes and references\n visit(tree, 'element', (node: Element) => {\n // Handle footnote references in the body\n if (\n node.tagName === 'sup' &&\n node.children?.[0]?.type === 'element' &&\n node.children[0].tagName === 'a'\n ) {\n const link = node.children[0] as Element\n if (\n link.properties?.href &&\n typeof link.properties.href === 'string' &&\n link.properties.href.startsWith('#user-content-fn-')\n ) {\n // Remove target and rel, and update class\n if (link.properties) {\n delete link.properties.target\n delete link.properties.rel\n link.properties.className = 'text-primary hover:underline'\n }\n\n // Update link href\n link.properties.href = link.properties.href.replaceAll('#user-content-fn-', '#footnote-')\n\n // Move ID from sup to link and update format\n if (node.properties?.id && typeof node.properties.id === 'string') {\n const newId = node.properties.id.replaceAll('user-content-fnref-', 'ref-')\n link.properties.id = newId\n delete node.properties.id\n }\n\n // Add brackets to content\n if (link.children?.[0]?.type === 'text') {\n link.children[0].value = `[${link.children[0].value}]`\n }\n }\n }\n\n // Handle footnotes section\n if (\n node.tagName === 'section' &&\n (Array.isArray(node.properties?.className)\n ? node.properties.className.includes('footnotes')\n : node.properties?.className === 'footnotes')\n ) {\n // Find the ol\n const olCandidate = node.children.find(\n (child) => child.type === 'element' && (child as Element).tagName === 'ol'\n )\n if (olCandidate?.type === 'element') {\n const ol = olCandidate as Element\n ol.properties.className = 'list-decimal ml-6 space-y-2 text-sm text-muted-foreground'\n\n // Process list items\n ol.children.forEach((li) => {\n if (li.type === 'element' && li.tagName === 'li') {\n const liEl = li as Element\n if (liEl.properties) {\n liEl.properties.className = 'pl-2'\n // Update ID: user-content-fn-1 -> footnote-1\n if (typeof liEl.properties.id === 'string') {\n liEl.properties.id = liEl.properties.id.replaceAll(\n 'user-content-fn-',\n 'footnote-'\n )\n }\n }\n\n // Unwrap p tags inside li\n const pIndex = liEl.children.findIndex(\n (child) => child.type === 'element' && (child as Element).tagName === 'p'\n )\n\n if (pIndex !== -1) {\n const p = liEl.children[pIndex] as Element\n liEl.children.splice(pIndex, 1, ...p.children)\n }\n\n // Style links inside the list item\n const styleLinks = (nodes: ElementContent[]): void => {\n nodes.forEach((n) => {\n if (n.type !== 'element') return\n const el = n as Element\n if (el.tagName === 'a') {\n const isBackRef =\n el.properties?.['dataFootnoteBackref'] !== undefined ||\n (el.children[0]?.type === 'text' && el.children[0].value === '↩')\n\n if (isBackRef) {\n el.properties.className = 'text-primary hover:underline ml-1'\n // Update backref href: #user-content-fnref-1 -> #ref-1\n if (typeof el.properties.href === 'string') {\n el.properties.href = el.properties.href.replaceAll(\n '#user-content-fnref-',\n '#ref-'\n )\n }\n } else {\n el.properties.className = 'text-primary hover:underline break-all'\n }\n }\n if (el.children) styleLinks(el.children)\n })\n }\n\n styleLinks(liEl.children)\n }\n })\n\n // Reconstruct section children with HR and H3\n const hr: Element = {\n type: 'element',\n tagName: 'hr',\n properties: { className: 'my-8 border-border' },\n children: [],\n }\n\n const h3: Element = {\n type: 'element',\n tagName: 'h3',\n properties: { className: 'text-lg font-semibold mb-4' },\n children: [{ type: 'text', value: 'References' }],\n }\n\n node.children = [hr, h3, ol]\n\n // Remove the footnotes class to avoid default styling interference\n if (node.properties) {\n node.properties.className = undefined\n }\n }\n }\n })\n }\n}\n\n// Custom rehype plugin to process image URLs\nconst rehypeProcessImages: Plugin<[{ articleSlug?: string }], Root> = (options = {}) => {\n return (tree: Root) => {\n visit(tree, 'element', (node: Element) => {\n if (node.tagName === 'img' && node.properties) {\n const src = node.properties.src\n if (src && typeof src === 'string' && options.articleSlug) {\n // Sanitize the image path\n const sanitizedSrc = sanitizeImagePath(src, options.articleSlug)\n if (sanitizedSrc) {\n node.properties.src = sanitizedSrc\n } else {\n // If sanitization fails, use a placeholder\n console.warn(`Using placeholder for unsafe image path: ${src}`)\n node.properties.src = '/placeholder-logo.png'\n }\n }\n }\n })\n }\n}\n\nexport async function markdownToHtml(markdown: string, articleSlug?: string) {\n try {\n // Start building the remark processor\n let processor = remark()\n .use(remarkParse)\n .use(remarkGfm)\n .use(remarkRehype)\n .use(customRenderer)\n // @ts-ignore\n .use(rehypePrism)\n .use(rehypeSanitize, {\n attributes: {\n '*': ['className', 'class', 'id'],\n a: ['href', 'target', 'rel', 'id'],\n img: ['src', 'alt'],\n },\n })\n\n // Add image processing plugin if articleSlug is provided\n if (articleSlug) {\n processor = processor.use(rehypeProcessImages, { articleSlug })\n }\n\n const result = await processor.use(rehypeStringify).process(markdown)\n\n return result.toString()\n } catch (error) {\n console.error('Markdown conversion error:', error)\n // Return the original markdown as fallback\n return markdown\n }\n}\n","import type { Metadata, MetadataRoute } from 'next'\nimport {\n getArticleMetadata,\n getAllArticles,\n getAllCategories,\n getArticlesByCategory,\n getAvailableArticleSlugs,\n} from './server-articles'\nimport { ArticlesConfig } from './articlesConfig'\n\nexport function generateArticleStaticParams(): { slug: string }[] {\n return getAvailableArticleSlugs().map((slug) => ({ slug }))\n}\n\nexport async function generateCategoryStaticParams(): Promise<{ category: string }[]> {\n const categories = await getAllCategories()\n return categories.map((cat) => ({ category: cat.slug }))\n}\n\nfunction resolveImageUrl(featuredImage: string, siteUrl: string): string {\n const base = siteUrl.replace(/\\/$/, '')\n if (featuredImage.startsWith('http://') || featuredImage.startsWith('https://')) {\n return featuredImage\n }\n return `${base}/${featuredImage.replace(/^\\/+/, '')}`\n}\n\nexport async function generateArticleMetadata(\n slug: string,\n config: ArticlesConfig\n): Promise<Metadata> {\n const article = await getArticleMetadata(slug)\n\n if (!article) {\n return {\n title: 'Article Not Found',\n description: 'The requested article could not be found.',\n }\n }\n\n const siteUrl = config.siteUrl.replace(/\\/$/, '')\n const articleUrl = `${siteUrl}/articles/${slug}`\n const imageUrl = article.featuredImage\n ? resolveImageUrl(article.featuredImage, siteUrl)\n : `${siteUrl}/placeholder-logo.png`\n const description = article.excerpt ?? `Read ${article.title} on ${config.siteName}.`\n\n return {\n title: `${article.title} | ${config.siteName}`,\n description,\n keywords: [\n 'volunteer management software',\n 'political campaign software',\n 'campaign operations',\n 'civic tech',\n ...(article.tags ?? []).map((tag) => tag.toLowerCase()),\n ].join(', '),\n openGraph: {\n title: article.title,\n description,\n url: articleUrl,\n siteName: config.siteName,\n images: [{ url: imageUrl, width: 1200, height: 630, alt: article.title }],\n locale: 'en_US',\n type: 'article',\n ...(article.date && { publishedTime: article.date }),\n authors: [article.author],\n tags: article.tags ?? [],\n },\n twitter: {\n card: 'summary_large_image',\n title: article.title,\n description,\n images: [imageUrl],\n },\n alternates: {\n canonical: articleUrl,\n },\n robots: {\n index: true,\n follow: true,\n googleBot: {\n index: true,\n follow: true,\n 'max-video-preview': -1,\n 'max-image-preview': 'large',\n 'max-snippet': -1,\n },\n },\n other: {\n 'article:author': article.author,\n ...(article.date && {\n 'article:published_time': new Date(article.date).toISOString(),\n }),\n 'article:section': article.category,\n 'article:tag': article.tags?.join(',') ?? '',\n 'linkedin:owner': process.env.NEXT_PUBLIC_LINKEDIN_COMPANY_ID ?? '',\n },\n }\n}\n\nexport async function generateCategoryMetadata(\n categorySlug: string,\n config: ArticlesConfig\n): Promise<Metadata> {\n const articles = await getArticlesByCategory(categorySlug)\n\n if (articles.length === 0) return { title: 'Category Not Found' }\n\n const categoryName = articles[0].category\n const siteUrl = config.siteUrl.replace(/\\/$/, '')\n const categoryUrl = `${siteUrl}/articles/category/${categorySlug}`\n const raw = config.categoryDescriptions?.[categorySlug]\n const fallback = `Browse ${articles.length} article${articles.length === 1 ? '' : 's'} in the ${categoryName} category.`\n const description = typeof raw === 'string' ? raw : (raw?.short ?? fallback)\n\n return {\n title: `${categoryName} Articles | ${config.siteName}`,\n description,\n openGraph: {\n title: `${categoryName} Articles`,\n description,\n url: categoryUrl,\n siteName: config.siteName,\n images: [{ url: articles[0].featuredImage }],\n },\n alternates: {\n canonical: categoryUrl,\n },\n }\n}\n\nexport async function getArticleSitemapEntries(baseUrl: string): Promise<MetadataRoute.Sitemap> {\n try {\n const [articles, categories] = await Promise.all([getAllArticles(), getAllCategories()])\n\n const articleEntries: MetadataRoute.Sitemap = articles.map((article) => ({\n url: `${baseUrl}/articles/${article.slug}`,\n lastModified: article.date ? new Date(article.date) : undefined,\n changeFrequency: 'weekly' as const,\n priority: 0.8,\n }))\n\n const categoryEntries: MetadataRoute.Sitemap = categories.map((cat) => ({\n url: `${baseUrl}/articles/category/${cat.slug}`,\n lastModified: new Date(),\n changeFrequency: 'weekly' as const,\n priority: 0.7,\n }))\n\n return [...articleEntries, ...categoryEntries]\n } catch {\n return []\n }\n}\n"],"mappings":"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","names":["sanitizeImagePath","_a","_b","remarkParse","remarkGfm","remarkRehype","rehypePrism","rehypeSanitize","rehypeStringify","path","readingTime","fs","sanitizeImagePath","matter"]}
@@ -0,0 +1,127 @@
1
+ import { Metadata, MetadataRoute } from 'next';
2
+
3
+ interface Article {
4
+ slug: string;
5
+ title: string;
6
+ excerpt: string;
7
+ date?: string;
8
+ author: string;
9
+ category: string;
10
+ categories: string[];
11
+ readTime: string;
12
+ featuredImage: string;
13
+ tags?: string[];
14
+ content?: string;
15
+ htmlContent?: string;
16
+ }
17
+ interface CategoryInfo {
18
+ name: string;
19
+ slug: string;
20
+ count: number;
21
+ featuredImage: string;
22
+ }
23
+
24
+ declare function sanitizeImagePath(rawPath: string, articleSlug: string): string | null;
25
+ declare function getAvailableArticleSlugs(): string[];
26
+ declare const getArticleMetadata: (slug: string) => Promise<Article | null>;
27
+ declare const getAllArticles: () => Promise<Article[]>;
28
+ declare function getAdjacentArticles(currentSlug: string): Promise<{
29
+ previous: Article | null;
30
+ next: Article | null;
31
+ }>;
32
+ declare function searchArticles(query: string): Promise<Article[]>;
33
+ declare function categoryToSlug(category: string): string;
34
+ declare function getAllCategories(): Promise<CategoryInfo[]>;
35
+ declare function getArticlesByCategory(categorySlug: string): Promise<Article[]>;
36
+
37
+ /** Keys for each renderable section of the articles listing page. */
38
+ type ArticlesSection = 'hero' | 'search' | 'featured' | 'latest' | 'categories' | 'newsletter';
39
+ /**
40
+ * CSS custom-property overrides applied to the library's wrapper div.
41
+ * All fields are optional; omitted fields fall back to the consuming app's Tailwind theme.
42
+ */
43
+ interface ArticlesTheme {
44
+ /** Font family for the articles section. Example: `"'Inter', sans-serif"` */
45
+ fontFamily?: string;
46
+ /** Color for article card titles and section headings. Example: `'#111827'` */
47
+ headerColor?: string;
48
+ /** Color for body and excerpt text. Example: `'#6b7280'` */
49
+ textColor?: string;
50
+ /** Background color for sections and cards. Example: `'#ffffff'` */
51
+ backgroundColor?: string;
52
+ /** Color for "Read More" links and inline links. Example: `'#4f46e5'` */
53
+ linkColor?: string;
54
+ /** Hover color for links. Example: `'#4338ca'` */
55
+ linkHoverColor?: string;
56
+ /** Text decoration for links. Values: `'underline'` | `'none'` */
57
+ linkTextDecoration?: string;
58
+ /** Font weight for headings. Example: `700` */
59
+ headerFontWeight?: string | number;
60
+ /** Font size for article card titles. Example: `'1.25rem'` */
61
+ headerFontSize?: string;
62
+ /** Font size for body text. Example: `'1rem'` */
63
+ bodyFontSize?: string;
64
+ /** Line height for body text. Example: `'1.75'` */
65
+ lineHeight?: string;
66
+ /** Border radius for cards. Example: `'0.5rem'` */
67
+ borderRadius?: string;
68
+ }
69
+ /** Per-category text used in meta descriptions and on the category hero page. */
70
+ interface CategoryDescription {
71
+ /** Short description — used in meta description and category grid card. */
72
+ short: string;
73
+ /** Long description — rendered as a hero paragraph on the category page. */
74
+ long?: string;
75
+ }
76
+ /** Controls the comments feature. Attach to `ArticlesConfig.comments`. */
77
+ interface CommentsConfig {
78
+ /** Set to `true` to enable comments globally across all articles. */
79
+ enabled: boolean;
80
+ /** Maximum reply nesting depth. Default: `1` (replies to top-level only). */
81
+ maxDepth?: number;
82
+ /**
83
+ * Per-article overrides keyed by slug.
84
+ * Example: `{ 'sensitive-article': false }` disables comments on that article only.
85
+ */
86
+ perArticleOverride?: Record<string, boolean>;
87
+ }
88
+ /** Top-level configuration object. Pass one instance to every library component. */
89
+ interface ArticlesConfig {
90
+ /** Canonical base URL of the site, used in metadata and JSON-LD. Example: `'https://yoursite.com'` */
91
+ siteUrl: string;
92
+ /** Site name shown in metadata titles and JSON-LD publisher fields. Example: `'Vox Populus'` */
93
+ siteName: string;
94
+ /** Number of articles shown per page in the grid and loaded on each "Load more" click. Default: `6` */
95
+ pageSize?: number;
96
+ /** Number of category cards shown before a "Load more categories" button appears. Default: `8` */
97
+ categoriesPageSize?: number;
98
+ /**
99
+ * Ordered list of sections to render on the articles listing page.
100
+ * Omit a key to hide that section entirely. Default: `['hero','search','featured','latest','categories']`
101
+ */
102
+ layout?: ArticlesSection[];
103
+ /** CSS custom-property overrides for colors, fonts, and spacing. All fields optional. */
104
+ theme?: ArticlesTheme;
105
+ /**
106
+ * Descriptive text for each category, keyed by category slug.
107
+ * A plain string is treated as the short description only.
108
+ * Example: `{ 'campaigns': { short: 'Campaign tips', long: 'Full paragraph...' } }`
109
+ */
110
+ categoryDescriptions?: Record<string, string | CategoryDescription>;
111
+ /** Comments configuration. Omit or set `enabled: false` to hide comments entirely. */
112
+ comments?: CommentsConfig;
113
+ }
114
+
115
+ declare function generateArticleStaticParams(): {
116
+ slug: string;
117
+ }[];
118
+ declare function generateCategoryStaticParams(): Promise<{
119
+ category: string;
120
+ }[]>;
121
+ declare function generateArticleMetadata(slug: string, config: ArticlesConfig): Promise<Metadata>;
122
+ declare function generateCategoryMetadata(categorySlug: string, config: ArticlesConfig): Promise<Metadata>;
123
+ declare function getArticleSitemapEntries(baseUrl: string): Promise<MetadataRoute.Sitemap>;
124
+
125
+ declare function markdownToHtml(markdown: string, articleSlug?: string): Promise<string>;
126
+
127
+ export { type Article, type ArticlesConfig, type CategoryInfo, categoryToSlug, generateArticleMetadata, generateArticleStaticParams, generateCategoryMetadata, generateCategoryStaticParams, getAdjacentArticles, getAllArticles, getAllCategories, getArticleMetadata, getArticleSitemapEntries, getArticlesByCategory, getAvailableArticleSlugs, markdownToHtml, sanitizeImagePath, searchArticles };
@@ -0,0 +1,127 @@
1
+ import { Metadata, MetadataRoute } from 'next';
2
+
3
+ interface Article {
4
+ slug: string;
5
+ title: string;
6
+ excerpt: string;
7
+ date?: string;
8
+ author: string;
9
+ category: string;
10
+ categories: string[];
11
+ readTime: string;
12
+ featuredImage: string;
13
+ tags?: string[];
14
+ content?: string;
15
+ htmlContent?: string;
16
+ }
17
+ interface CategoryInfo {
18
+ name: string;
19
+ slug: string;
20
+ count: number;
21
+ featuredImage: string;
22
+ }
23
+
24
+ declare function sanitizeImagePath(rawPath: string, articleSlug: string): string | null;
25
+ declare function getAvailableArticleSlugs(): string[];
26
+ declare const getArticleMetadata: (slug: string) => Promise<Article | null>;
27
+ declare const getAllArticles: () => Promise<Article[]>;
28
+ declare function getAdjacentArticles(currentSlug: string): Promise<{
29
+ previous: Article | null;
30
+ next: Article | null;
31
+ }>;
32
+ declare function searchArticles(query: string): Promise<Article[]>;
33
+ declare function categoryToSlug(category: string): string;
34
+ declare function getAllCategories(): Promise<CategoryInfo[]>;
35
+ declare function getArticlesByCategory(categorySlug: string): Promise<Article[]>;
36
+
37
+ /** Keys for each renderable section of the articles listing page. */
38
+ type ArticlesSection = 'hero' | 'search' | 'featured' | 'latest' | 'categories' | 'newsletter';
39
+ /**
40
+ * CSS custom-property overrides applied to the library's wrapper div.
41
+ * All fields are optional; omitted fields fall back to the consuming app's Tailwind theme.
42
+ */
43
+ interface ArticlesTheme {
44
+ /** Font family for the articles section. Example: `"'Inter', sans-serif"` */
45
+ fontFamily?: string;
46
+ /** Color for article card titles and section headings. Example: `'#111827'` */
47
+ headerColor?: string;
48
+ /** Color for body and excerpt text. Example: `'#6b7280'` */
49
+ textColor?: string;
50
+ /** Background color for sections and cards. Example: `'#ffffff'` */
51
+ backgroundColor?: string;
52
+ /** Color for "Read More" links and inline links. Example: `'#4f46e5'` */
53
+ linkColor?: string;
54
+ /** Hover color for links. Example: `'#4338ca'` */
55
+ linkHoverColor?: string;
56
+ /** Text decoration for links. Values: `'underline'` | `'none'` */
57
+ linkTextDecoration?: string;
58
+ /** Font weight for headings. Example: `700` */
59
+ headerFontWeight?: string | number;
60
+ /** Font size for article card titles. Example: `'1.25rem'` */
61
+ headerFontSize?: string;
62
+ /** Font size for body text. Example: `'1rem'` */
63
+ bodyFontSize?: string;
64
+ /** Line height for body text. Example: `'1.75'` */
65
+ lineHeight?: string;
66
+ /** Border radius for cards. Example: `'0.5rem'` */
67
+ borderRadius?: string;
68
+ }
69
+ /** Per-category text used in meta descriptions and on the category hero page. */
70
+ interface CategoryDescription {
71
+ /** Short description — used in meta description and category grid card. */
72
+ short: string;
73
+ /** Long description — rendered as a hero paragraph on the category page. */
74
+ long?: string;
75
+ }
76
+ /** Controls the comments feature. Attach to `ArticlesConfig.comments`. */
77
+ interface CommentsConfig {
78
+ /** Set to `true` to enable comments globally across all articles. */
79
+ enabled: boolean;
80
+ /** Maximum reply nesting depth. Default: `1` (replies to top-level only). */
81
+ maxDepth?: number;
82
+ /**
83
+ * Per-article overrides keyed by slug.
84
+ * Example: `{ 'sensitive-article': false }` disables comments on that article only.
85
+ */
86
+ perArticleOverride?: Record<string, boolean>;
87
+ }
88
+ /** Top-level configuration object. Pass one instance to every library component. */
89
+ interface ArticlesConfig {
90
+ /** Canonical base URL of the site, used in metadata and JSON-LD. Example: `'https://yoursite.com'` */
91
+ siteUrl: string;
92
+ /** Site name shown in metadata titles and JSON-LD publisher fields. Example: `'Vox Populus'` */
93
+ siteName: string;
94
+ /** Number of articles shown per page in the grid and loaded on each "Load more" click. Default: `6` */
95
+ pageSize?: number;
96
+ /** Number of category cards shown before a "Load more categories" button appears. Default: `8` */
97
+ categoriesPageSize?: number;
98
+ /**
99
+ * Ordered list of sections to render on the articles listing page.
100
+ * Omit a key to hide that section entirely. Default: `['hero','search','featured','latest','categories']`
101
+ */
102
+ layout?: ArticlesSection[];
103
+ /** CSS custom-property overrides for colors, fonts, and spacing. All fields optional. */
104
+ theme?: ArticlesTheme;
105
+ /**
106
+ * Descriptive text for each category, keyed by category slug.
107
+ * A plain string is treated as the short description only.
108
+ * Example: `{ 'campaigns': { short: 'Campaign tips', long: 'Full paragraph...' } }`
109
+ */
110
+ categoryDescriptions?: Record<string, string | CategoryDescription>;
111
+ /** Comments configuration. Omit or set `enabled: false` to hide comments entirely. */
112
+ comments?: CommentsConfig;
113
+ }
114
+
115
+ declare function generateArticleStaticParams(): {
116
+ slug: string;
117
+ }[];
118
+ declare function generateCategoryStaticParams(): Promise<{
119
+ category: string;
120
+ }[]>;
121
+ declare function generateArticleMetadata(slug: string, config: ArticlesConfig): Promise<Metadata>;
122
+ declare function generateCategoryMetadata(categorySlug: string, config: ArticlesConfig): Promise<Metadata>;
123
+ declare function getArticleSitemapEntries(baseUrl: string): Promise<MetadataRoute.Sitemap>;
124
+
125
+ declare function markdownToHtml(markdown: string, articleSlug?: string): Promise<string>;
126
+
127
+ export { type Article, type ArticlesConfig, type CategoryInfo, categoryToSlug, generateArticleMetadata, generateArticleStaticParams, generateCategoryMetadata, generateCategoryStaticParams, getAdjacentArticles, getAllArticles, getAllCategories, getArticleMetadata, getArticleSitemapEntries, getArticlesByCategory, getAvailableArticleSlugs, markdownToHtml, sanitizeImagePath, searchArticles };