@fullstackdatasolutions/articles 0.3.0 → 0.4.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +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"]}
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 generateArticlesIndexMetadata,\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 function generateArticlesIndexMetadata(config: ArticlesConfig): Metadata {\n const siteUrl = config.siteUrl.replace(/\\/$/, '')\n const indexUrl = `${siteUrl}/articles`\n const title = `Articles | ${config.siteName}`\n const description =\n config.hero?.description ?? `Expert analysis and insights from ${config.siteName}.`\n return {\n title,\n description,\n openGraph: {\n title,\n description,\n url: indexUrl,\n siteName: config.siteName,\n type: 'website',\n locale: 'en_US',\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n },\n alternates: {\n canonical: indexUrl,\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 }\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 const title = `${categoryName} Articles | ${config.siteName}`\n return {\n title,\n description,\n openGraph: {\n title: `${categoryName} Articles`,\n description,\n url: categoryUrl,\n siteName: config.siteName,\n images: [{ url: articles[0].featuredImage }],\n type: 'website',\n locale: 'en_US',\n },\n twitter: {\n card: 'summary_large_image',\n title: `${categoryName} Articles`,\n description,\n },\n alternates: {\n canonical: categoryUrl,\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 }\n}\n\nexport async function getArticleSitemapEntries(\n baseUrlOrConfig: string | ArticlesConfig\n): Promise<MetadataRoute.Sitemap> {\n const baseUrl = (\n typeof baseUrlOrConfig === 'string' ? baseUrlOrConfig : baseUrlOrConfig.siteUrl\n ).replace(/\\/$/, '')\n\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"]}
package/dist/server.d.cts CHANGED
@@ -128,9 +128,10 @@ declare function generateCategoryStaticParams(): Promise<{
128
128
  category: string;
129
129
  }[]>;
130
130
  declare function generateArticleMetadata(slug: string, config: ArticlesConfig): Promise<Metadata>;
131
+ declare function generateArticlesIndexMetadata(config: ArticlesConfig): Metadata;
131
132
  declare function generateCategoryMetadata(categorySlug: string, config: ArticlesConfig): Promise<Metadata>;
132
- declare function getArticleSitemapEntries(baseUrl: string): Promise<MetadataRoute.Sitemap>;
133
+ declare function getArticleSitemapEntries(baseUrlOrConfig: string | ArticlesConfig): Promise<MetadataRoute.Sitemap>;
133
134
 
134
135
  declare function markdownToHtml(markdown: string, articleSlug?: string): Promise<string>;
135
136
 
136
- export { type Article, type ArticlesConfig, type CategoryInfo, categoryToSlug, generateArticleMetadata, generateArticleStaticParams, generateCategoryMetadata, generateCategoryStaticParams, getAdjacentArticles, getAllArticles, getAllCategories, getArticleMetadata, getArticleSitemapEntries, getArticlesByCategory, getAvailableArticleSlugs, markdownToHtml, sanitizeImagePath, searchArticles };
137
+ export { type Article, type ArticlesConfig, type CategoryInfo, categoryToSlug, generateArticleMetadata, generateArticleStaticParams, generateArticlesIndexMetadata, generateCategoryMetadata, generateCategoryStaticParams, getAdjacentArticles, getAllArticles, getAllCategories, getArticleMetadata, getArticleSitemapEntries, getArticlesByCategory, getAvailableArticleSlugs, markdownToHtml, sanitizeImagePath, searchArticles };
package/dist/server.d.ts CHANGED
@@ -128,9 +128,10 @@ declare function generateCategoryStaticParams(): Promise<{
128
128
  category: string;
129
129
  }[]>;
130
130
  declare function generateArticleMetadata(slug: string, config: ArticlesConfig): Promise<Metadata>;
131
+ declare function generateArticlesIndexMetadata(config: ArticlesConfig): Metadata;
131
132
  declare function generateCategoryMetadata(categorySlug: string, config: ArticlesConfig): Promise<Metadata>;
132
- declare function getArticleSitemapEntries(baseUrl: string): Promise<MetadataRoute.Sitemap>;
133
+ declare function getArticleSitemapEntries(baseUrlOrConfig: string | ArticlesConfig): Promise<MetadataRoute.Sitemap>;
133
134
 
134
135
  declare function markdownToHtml(markdown: string, articleSlug?: string): Promise<string>;
135
136
 
136
- export { type Article, type ArticlesConfig, type CategoryInfo, categoryToSlug, generateArticleMetadata, generateArticleStaticParams, generateCategoryMetadata, generateCategoryStaticParams, getAdjacentArticles, getAllArticles, getAllCategories, getArticleMetadata, getArticleSitemapEntries, getArticlesByCategory, getAvailableArticleSlugs, markdownToHtml, sanitizeImagePath, searchArticles };
137
+ export { type Article, type ArticlesConfig, type CategoryInfo, categoryToSlug, generateArticleMetadata, generateArticleStaticParams, generateArticlesIndexMetadata, generateCategoryMetadata, generateCategoryStaticParams, getAdjacentArticles, getAllArticles, getAllCategories, getArticleMetadata, getArticleSitemapEntries, getArticlesByCategory, getAvailableArticleSlugs, markdownToHtml, sanitizeImagePath, searchArticles };
package/dist/server.js CHANGED
@@ -597,6 +597,44 @@ function generateArticleMetadata(slug, config) {
597
597
  };
598
598
  });
599
599
  }
600
+ function generateArticlesIndexMetadata(config) {
601
+ var _a, _b;
602
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
603
+ const indexUrl = `${siteUrl}/articles`;
604
+ const title = `Articles | ${config.siteName}`;
605
+ const description = (_b = (_a = config.hero) == null ? void 0 : _a.description) != null ? _b : `Expert analysis and insights from ${config.siteName}.`;
606
+ return {
607
+ title,
608
+ description,
609
+ openGraph: {
610
+ title,
611
+ description,
612
+ url: indexUrl,
613
+ siteName: config.siteName,
614
+ type: "website",
615
+ locale: "en_US"
616
+ },
617
+ twitter: {
618
+ card: "summary_large_image",
619
+ title,
620
+ description
621
+ },
622
+ alternates: {
623
+ canonical: indexUrl
624
+ },
625
+ robots: {
626
+ index: true,
627
+ follow: true,
628
+ googleBot: {
629
+ index: true,
630
+ follow: true,
631
+ "max-video-preview": -1,
632
+ "max-image-preview": "large",
633
+ "max-snippet": -1
634
+ }
635
+ }
636
+ };
637
+ }
600
638
  function generateCategoryMetadata(categorySlug, config) {
601
639
  return __async(this, null, function* () {
602
640
  var _a, _b;
@@ -608,24 +646,44 @@ function generateCategoryMetadata(categorySlug, config) {
608
646
  const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[categorySlug];
609
647
  const fallback = `Browse ${articles.length} article${articles.length === 1 ? "" : "s"} in the ${categoryName} category.`;
610
648
  const description = typeof raw === "string" ? raw : (_b = raw == null ? void 0 : raw.short) != null ? _b : fallback;
649
+ const title = `${categoryName} Articles | ${config.siteName}`;
611
650
  return {
612
- title: `${categoryName} Articles | ${config.siteName}`,
651
+ title,
613
652
  description,
614
653
  openGraph: {
615
654
  title: `${categoryName} Articles`,
616
655
  description,
617
656
  url: categoryUrl,
618
657
  siteName: config.siteName,
619
- images: [{ url: articles[0].featuredImage }]
658
+ images: [{ url: articles[0].featuredImage }],
659
+ type: "website",
660
+ locale: "en_US"
661
+ },
662
+ twitter: {
663
+ card: "summary_large_image",
664
+ title: `${categoryName} Articles`,
665
+ description
620
666
  },
621
667
  alternates: {
622
668
  canonical: categoryUrl
669
+ },
670
+ robots: {
671
+ index: true,
672
+ follow: true,
673
+ googleBot: {
674
+ index: true,
675
+ follow: true,
676
+ "max-video-preview": -1,
677
+ "max-image-preview": "large",
678
+ "max-snippet": -1
679
+ }
623
680
  }
624
681
  };
625
682
  });
626
683
  }
627
- function getArticleSitemapEntries(baseUrl) {
684
+ function getArticleSitemapEntries(baseUrlOrConfig) {
628
685
  return __async(this, null, function* () {
686
+ const baseUrl = (typeof baseUrlOrConfig === "string" ? baseUrlOrConfig : baseUrlOrConfig.siteUrl).replace(/\/$/, "");
629
687
  try {
630
688
  const [articles, categories] = yield Promise.all([getAllArticles(), getAllCategories()]);
631
689
  const articleEntries = articles.map((article) => ({
@@ -650,6 +708,7 @@ export {
650
708
  categoryToSlug,
651
709
  generateArticleMetadata,
652
710
  generateArticleStaticParams,
711
+ generateArticlesIndexMetadata,
653
712
  generateCategoryMetadata,
654
713
  generateCategoryStaticParams,
655
714
  getAdjacentArticles,
@@ -1 +1 @@
1
- {"version":3,"sources":["../src/server-articles.ts","../src/markdown.ts","../src/seoUtils.ts"],"sourcesContent":["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":["_a","_b","sanitizeImagePath"]}
1
+ {"version":3,"sources":["../src/server-articles.ts","../src/markdown.ts","../src/seoUtils.ts"],"sourcesContent":["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 function generateArticlesIndexMetadata(config: ArticlesConfig): Metadata {\n const siteUrl = config.siteUrl.replace(/\\/$/, '')\n const indexUrl = `${siteUrl}/articles`\n const title = `Articles | ${config.siteName}`\n const description =\n config.hero?.description ?? `Expert analysis and insights from ${config.siteName}.`\n return {\n title,\n description,\n openGraph: {\n title,\n description,\n url: indexUrl,\n siteName: config.siteName,\n type: 'website',\n locale: 'en_US',\n },\n twitter: {\n card: 'summary_large_image',\n title,\n description,\n },\n alternates: {\n canonical: indexUrl,\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 }\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 const title = `${categoryName} Articles | ${config.siteName}`\n return {\n title,\n description,\n openGraph: {\n title: `${categoryName} Articles`,\n description,\n url: categoryUrl,\n siteName: config.siteName,\n images: [{ url: articles[0].featuredImage }],\n type: 'website',\n locale: 'en_US',\n },\n twitter: {\n card: 'summary_large_image',\n title: `${categoryName} Articles`,\n description,\n },\n alternates: {\n canonical: categoryUrl,\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 }\n}\n\nexport async function getArticleSitemapEntries(\n baseUrlOrConfig: string | ArticlesConfig\n): Promise<MetadataRoute.Sitemap> {\n const baseUrl = (\n typeof baseUrlOrConfig === 'string' ? baseUrlOrConfig : baseUrlOrConfig.siteUrl\n ).replace(/\\/$/, '')\n\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":["_a","_b","sanitizeImagePath"]}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@fullstackdatasolutions/articles",
3
- "version": "0.3.0",
3
+ "version": "0.4.3",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -85,6 +85,7 @@
85
85
  "lint": "eslint src",
86
86
  "lint:fix": "eslint src --fix",
87
87
  "format": "prettier --write \"src/**/*.{ts,tsx}\"",
88
- "test": "jest"
88
+ "test": "jest",
89
+ "test:coverage": "jest --coverage"
89
90
  }
90
91
  }