@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 +20 -2
- package/dist/server.cjs +61 -2
- package/dist/server.cjs.map +1 -1
- package/dist/server.d.cts +2 -1
- package/dist/server.d.ts +2 -1
- package/dist/server.js +60 -2
- package/dist/server.js.map +1 -1
- package/package.json +1 -1
- package/src/__tests__/seoUtils.test.ts +28 -0
- package/src/seoUtils.ts +59 -1
- package/src/server.ts +1 -0
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).
|
|
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
|
|
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,
|
package/dist/server.cjs.map
CHANGED
|
@@ -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
|
|
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,
|
package/dist/server.js.map
CHANGED
|
@@ -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":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA,SAAS,aAAa;AACtB,OAAO,YAAY;AACnB,OAAO,QAAQ;AACf,OAAO,UAAU;AACjB,OAAO,iBAAiB;;;ACHxB,OAAO,iBAAiB;AACxB,OAAO,oBAAoB;AAC3B,OAAO,qBAAqB;AAC5B,SAAS,cAAc;AACvB,OAAO,eAAe;AACtB,OAAO,iBAAiB;AACxB,OAAO,kBAAkB;AAEzB,SAAS,aAAa;AAGtB,SAAS,kBAAkB,SAAiB,aAAoC;AAC9E,MAAI,CAAC,WAAW,OAAO,YAAY,UAAU;AAC3C,WAAO;AAAA,EACT;AAGA,QAAM,YAAY,QAAQ,WAAW,yBAAyB,EAAE;AAGhE,MAAI,UAAU,WAAW,SAAS,KAAK,UAAU,WAAW,UAAU,GAAG;AAEvE,WAAO;AAAA,EACT;AAGA,MAAI,UAAU,SAAS,IAAI,KAAK,UAAU,SAAS,IAAI,KAAK,UAAU,WAAW,GAAG,GAAG;AACrF,YAAQ,KAAK,2CAA2C,SAAS,EAAE;AACnE,WAAO;AAAA,EACT;AAGA,MAAI,CAAC,qBAAqB,KAAK,SAAS,GAAG;AACzC,YAAQ,KAAK,qCAAqC,SAAS,EAAE;AAC7D,WAAO;AAAA,EACT;AAGA,MAAI,UAAU,SAAS,GAAG,GAAG;AAE3B,UAAM,iBAAiB,UAAU,WAAW,MAAM,GAAG;AACrD,QAAI,eAAe,WAAW,IAAI,KAAK,eAAe,SAAS,KAAK,GAAG;AACrE,cAAQ,KAAK,oCAAoC,SAAS,EAAE;AAC5D,aAAO;AAAA,IACT;AACA,WAAO,aAAa,WAAW,IAAI,cAAc;AAAA,EACnD,OAAO;AAEL,WAAO,aAAa,WAAW,IAAI,SAAS;AAAA,EAC9C;AACF;AAEA,IAAM,iBAAmC,MAAM;AAC7C,SAAO,CAAC,SAAe;AAErB,UAAM,MAAM,WAAW,CAAC,SAAkB;AACxC,UAAI,KAAK,SAAS;AAChB,cAAM,QAAQ,KAAK,cAAc,CAAC;AAElC,gBAAQ,KAAK,SAAS;AAAA,UACpB,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB,kBAAM,SAAS;AACf,kBAAM,MAAM;AACZ;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,aACH,MAAM,YAAY,MAAM,YAAY,MAAM,MAC3C;AACF;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF,KAAK;AACH,kBAAM,YAAY;AAClB;AAAA,UACF;AACE;AAAA,QACJ;AAEA,aAAK,aAAa;AAAA,MACpB;AAAA,IACF,CAAC;AAGD,UAAM,MAAM,WAAW,CAAC,SAAkB;AA7H9C;AA+HM,UACE,KAAK,YAAY,WACjB,gBAAK,aAAL,mBAAgB,OAAhB,mBAAoB,UAAS,aAC7B,KAAK,SAAS,CAAC,EAAE,YAAY,KAC7B;AACA,cAAM,OAAO,KAAK,SAAS,CAAC;AAC5B,cACE,UAAK,eAAL,mBAAiB,SACjB,OAAO,KAAK,WAAW,SAAS,YAChC,KAAK,WAAW,KAAK,WAAW,mBAAmB,GACnD;AAEA,cAAI,KAAK,YAAY;AACnB,mBAAO,KAAK,WAAW;AACvB,mBAAO,KAAK,WAAW;AACvB,iBAAK,WAAW,YAAY;AAAA,UAC9B;AAGA,eAAK,WAAW,OAAO,KAAK,WAAW,KAAK,WAAW,qBAAqB,YAAY;AAGxF,gBAAI,UAAK,eAAL,mBAAiB,OAAM,OAAO,KAAK,WAAW,OAAO,UAAU;AACjE,kBAAM,QAAQ,KAAK,WAAW,GAAG,WAAW,uBAAuB,MAAM;AACzE,iBAAK,WAAW,KAAK;AACrB,mBAAO,KAAK,WAAW;AAAA,UACzB;AAGA,gBAAI,gBAAK,aAAL,mBAAgB,OAAhB,mBAAoB,UAAS,QAAQ;AACvC,iBAAK,SAAS,CAAC,EAAE,QAAQ,IAAI,KAAK,SAAS,CAAC,EAAE,KAAK;AAAA,UACrD;AAAA,QACF;AAAA,MACF;AAGA,UACE,KAAK,YAAY,cAChB,MAAM,SAAQ,UAAK,eAAL,mBAAiB,SAAS,IACrC,KAAK,WAAW,UAAU,SAAS,WAAW,MAC9C,UAAK,eAAL,mBAAiB,eAAc,cACnC;AAEA,cAAM,cAAc,KAAK,SAAS;AAAA,UAChC,CAAC,UAAU,MAAM,SAAS,aAAc,MAAkB,YAAY;AAAA,QACxE;AACA,aAAI,2CAAa,UAAS,WAAW;AACnC,gBAAM,KAAK;AACX,aAAG,WAAW,YAAY;AAG1B,aAAG,SAAS,QAAQ,CAAC,OAAO;AAC1B,gBAAI,GAAG,SAAS,aAAa,GAAG,YAAY,MAAM;AAChD,oBAAM,OAAO;AACb,kBAAI,KAAK,YAAY;AACnB,qBAAK,WAAW,YAAY;AAE5B,oBAAI,OAAO,KAAK,WAAW,OAAO,UAAU;AAC1C,uBAAK,WAAW,KAAK,KAAK,WAAW,GAAG;AAAA,oBACtC;AAAA,oBACA;AAAA,kBACF;AAAA,gBACF;AAAA,cACF;AAGA,oBAAM,SAAS,KAAK,SAAS;AAAA,gBAC3B,CAAC,UAAU,MAAM,SAAS,aAAc,MAAkB,YAAY;AAAA,cACxE;AAEA,kBAAI,WAAW,IAAI;AACjB,sBAAM,IAAI,KAAK,SAAS,MAAM;AAC9B,qBAAK,SAAS,OAAO,QAAQ,GAAG,GAAG,EAAE,QAAQ;AAAA,cAC/C;AAGA,oBAAM,aAAa,CAAC,UAAkC;AACpD,sBAAM,QAAQ,CAAC,MAAM;AA5MrC,sBAAAA,KAAAC;AA6MkB,sBAAI,EAAE,SAAS,UAAW;AAC1B,wBAAM,KAAK;AACX,sBAAI,GAAG,YAAY,KAAK;AACtB,0BAAM,cACJD,MAAA,GAAG,eAAH,gBAAAA,IAAgB,4BAA2B,YAC1CC,MAAA,GAAG,SAAS,CAAC,MAAb,gBAAAA,IAAgB,UAAS,UAAU,GAAG,SAAS,CAAC,EAAE,UAAU;AAE/D,wBAAI,WAAW;AACb,yBAAG,WAAW,YAAY;AAE1B,0BAAI,OAAO,GAAG,WAAW,SAAS,UAAU;AAC1C,2BAAG,WAAW,OAAO,GAAG,WAAW,KAAK;AAAA,0BACtC;AAAA,0BACA;AAAA,wBACF;AAAA,sBACF;AAAA,oBACF,OAAO;AACL,yBAAG,WAAW,YAAY;AAAA,oBAC5B;AAAA,kBACF;AACA,sBAAI,GAAG,SAAU,YAAW,GAAG,QAAQ;AAAA,gBACzC,CAAC;AAAA,cACH;AAEA,yBAAW,KAAK,QAAQ;AAAA,YAC1B;AAAA,UACF,CAAC;AAGD,gBAAM,KAAc;AAAA,YAClB,MAAM;AAAA,YACN,SAAS;AAAA,YACT,YAAY,EAAE,WAAW,qBAAqB;AAAA,YAC9C,UAAU,CAAC;AAAA,UACb;AAEA,gBAAM,KAAc;AAAA,YAClB,MAAM;AAAA,YACN,SAAS;AAAA,YACT,YAAY,EAAE,WAAW,6BAA6B;AAAA,YACtD,UAAU,CAAC,EAAE,MAAM,QAAQ,OAAO,aAAa,CAAC;AAAA,UAClD;AAEA,eAAK,WAAW,CAAC,IAAI,IAAI,EAAE;AAG3B,cAAI,KAAK,YAAY;AACnB,iBAAK,WAAW,YAAY;AAAA,UAC9B;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAGA,IAAM,sBAAgE,CAAC,UAAU,CAAC,MAAM;AACtF,SAAO,CAAC,SAAe;AACrB,UAAM,MAAM,WAAW,CAAC,SAAkB;AACxC,UAAI,KAAK,YAAY,SAAS,KAAK,YAAY;AAC7C,cAAM,MAAM,KAAK,WAAW;AAC5B,YAAI,OAAO,OAAO,QAAQ,YAAY,QAAQ,aAAa;AAEzD,gBAAM,eAAe,kBAAkB,KAAK,QAAQ,WAAW;AAC/D,cAAI,cAAc;AAChB,iBAAK,WAAW,MAAM;AAAA,UACxB,OAAO;AAEL,oBAAQ,KAAK,4CAA4C,GAAG,EAAE;AAC9D,iBAAK,WAAW,MAAM;AAAA,UACxB;AAAA,QACF;AAAA,MACF;AAAA,IACF,CAAC;AAAA,EACH;AACF;AAEA,SAAsB,eAAe,UAAkB,aAAsB;AAAA;AAC3E,QAAI;AAEF,UAAI,YAAY,OAAO,EACpB,IAAI,WAAW,EACf,IAAI,SAAS,EACb,IAAI,YAAY,EAChB,IAAI,cAAc,EAElB,IAAI,WAAW,EACf,IAAI,gBAAgB;AAAA,QACnB,YAAY;AAAA,UACV,KAAK,CAAC,aAAa,SAAS,IAAI;AAAA,UAChC,GAAG,CAAC,QAAQ,UAAU,OAAO,IAAI;AAAA,UACjC,KAAK,CAAC,OAAO,KAAK;AAAA,QACpB;AAAA,MACF,CAAC;AAGH,UAAI,aAAa;AACf,oBAAY,UAAU,IAAI,qBAAqB,EAAE,YAAY,CAAC;AAAA,MAChE;AAEA,YAAM,SAAS,MAAM,UAAU,IAAI,eAAe,EAAE,QAAQ,QAAQ;AAEpE,aAAO,OAAO,SAAS;AAAA,IACzB,SAAS,OAAO;AACd,cAAQ,MAAM,8BAA8B,KAAK;AAEjD,aAAO;AAAA,IACT;AAAA,EACF;AAAA;;;ADjTA,IAAM,oBAAoB,KAAK,KAAK,QAAQ,IAAI,GAAG,iBAAiB;AAEpE,SAAS,eAAe,SAAyB;AAC/C,SAAO,YAAY,OAAO,EAAE;AAC9B;AAEA,SAAS,iBAAiB,MAA6B;AACrD,MAAI;AACF,UAAM,aAAa,KAAK,KAAK,mBAAmB,IAAI;AACpD,UAAM,UAAkC,GAAG,YAAY,YAAY,EAAE,eAAe,KAAK,CAAC;AAC1F,UAAM,QAAQ,QACX,IAAI,CAAC,UAAU;AACd,UAAI,OAAO,UAAU,SAAU,QAAO;AACtC,UAAI,SAAS,OAAO,MAAM,SAAS,SAAU,QAAO,MAAM;AAC1D,aAAO;AAAA,IACT,CAAC,EACA,OAAO,CAAC,SAAyB,QAAQ,IAAI,CAAC;AACjD,UAAM,kBAAkB,CAAC,QAAQ,QAAQ,SAAS,QAAQ,OAAO;AACjE,UAAM,gBAAgB,MAAM;AAAA,MAAK,CAAC,SAChC,gBAAgB,KAAK,CAAC,QAAQ,KAAK,YAAY,EAAE,SAAS,GAAG,CAAC;AAAA,IAChE;AACA,WAAO,gBAAgBC,mBAAkB,eAAe,IAAI,IAAI;AAAA,EAClE,SAAQ;AACN,WAAO;AAAA,EACT;AACF;AAEA,SAASA,mBAAkB,SAAiB,aAAoC;AAC9E,MAAI,CAAC,WAAW,OAAO,YAAY,SAAU,QAAO;AACpD,QAAM,YAAY,QAAQ,WAAW,yBAAyB,EAAE;AAChE,MAAI,UAAU,WAAW,SAAS,KAAK,UAAU,WAAW,UAAU,EAAG,QAAO;AAChF,MAAI,UAAU,SAAS,IAAI,KAAK,UAAU,SAAS,IAAI,KAAK,UAAU,WAAW,GAAG,GAAG;AACrF,YAAQ,KAAK,2CAA2C,SAAS,EAAE;AACnE,WAAO;AAAA,EACT;AACA,MAAI,CAAC,qBAAqB,KAAK,SAAS,GAAG;AACzC,YAAQ,KAAK,qCAAqC,SAAS,EAAE;AAC7D,WAAO;AAAA,EACT;AACA,MAAI,UAAU,SAAS,GAAG,GAAG;AAC3B,UAAM,iBAAiB,KAAK,UAAU,SAAS;AAC/C,QAAI,eAAe,WAAW,IAAI,KAAK,eAAe,SAAS,KAAK,GAAG;AACrE,cAAQ,KAAK,oCAAoC,SAAS,EAAE;AAC5D,aAAO;AAAA,IACT;AACA,WAAO,aAAa,WAAW,IAAI,SAAS;AAAA,EAC9C;AACA,SAAO,aAAa,WAAW,IAAI,SAAS;AAC9C;AAEO,SAAS,2BAAqC;AACnD,MAAI;AACF,QAAI,CAAC,GAAG,WAAW,iBAAiB,EAAG,QAAO,CAAC;AAC/C,UAAM,QAAQ,GAAG,YAAY,mBAAmB,EAAE,eAAe,KAAK,CAAC;AACvE,WAAO,MACJ,OAAO,CAAC,SAAS,KAAK,YAAY,CAAC,EACnC,OAAO,CAAC,QAAQ;AACf,YAAM,aAAa,KAAK,KAAK,mBAAmB,IAAI,IAAI;AACxD,UAAI;AACF,cAAM,UAAkC,GAAG,YAAY,YAAY;AAAA,UACjE,eAAe;AAAA,QACjB,CAAC;AACD,eAAO,QAAQ,KAAK,CAAC,UAAU;AAC7B,gBAAM,OAAO,OAAO,UAAU,WAAW,QAAQ,+BAAO;AACxD,cAAI,SAAS,aAAc,QAAO;AAClC,cAAI,OAAO,UAAU,SAAU,QAAO;AACtC,cAAI,QAAO,+BAAO,YAAW,WAAY,QAAO,MAAM,OAAO;AAC7D,iBAAO;AAAA,QACT,CAAC;AAAA,MACH,SAAQ;AACN,eAAO;AAAA,MACT;AAAA,IACF,CAAC,EACA,IAAI,CAAC,QAAQ,IAAI,IAAI;AAAA,EAC1B,SAAS,OAAO;AACd,YAAQ,MAAM,qCAAqC,KAAK;AACxD,WAAO,CAAC;AAAA,EACV;AACF;AAEA,SAAe,kBAAkB,MAAuC;AAAA;AACtE,QAAI;AACF,YAAM,cAAc,KAAK,KAAK,mBAAmB,MAAM,YAAY;AACnE,UAAI,CAAC,GAAG,WAAW,WAAW,EAAG,QAAO;AACxC,YAAM,cAAc,GAAG,aAAa,aAAa,MAAM;AACvD,YAAM,EAAE,MAAM,SAAS,gBAAgB,IAAI,OAAO,WAAW;AAC7D,YAAM,WAAW,eAAe,eAAe;AAC/C,UAAI,gBAAgB,KAAK,iBAAiB;AAC1C,UAAI,iBAAiB,CAAC,cAAc,WAAW,MAAM,GAAG;AACtD,cAAM,gBAAgBA,mBAAkB,eAAe,IAAI;AAC3D,wBAAgB,iBAAiB;AAAA,MACnC,WAAW,CAAC,eAAe;AACzB,wBAAgB,iBAAiB,IAAI,KAAK;AAAA,MAC5C;AACA,UAAI;AACJ,UAAI,KAAK,MAAM;AACb,YAAI;AACF,gBAAM,aAAa,IAAI,KAAK,KAAK,IAAI;AACrC,cAAI,CAAC,OAAO,MAAM,WAAW,QAAQ,CAAC,GAAG;AACvC,0BAAc,WAAW,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,UACrD;AAAA,QACF,SAAQ;AACN,kBAAQ,KAAK,4BAA4B,IAAI,KAAK,KAAK,IAAI,EAAE;AAAA,QAC/D;AAAA,MACF;AACA,YAAM,UAAoB,MAAM,QAAQ,KAAK,IAAI,IAC7C,KAAK,KAAK,OAAO,CAAC,MAAe,OAAO,MAAM,YAAY,OAAO,CAAC,EAAE,KAAK,CAAC,IAC1E,CAAC;AACL,YAAM,aACJ,QAAQ,SAAS,IAAI,QAAQ,IAAI,CAAC,MAAc,EAAE,WAAW,KAAK,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW;AAC/F,aAAO;AAAA,QACL;AAAA,QACA,OAAO,KAAK,SAAS,KAAK,WAAW,KAAK,GAAG;AAAA,QAC7C,SAAS,KAAK,WAAW;AAAA,QACzB,MAAM;AAAA,QACN,QAAQ,KAAK,UAAU;AAAA,QACvB,UAAU,WAAW,CAAC;AAAA,QACtB;AAAA,QACA;AAAA,QACA;AAAA,QACA,MAAM,KAAK,QAAQ,CAAC;AAAA,MACtB;AAAA,IACF,SAAS,OAAO;AACd,cAAQ,MAAM,yBAAyB,IAAI,KAAK,KAAK;AACrD,aAAO;AAAA,IACT;AAAA,EACF;AAAA;AAEO,IAAM,qBAAqB,MAAM,CAAO,SAA0C;AACvF,MAAI;AACF,UAAM,cAAc,KAAK,KAAK,mBAAmB,MAAM,YAAY;AACnE,QAAI,CAAC,GAAG,WAAW,WAAW,EAAG,QAAO;AACxC,UAAM,cAAc,GAAG,aAAa,aAAa,MAAM;AACvD,UAAM,EAAE,MAAM,SAAS,gBAAgB,IAAI,OAAO,WAAW;AAC7D,UAAM,cAAc,MAAM,eAAe,iBAAiB,IAAI;AAC9D,UAAM,WAAW,eAAe,eAAe;AAC/C,QAAI,gBAAgB,KAAK,iBAAiB;AAC1C,QAAI,iBAAiB,CAAC,cAAc,WAAW,MAAM,GAAG;AACtD,YAAM,gBAAgBA,mBAAkB,eAAe,IAAI;AAC3D,sBAAgB,iBAAiB;AAAA,IACnC,WAAW,CAAC,eAAe;AACzB,sBAAgB,iBAAiB,IAAI,KAAK;AAAA,IAC5C;AACA,QAAI;AACJ,QAAI,KAAK,MAAM;AACb,UAAI;AACF,cAAM,aAAa,IAAI,KAAK,KAAK,IAAI;AACrC,YAAI,CAAC,OAAO,MAAM,WAAW,QAAQ,CAAC,GAAG;AACvC,wBAAc,WAAW,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AAAA,QACrD;AAAA,MACF,SAAQ;AACN,gBAAQ,KAAK,4BAA4B,IAAI,KAAK,KAAK,IAAI,EAAE;AAAA,MAC/D;AAAA,IACF;AACA,UAAM,UAAoB,MAAM,QAAQ,KAAK,IAAI,IAC7C,KAAK,KAAK,OAAO,CAAC,MAAe,OAAO,MAAM,YAAY,OAAO,CAAC,EAAE,KAAK,CAAC,IAC1E,CAAC;AACL,UAAM,aACJ,QAAQ,SAAS,IAAI,QAAQ,IAAI,CAAC,MAAc,EAAE,WAAW,KAAK,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC,WAAW;AAC/F,WAAO;AAAA,MACL;AAAA,MACA,OAAO,KAAK,SAAS,KAAK,WAAW,KAAK,GAAG;AAAA,MAC7C,SAAS,KAAK,WAAW;AAAA,MACzB,MAAM;AAAA,MACN,QAAQ,KAAK,UAAU;AAAA,MACvB,UAAU,WAAW,CAAC;AAAA,MACtB;AAAA,MACA;AAAA,MACA;AAAA,MACA,MAAM,KAAK,QAAQ,CAAC;AAAA,MACpB,SAAS;AAAA,MACT;AAAA,IACF;AAAA,EACF,SAAS,OAAO;AACd,YAAQ,MAAM,yBAAyB,IAAI,KAAK,KAAK;AACrD,WAAO;AAAA,EACT;AACF,EAAC;AAEM,IAAM,iBAAiB,MAAM,MAAgC;AAClE,QAAM,QAAQ,yBAAyB;AACvC,QAAM,WAAW,MAAM,QAAQ,IAAI,MAAM,IAAI,CAAC,SAAS,kBAAkB,IAAI,CAAC,CAAC;AAC/E,QAAM,eAAc,oBAAI,KAAK,GAAE,YAAY,EAAE,MAAM,GAAG,EAAE,CAAC;AACzD,SAAO,SACJ,OAAO,CAAC,YAAgC,YAAY,IAAI,EACxD,OAAO,CAAC,YAAY,CAAC,QAAQ,QAAQ,QAAQ,QAAQ,WAAW,EAChE,KAAK,CAAC,GAAG,MAAM;AACd,QAAI,CAAC,EAAE,QAAQ,CAAC,EAAE,KAAM,QAAO;AAC/B,QAAI,CAAC,EAAE,KAAM,QAAO;AACpB,QAAI,CAAC,EAAE,KAAM,QAAO;AACpB,WAAO,IAAI,KAAK,EAAE,IAAI,EAAE,QAAQ,IAAI,IAAI,KAAK,EAAE,IAAI,EAAE,QAAQ;AAAA,EAC/D,CAAC;AACL,EAAC;AAED,SAAsB,oBACpB,aAC6D;AAAA;AAC7D,UAAM,cAAc,MAAM,eAAe;AACzC,UAAM,eAAe,YAAY,UAAU,CAAC,YAAY,QAAQ,SAAS,WAAW;AACpF,QAAI,iBAAiB,GAAI,QAAO,EAAE,UAAU,MAAM,MAAM,KAAK;AAC7D,UAAM,WAAW,eAAe,YAAY,SAAS,IAAI,YAAY,eAAe,CAAC,IAAI;AACzF,UAAM,OAAO,eAAe,IAAI,YAAY,eAAe,CAAC,IAAI;AAChE,WAAO,EAAE,UAAU,KAAK;AAAA,EAC1B;AAAA;AAEA,SAAsB,eAAe,OAAmC;AAAA;AACtE,QAAI,EAAC,+BAAO,QAAQ,QAAO,eAAe;AAC1C,UAAM,WAAW,MAAM,eAAe;AACtC,UAAM,aAAa,MAAM,YAAY,EAAE,KAAK;AAC5C,WAAO,SAAS,OAAO,CAAC,YAAY;AAzNtC;AA0NI,YAAM,eAAe,QAAQ,MAAM,YAAY,EAAE,SAAS,UAAU;AACpE,YAAM,iBAAiB,QAAQ,QAAQ,YAAY,EAAE,SAAS,UAAU;AACxE,YAAM,gBAAgB,QAAQ,OAAO,YAAY,EAAE,SAAS,UAAU;AACtE,YAAM,kBAAkB,QAAQ,WAAW,KAAK,CAAC,QAAQ,IAAI,YAAY,EAAE,SAAS,UAAU,CAAC;AAC/F,YAAM,eAAc,aAAQ,SAAR,mBAAc,KAAK,CAAC,QAAQ,IAAI,YAAY,EAAE,SAAS,UAAU;AACrF,aACE,gBAAgB,kBAAkB,iBAAiB,mBAAmB,QAAQ,WAAW;AAAA,IAE7F,CAAC;AAAA,EACH;AAAA;AAEO,SAAS,eAAe,UAA0B;AACvD,SAAO,SACJ,YAAY,EACZ,WAAW,QAAQ,GAAG,EACtB,WAAW,eAAe,EAAE;AACjC;AAEA,SAAsB,mBAA4C;AAAA;AAChE,UAAM,WAAW,MAAM,eAAe;AACtC,UAAM,cAAc,oBAAI,IAAsD;AAC9E,eAAW,WAAW,UAAU;AAC9B,iBAAW,OAAO,QAAQ,YAAY;AACpC,YAAI,CAAC,YAAY,IAAI,GAAG,GAAG;AACzB,sBAAY,IAAI,KAAK,EAAE,OAAO,GAAG,eAAe,QAAQ,cAAc,CAAC;AAAA,QACzE;AACA,oBAAY,IAAI,GAAG,EAAG;AAAA,MACxB;AAAA,IACF;AACA,WAAO,MAAM,KAAK,YAAY,QAAQ,CAAC,EACpC,IAAI,CAAC,CAAC,MAAM,EAAE,OAAO,cAAc,CAAC,OAAO;AAAA,MAC1C;AAAA,MACA,MAAM,eAAe,IAAI;AAAA,MACzB;AAAA,MACA;AAAA,IACF,EAAE,EACD,KAAK,CAAC,GAAG,MAAM,EAAE,QAAQ,EAAE,KAAK;AAAA,EACrC;AAAA;AAEA,SAAsB,sBAAsB,cAA0C;AAAA;AACpF,UAAM,WAAW,MAAM,eAAe;AACtC,WAAO,SAAS;AAAA,MAAO,CAAC,YACtB,QAAQ,WAAW,KAAK,CAAC,QAAQ,eAAe,GAAG,MAAM,YAAY;AAAA,IACvE;AAAA,EACF;AAAA;;;AE5PO,SAAS,8BAAkD;AAChE,SAAO,yBAAyB,EAAE,IAAI,CAAC,UAAU,EAAE,KAAK,EAAE;AAC5D;AAEA,SAAsB,+BAAgE;AAAA;AACpF,UAAM,aAAa,MAAM,iBAAiB;AAC1C,WAAO,WAAW,IAAI,CAAC,SAAS,EAAE,UAAU,IAAI,KAAK,EAAE;AAAA,EACzD;AAAA;AAEA,SAAS,gBAAgB,eAAuB,SAAyB;AACvE,QAAM,OAAO,QAAQ,QAAQ,OAAO,EAAE;AACtC,MAAI,cAAc,WAAW,SAAS,KAAK,cAAc,WAAW,UAAU,GAAG;AAC/E,WAAO;AAAA,EACT;AACA,SAAO,GAAG,IAAI,IAAI,cAAc,QAAQ,QAAQ,EAAE,CAAC;AACrD;AAEA,SAAsB,wBACpB,MACA,QACmB;AAAA;AA9BrB;AA+BE,UAAM,UAAU,MAAM,mBAAmB,IAAI;AAE7C,QAAI,CAAC,SAAS;AACZ,aAAO;AAAA,QACL,OAAO;AAAA,QACP,aAAa;AAAA,MACf;AAAA,IACF;AAEA,UAAM,UAAU,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAChD,UAAM,aAAa,GAAG,OAAO,aAAa,IAAI;AAC9C,UAAM,WAAW,QAAQ,gBACrB,gBAAgB,QAAQ,eAAe,OAAO,IAC9C,GAAG,OAAO;AACd,UAAM,eAAc,aAAQ,YAAR,YAAmB,QAAQ,QAAQ,KAAK,OAAO,OAAO,QAAQ;AAElF,WAAO;AAAA,MACL,OAAO,GAAG,QAAQ,KAAK,MAAM,OAAO,QAAQ;AAAA,MAC5C;AAAA,MACA,UAAU;AAAA,QACR;AAAA,QACA;AAAA,QACA;AAAA,QACA;AAAA,QACA,KAAI,aAAQ,SAAR,YAAgB,CAAC,GAAG,IAAI,CAAC,QAAQ,IAAI,YAAY,CAAC;AAAA,MACxD,EAAE,KAAK,IAAI;AAAA,MACX,WAAW;AAAA,QACT,OAAO,QAAQ;AAAA,QACf;AAAA,QACA,KAAK;AAAA,QACL,UAAU,OAAO;AAAA,QACjB,QAAQ,CAAC,EAAE,KAAK,UAAU,OAAO,MAAM,QAAQ,KAAK,KAAK,QAAQ,MAAM,CAAC;AAAA,QACxE,QAAQ;AAAA,QACR,MAAM;AAAA,SACF,QAAQ,QAAQ,EAAE,eAAe,QAAQ,KAAK,IARzC;AAAA,QAST,SAAS,CAAC,QAAQ,MAAM;AAAA,QACxB,OAAM,aAAQ,SAAR,YAAgB,CAAC;AAAA,MACzB;AAAA,MACA,SAAS;AAAA,QACP,MAAM;AAAA,QACN,OAAO,QAAQ;AAAA,QACf;AAAA,QACA,QAAQ,CAAC,QAAQ;AAAA,MACnB;AAAA,MACA,YAAY;AAAA,QACV,WAAW;AAAA,MACb;AAAA,MACA,QAAQ;AAAA,QACN,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,WAAW;AAAA,UACT,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,qBAAqB;AAAA,UACrB,qBAAqB;AAAA,UACrB,eAAe;AAAA,QACjB;AAAA,MACF;AAAA,MACA,OAAO;AAAA,QACL,kBAAkB,QAAQ;AAAA,SACtB,QAAQ,QAAQ;AAAA,QAClB,0BAA0B,IAAI,KAAK,QAAQ,IAAI,EAAE,YAAY;AAAA,MAC/D,IAJK;AAAA,QAKL,mBAAmB,QAAQ;AAAA,QAC3B,gBAAe,mBAAQ,SAAR,mBAAc,KAAK,SAAnB,YAA2B;AAAA,QAC1C,mBAAkB,aAAQ,IAAI,oCAAZ,YAA+C;AAAA,MACnE;AAAA,IACF;AAAA,EACF;AAAA;AAEO,SAAS,8BAA8B,QAAkC;AArGhF;AAsGE,QAAM,UAAU,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAChD,QAAM,WAAW,GAAG,OAAO;AAC3B,QAAM,QAAQ,cAAc,OAAO,QAAQ;AAC3C,QAAM,eACJ,kBAAO,SAAP,mBAAa,gBAAb,YAA4B,qCAAqC,OAAO,QAAQ;AAClF,SAAO;AAAA,IACL;AAAA,IACA;AAAA,IACA,WAAW;AAAA,MACT;AAAA,MACA;AAAA,MACA,KAAK;AAAA,MACL,UAAU,OAAO;AAAA,MACjB,MAAM;AAAA,MACN,QAAQ;AAAA,IACV;AAAA,IACA,SAAS;AAAA,MACP,MAAM;AAAA,MACN;AAAA,MACA;AAAA,IACF;AAAA,IACA,YAAY;AAAA,MACV,WAAW;AAAA,IACb;AAAA,IACA,QAAQ;AAAA,MACN,OAAO;AAAA,MACP,QAAQ;AAAA,MACR,WAAW;AAAA,QACT,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,qBAAqB;AAAA,QACrB,qBAAqB;AAAA,QACrB,eAAe;AAAA,MACjB;AAAA,IACF;AAAA,EACF;AACF;AAEA,SAAsB,yBACpB,cACA,QACmB;AAAA;AA/IrB;AAgJE,UAAM,WAAW,MAAM,sBAAsB,YAAY;AAEzD,QAAI,SAAS,WAAW,EAAG,QAAO,EAAE,OAAO,qBAAqB;AAEhE,UAAM,eAAe,SAAS,CAAC,EAAE;AACjC,UAAM,UAAU,OAAO,QAAQ,QAAQ,OAAO,EAAE;AAChD,UAAM,cAAc,GAAG,OAAO,sBAAsB,YAAY;AAChE,UAAM,OAAM,YAAO,yBAAP,mBAA8B;AAC1C,UAAM,WAAW,UAAU,SAAS,MAAM,WAAW,SAAS,WAAW,IAAI,KAAK,GAAG,WAAW,YAAY;AAC5G,UAAM,cAAc,OAAO,QAAQ,WAAW,OAAO,gCAAK,UAAL,YAAc;AAEnE,UAAM,QAAQ,GAAG,YAAY,eAAe,OAAO,QAAQ;AAC3D,WAAO;AAAA,MACL;AAAA,MACA;AAAA,MACA,WAAW;AAAA,QACT,OAAO,GAAG,YAAY;AAAA,QACtB;AAAA,QACA,KAAK;AAAA,QACL,UAAU,OAAO;AAAA,QACjB,QAAQ,CAAC,EAAE,KAAK,SAAS,CAAC,EAAE,cAAc,CAAC;AAAA,QAC3C,MAAM;AAAA,QACN,QAAQ;AAAA,MACV;AAAA,MACA,SAAS;AAAA,QACP,MAAM;AAAA,QACN,OAAO,GAAG,YAAY;AAAA,QACtB;AAAA,MACF;AAAA,MACA,YAAY;AAAA,QACV,WAAW;AAAA,MACb;AAAA,MACA,QAAQ;AAAA,QACN,OAAO;AAAA,QACP,QAAQ;AAAA,QACR,WAAW;AAAA,UACT,OAAO;AAAA,UACP,QAAQ;AAAA,UACR,qBAAqB;AAAA,UACrB,qBAAqB;AAAA,UACrB,eAAe;AAAA,QACjB;AAAA,MACF;AAAA,IACF;AAAA,EACF;AAAA;AAEA,SAAsB,yBACpB,iBACgC;AAAA;AAChC,UAAM,WACJ,OAAO,oBAAoB,WAAW,kBAAkB,gBAAgB,SACxE,QAAQ,OAAO,EAAE;AAEnB,QAAI;AACF,YAAM,CAAC,UAAU,UAAU,IAAI,MAAM,QAAQ,IAAI,CAAC,eAAe,GAAG,iBAAiB,CAAC,CAAC;AAEvF,YAAM,iBAAwC,SAAS,IAAI,CAAC,aAAa;AAAA,QACvE,KAAK,GAAG,OAAO,aAAa,QAAQ,IAAI;AAAA,QACxC,cAAc,QAAQ,OAAO,IAAI,KAAK,QAAQ,IAAI,IAAI;AAAA,QACtD,iBAAiB;AAAA,QACjB,UAAU;AAAA,MACZ,EAAE;AAEF,YAAM,kBAAyC,WAAW,IAAI,CAAC,SAAS;AAAA,QACtE,KAAK,GAAG,OAAO,sBAAsB,IAAI,IAAI;AAAA,QAC7C,cAAc,oBAAI,KAAK;AAAA,QACvB,iBAAiB;AAAA,QACjB,UAAU;AAAA,MACZ,EAAE;AAEF,aAAO,CAAC,GAAG,gBAAgB,GAAG,eAAe;AAAA,IAC/C,SAAQ;AACN,aAAO,CAAC;AAAA,IACV;AAAA,EACF;AAAA;","names":["_a","_b","sanitizeImagePath"]}
|
package/package.json
CHANGED
|
@@ -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
|
|
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
|
|