@fullstackdatasolutions/articles 0.4.2 → 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.
package/README.md CHANGED
@@ -62,8 +62,11 @@ export const siteConfig: ArticlesConfig = {
62
62
 
63
63
  ```tsx
64
64
  // app/articles/page.tsx
65
+ import type { Metadata } from 'next'
65
66
  import { ArticlesPage } from '@fullstackdatasolutions/articles'
67
+ import { generateArticlesIndexMetadata } from '@fullstackdatasolutions/articles/server'
66
68
  import { siteConfig } from '@/config/articles'
69
+ export const metadata: Metadata = generateArticlesIndexMetadata(siteConfig)
67
70
  export default function Page() {
68
71
  return <ArticlesPage config={siteConfig} />
69
72
  }
@@ -173,7 +176,7 @@ hero: {
173
176
 
174
177
  ## ArticleDetailHero
175
178
 
176
- `ArticleDetailHero` renders the article's featured image full-width behind the title, category tags, and metadata (date, read time, author). Use it in your `app/articles/[slug]/page.tsx` instead of building a custom hero.
179
+ `ArticleDetailHero` renders the article's featured image full-width behind the title, category tags, and metadata (date, read time, author). When you render `ArticleDetailHero` exactly as shown below the hero image and linked categories appear automatically—no additional styling or extra `<Link>` wiring is required in the consuming app. Just pass the article payload and `categoryBasePath` (defaults to `/articles/category`) so the tags render as links.
177
180
 
178
181
  ```tsx
179
182
  import { ArticleDetailHero } from '@fullstackdatasolutions/articles'
@@ -193,6 +196,7 @@ import { ArticleDetailHero } from '@fullstackdatasolutions/articles'
193
196
  - Category tags are capped at 4. When an article has more than 4 categories, only the first 4 are shown.
194
197
  - The date field is hidden by default. Pass `showDate={true}` to display it.
195
198
  - All layout styles are inline so the component renders correctly regardless of whether `@source` is configured for the package.
199
+ - Hero image / categories behave automatically once you use this component; apps do not need to reimplement the hero section for those features.
196
200
 
197
201
  ---
198
202
 
@@ -276,6 +280,19 @@ app/api/articles/[slug]/comments/[commentId]/route.ts ← DELETE (soft)
276
280
 
277
281
  ## SEO utilities
278
282
 
283
+ ### Articles index metadata
284
+
285
+ ```ts
286
+ // app/articles/page.tsx
287
+ import type { Metadata } from 'next'
288
+ import { generateArticlesIndexMetadata } from '@fullstackdatasolutions/articles/server'
289
+ import { siteConfig } from '@/config/articles'
290
+ export const metadata: Metadata = generateArticlesIndexMetadata(siteConfig)
291
+ // Produces: title, description, full OpenGraph (type: 'website'), Twitter Card,
292
+ // canonical URL (/articles), and robots directives.
293
+ // Description falls back to config.hero?.description when set.
294
+ ```
295
+
279
296
  ### Article and category metadata
280
297
 
281
298
  ```ts
@@ -346,6 +363,7 @@ import {
346
363
  searchArticles,
347
364
  categoryToSlug,
348
365
  getArticleSitemapEntries,
366
+ generateArticlesIndexMetadata,
349
367
  generateArticleMetadata,
350
368
  generateCategoryMetadata,
351
369
  } from '@fullstackdatasolutions/articles/server'
@@ -401,7 +419,7 @@ The package ships its source TypeScript files so Tailwind can scan them.
401
419
  If this line is missing, most component classes will not render correctly. Note: `ArticleCategoryGrid` images, `CategoryArticlesPage` hero, and `ArticleDetailHero` image use inline styles for critical layout so they render correctly without this line. All other styling still requires it.
402
420
 
403
421
  ### Server utilities
404
- For sitemaps and metadata: `import { getArticleSitemapEntries, generateArticleMetadata, generateCategoryMetadata } from '@fullstackdatasolutions/articles/server'`. See README for usage examples.
422
+ For sitemaps and metadata: `import { getArticleSitemapEntries, generateArticlesIndexMetadata, generateArticleMetadata, generateCategoryMetadata } from '@fullstackdatasolutions/articles/server'`. See README for usage examples.
405
423
 
406
424
  ### Rules
407
425
  - Change `config/articles.ts` first; only edit library source if config cannot express the need.
package/dist/server.cjs CHANGED
@@ -70,6 +70,7 @@ __export(server_exports, {
70
70
  categoryToSlug: () => categoryToSlug,
71
71
  generateArticleMetadata: () => generateArticleMetadata,
72
72
  generateArticleStaticParams: () => generateArticleStaticParams,
73
+ generateArticlesIndexMetadata: () => generateArticlesIndexMetadata,
73
74
  generateCategoryMetadata: () => generateCategoryMetadata,
74
75
  generateCategoryStaticParams: () => generateCategoryStaticParams,
75
76
  getAdjacentArticles: () => getAdjacentArticles,
@@ -644,6 +645,44 @@ function generateArticleMetadata(slug, config) {
644
645
  };
645
646
  });
646
647
  }
648
+ function generateArticlesIndexMetadata(config) {
649
+ var _a, _b;
650
+ const siteUrl = config.siteUrl.replace(/\/$/, "");
651
+ const indexUrl = `${siteUrl}/articles`;
652
+ const title = `Articles | ${config.siteName}`;
653
+ const description = (_b = (_a = config.hero) == null ? void 0 : _a.description) != null ? _b : `Expert analysis and insights from ${config.siteName}.`;
654
+ return {
655
+ title,
656
+ description,
657
+ openGraph: {
658
+ title,
659
+ description,
660
+ url: indexUrl,
661
+ siteName: config.siteName,
662
+ type: "website",
663
+ locale: "en_US"
664
+ },
665
+ twitter: {
666
+ card: "summary_large_image",
667
+ title,
668
+ description
669
+ },
670
+ alternates: {
671
+ canonical: indexUrl
672
+ },
673
+ robots: {
674
+ index: true,
675
+ follow: true,
676
+ googleBot: {
677
+ index: true,
678
+ follow: true,
679
+ "max-video-preview": -1,
680
+ "max-image-preview": "large",
681
+ "max-snippet": -1
682
+ }
683
+ }
684
+ };
685
+ }
647
686
  function generateCategoryMetadata(categorySlug, config) {
648
687
  return __async(this, null, function* () {
649
688
  var _a, _b;
@@ -655,18 +694,37 @@ function generateCategoryMetadata(categorySlug, config) {
655
694
  const raw = (_a = config.categoryDescriptions) == null ? void 0 : _a[categorySlug];
656
695
  const fallback = `Browse ${articles.length} article${articles.length === 1 ? "" : "s"} in the ${categoryName} category.`;
657
696
  const description = typeof raw === "string" ? raw : (_b = raw == null ? void 0 : raw.short) != null ? _b : fallback;
697
+ const title = `${categoryName} Articles | ${config.siteName}`;
658
698
  return {
659
- title: `${categoryName} Articles | ${config.siteName}`,
699
+ title,
660
700
  description,
661
701
  openGraph: {
662
702
  title: `${categoryName} Articles`,
663
703
  description,
664
704
  url: categoryUrl,
665
705
  siteName: config.siteName,
666
- images: [{ url: articles[0].featuredImage }]
706
+ images: [{ url: articles[0].featuredImage }],
707
+ type: "website",
708
+ locale: "en_US"
709
+ },
710
+ twitter: {
711
+ card: "summary_large_image",
712
+ title: `${categoryName} Articles`,
713
+ description
667
714
  },
668
715
  alternates: {
669
716
  canonical: categoryUrl
717
+ },
718
+ robots: {
719
+ index: true,
720
+ follow: true,
721
+ googleBot: {
722
+ index: true,
723
+ follow: true,
724
+ "max-video-preview": -1,
725
+ "max-image-preview": "large",
726
+ "max-snippet": -1
727
+ }
670
728
  }
671
729
  };
672
730
  });
@@ -699,6 +757,7 @@ function getArticleSitemapEntries(baseUrlOrConfig) {
699
757
  categoryToSlug,
700
758
  generateArticleMetadata,
701
759
  generateArticleStaticParams,
760
+ generateArticlesIndexMetadata,
702
761
  generateCategoryMetadata,
703
762
  generateCategoryStaticParams,
704
763
  getAdjacentArticles,
@@ -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(\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"]}
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
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
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,18 +646,37 @@ 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
  });
@@ -651,6 +708,7 @@ export {
651
708
  categoryToSlug,
652
709
  generateArticleMetadata,
653
710
  generateArticleStaticParams,
711
+ generateArticlesIndexMetadata,
654
712
  generateCategoryMetadata,
655
713
  generateCategoryStaticParams,
656
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(\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"]}
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.4.2",
3
+ "version": "0.4.3",
4
4
  "private": false,
5
5
  "license": "MIT",
6
6
  "keywords": [
@@ -1,6 +1,7 @@
1
1
  import { ArticlesConfig } from '../articlesConfig'
2
2
  import {
3
3
  generateArticleStaticParams,
4
+ generateArticlesIndexMetadata,
4
5
  generateCategoryStaticParams,
5
6
  generateArticleMetadata,
6
7
  generateCategoryMetadata,
@@ -98,6 +99,33 @@ describe('generateArticleStaticParams', () => {
98
99
  })
99
100
  })
100
101
 
102
+ describe('generateArticlesIndexMetadata', () => {
103
+ it('returns canonical URL pointing to /articles', () => {
104
+ const meta = generateArticlesIndexMetadata(config)
105
+ expect(meta.alternates?.canonical).toBe('https://example.com/articles')
106
+ })
107
+
108
+ it('strips trailing slash from siteUrl before building canonical', () => {
109
+ const meta = generateArticlesIndexMetadata({ ...config, siteUrl: 'https://example.com/' })
110
+ expect(meta.alternates?.canonical).toBe('https://example.com/articles')
111
+ })
112
+
113
+ it('works with a subdirectory siteUrl', () => {
114
+ const meta = generateArticlesIndexMetadata({ ...config, siteUrl: 'https://example.com/app' })
115
+ expect(meta.alternates?.canonical).toBe('https://example.com/app/articles')
116
+ })
117
+
118
+ it('includes title, description, openGraph, twitter, and robots', () => {
119
+ const meta = generateArticlesIndexMetadata(config)
120
+ expect(meta.title).toBeDefined()
121
+ expect(meta.description).toBeDefined()
122
+ expect(meta.openGraph).toBeDefined()
123
+ expect((meta.openGraph as { type?: string })?.type).toBe('website')
124
+ expect(meta.twitter).toBeDefined()
125
+ expect(meta.robots).toBeDefined()
126
+ })
127
+ })
128
+
101
129
  describe('generateCategoryStaticParams', () => {
102
130
  it('returns category slug params', async () => {
103
131
  const result = await generateCategoryStaticParams()
package/src/seoUtils.ts CHANGED
@@ -99,6 +99,45 @@ export async function generateArticleMetadata(
99
99
  }
100
100
  }
101
101
 
102
+ export function generateArticlesIndexMetadata(config: ArticlesConfig): Metadata {
103
+ const siteUrl = config.siteUrl.replace(/\/$/, '')
104
+ const indexUrl = `${siteUrl}/articles`
105
+ const title = `Articles | ${config.siteName}`
106
+ const description =
107
+ config.hero?.description ?? `Expert analysis and insights from ${config.siteName}.`
108
+ return {
109
+ title,
110
+ description,
111
+ openGraph: {
112
+ title,
113
+ description,
114
+ url: indexUrl,
115
+ siteName: config.siteName,
116
+ type: 'website',
117
+ locale: 'en_US',
118
+ },
119
+ twitter: {
120
+ card: 'summary_large_image',
121
+ title,
122
+ description,
123
+ },
124
+ alternates: {
125
+ canonical: indexUrl,
126
+ },
127
+ robots: {
128
+ index: true,
129
+ follow: true,
130
+ googleBot: {
131
+ index: true,
132
+ follow: true,
133
+ 'max-video-preview': -1,
134
+ 'max-image-preview': 'large',
135
+ 'max-snippet': -1,
136
+ },
137
+ },
138
+ }
139
+ }
140
+
102
141
  export async function generateCategoryMetadata(
103
142
  categorySlug: string,
104
143
  config: ArticlesConfig
@@ -114,8 +153,9 @@ export async function generateCategoryMetadata(
114
153
  const fallback = `Browse ${articles.length} article${articles.length === 1 ? '' : 's'} in the ${categoryName} category.`
115
154
  const description = typeof raw === 'string' ? raw : (raw?.short ?? fallback)
116
155
 
156
+ const title = `${categoryName} Articles | ${config.siteName}`
117
157
  return {
118
- title: `${categoryName} Articles | ${config.siteName}`,
158
+ title,
119
159
  description,
120
160
  openGraph: {
121
161
  title: `${categoryName} Articles`,
@@ -123,10 +163,28 @@ export async function generateCategoryMetadata(
123
163
  url: categoryUrl,
124
164
  siteName: config.siteName,
125
165
  images: [{ url: articles[0].featuredImage }],
166
+ type: 'website',
167
+ locale: 'en_US',
168
+ },
169
+ twitter: {
170
+ card: 'summary_large_image',
171
+ title: `${categoryName} Articles`,
172
+ description,
126
173
  },
127
174
  alternates: {
128
175
  canonical: categoryUrl,
129
176
  },
177
+ robots: {
178
+ index: true,
179
+ follow: true,
180
+ googleBot: {
181
+ index: true,
182
+ follow: true,
183
+ 'max-video-preview': -1,
184
+ 'max-image-preview': 'large',
185
+ 'max-snippet': -1,
186
+ },
187
+ },
130
188
  }
131
189
  }
132
190
 
package/src/server.ts CHANGED
@@ -14,6 +14,7 @@ export {
14
14
  export {
15
15
  generateArticleStaticParams,
16
16
  generateCategoryStaticParams,
17
+ generateArticlesIndexMetadata,
17
18
  generateArticleMetadata,
18
19
  generateCategoryMetadata,
19
20
  getArticleSitemapEntries,