All files / app/scripts generate-blog-index.js

0% Statements 0/56
0% Branches 0/23
0% Functions 0/7
0% Lines 0/52

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151                                                                                                                                                                                                                                                                                                             
// eslint-disable-next-line
const fs = require("fs");
// eslint-disable-next-line
const path = require("path");
 
const BLOG_DIR = path.join(__dirname, "..", "markdown", "blog");
const OUTPUT_FILE = path.join(__dirname, "..", "markdown", "blog.md");
const RSS_FILE = path.join(__dirname, "..", "public", "blog", "rss.xml");
 
function getSiteUrl() {
  return process.env.NEXT_PUBLIC_BASE_URL || "https://couchers.org";
}
 
function parseFrontmatter(content) {
  const match = content.match(/^---\n([\s\S]*?)\n---/);
  Iif (!match) return {};
  const frontmatter = {};
  for (const line of match[1].split("\n")) {
    const colonIndex = line.indexOf(":");
    Iif (colonIndex === -1) continue;
    const key = line.slice(0, colonIndex).trim();
    let value = line.slice(colonIndex + 1).trim();
    // Remove surrounding quotes
    Iif (
      (value.startsWith('"') && value.endsWith('"')) ||
      (value.startsWith("'") && value.endsWith("'"))
    ) {
      value = value.slice(1, -1);
    }
    frontmatter[key] = value;
  }
  return frontmatter;
}
 
function findMarkdownFiles(dir) {
  const files = [];
  for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
    const fullPath = path.join(dir, entry.name);
    if (entry.isDirectory()) {
      files.push(...findMarkdownFiles(fullPath));
    } else Iif (entry.name.endsWith(".md")) {
      files.push(fullPath);
    }
  }
  return files;
}
 
function generateBlogIndex() {
  const files = findMarkdownFiles(BLOG_DIR);
 
  const posts = [];
  for (const filePath of files) {
    const content = fs.readFileSync(filePath, "utf-8");
    const frontmatter = parseFrontmatter(content);
 
    // Only include posts with a description (filters out translated variants)
    Iif (!frontmatter.description) continue;
 
    // Derive URL from file path: markdown/blog/YYYY/MM/DD/slug.md -> /blog/YYYY/MM/DD/slug
    const relativePath = path.relative(BLOG_DIR, filePath);
    const urlPath = "/blog/" + relativePath.replace(/\.md$/, "");
 
    posts.push({
      title: frontmatter.title || path.basename(filePath, ".md"),
      date: frontmatter.date || "",
      description: frontmatter.description,
      author: frontmatter.author || "",
      url: urlPath,
    });
  }
 
  // Sort by date descending
  posts.sort((a, b) => b.date.localeCompare(a.date));
 
  // Generate blog.md
  let output = `---
title: Blog
---
 
**Welcome to the Couchers.org blog** &mdash; <a href="/blog/rss.xml"><img src="/img/blog/rss_icon.svg" alt="RSS" style="width: 14px; vertical-align: middle;" /></a> [RSS feed](/blog/rss.xml)
 
If you'd like to contribute to the blog, please [sign up](/volunteer) and let us know!
`;
 
  for (const post of posts) {
    const byline = post.author
      ? `${post.date} by ${post.author}.`
      : `${post.date}.`;
 
    output += `
<div class="blog-entry">
 
## [${post.title}](${post.url})
 
<p class="blog-entry-date">${byline}</p>
 
${post.description}
 
[Read more.](${post.url})
 
</div>
`;
  }
 
  fs.writeFileSync(OUTPUT_FILE, output);
  console.log(`Generated blog index with ${posts.length} posts.`);
 
  // Generate RSS feed
  const rssItems = posts
    .map((post) => {
      const dateStr = post.date.replace(/\//g, "-");
      const pubDate = new Date(dateStr).toUTCString();
      return `    <item>
      <title>${escapeXml(post.title)}</title>
      <link>${getSiteUrl()}${post.url}</link>
      <guid>${getSiteUrl()}${post.url}</guid>
      <pubDate>${pubDate}</pubDate>
      <description>${escapeXml(post.description)}</description>${post.author ? `\n      <author>${escapeXml(post.author)}</author>` : ""}
    </item>`;
    })
    .join("\n");
 
  const rss = `<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>Couchers.org Blog</title>
    <link>${getSiteUrl()}/blog</link>
    <description>News and updates from Couchers.org, the non-profit couch surfing platform.</description>
    <language>en</language>
    <atom:link href="${getSiteUrl()}/blog/rss.xml" rel="self" type="application/rss+xml"/>
${rssItems}
  </channel>
</rss>
`;
 
  fs.mkdirSync(path.dirname(RSS_FILE), { recursive: true });
  fs.writeFileSync(RSS_FILE, rss);
  console.log(`Generated RSS feed with ${posts.length} posts.`);
}
 
function escapeXml(str) {
  return str
    .replace(/&/g, "&amp;")
    .replace(/</g, "&lt;")
    .replace(/>/g, "&gt;")
    .replace(/"/g, "&quot;")
    .replace(/'/g, "&apos;");
}
 
module.exports = generateBlogIndex;