This commit is contained in:
parent
02e47cf5d0
commit
9074e1884b
4 changed files with 481 additions and 42 deletions
31
README.md
31
README.md
|
|
@ -2,10 +2,35 @@
|
||||||
|
|
||||||
Minimal static homepage for `neeldhara.blog`.
|
Minimal static homepage for `neeldhara.blog`.
|
||||||
|
|
||||||
The build script fetches the RSS feed from each blog and writes `dist/index.html`
|
The build script reads local Markdown content from `../allblogs/sites/*` when
|
||||||
with the latest two entries per blog. It also copies the bundled Heliotrope
|
that sibling repo exists. In standalone Docker/Dokploy builds, it falls back to
|
||||||
fonts into `dist/webfonts/`.
|
the committed `content/blog-index.json`.
|
||||||
|
|
||||||
|
Each blog section shows:
|
||||||
|
|
||||||
|
- one optional fixed featured post marked with `main-feature: true`
|
||||||
|
- the two most recent non-featured posts by `pubDate`
|
||||||
|
|
||||||
|
It also copies the bundled Heliotrope fonts into `dist/webfonts/`.
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
npm run build
|
npm run build
|
||||||
```
|
```
|
||||||
|
|
||||||
|
Refresh the committed content index from the sibling `allblogs` repo with:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
npm run refresh:index
|
||||||
|
```
|
||||||
|
|
||||||
|
To force a build from the committed index, use:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
BLOG_HOME_CONTENT_SOURCE=index npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Use this frontmatter key on at most one post per blog:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
main-feature: true
|
||||||
|
```
|
||||||
|
|
|
||||||
274
content/blog-index.json
Normal file
274
content/blog-index.json
Normal file
|
|
@ -0,0 +1,274 @@
|
||||||
|
{
|
||||||
|
"research": [
|
||||||
|
{
|
||||||
|
"title": "Intro to Crypto",
|
||||||
|
"link": "https://research.neeldhara.blog/crypto-intro/",
|
||||||
|
"date": "2023-12-21",
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Kidney Exchanges",
|
||||||
|
"link": "https://research.neeldhara.blog/kidney-exchanges/",
|
||||||
|
"date": "2022-02-25",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Sam I Am",
|
||||||
|
"link": "https://research.neeldhara.blog/cp/sam-i-am/",
|
||||||
|
"date": "2021-10-01",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "How Expensive Can Homework Help Be?",
|
||||||
|
"link": "https://research.neeldhara.blog/homework-help/",
|
||||||
|
"date": "2010-05-01",
|
||||||
|
"featured": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"vibes": [
|
||||||
|
{
|
||||||
|
"title": "Course Plan Generator",
|
||||||
|
"link": "https://vibes.neeldhara.blog/course-plan/2024-fall/",
|
||||||
|
"date": "2023-12-25",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Course Plan Generator",
|
||||||
|
"link": "https://vibes.neeldhara.blog/course-plan/2024-spring/",
|
||||||
|
"date": "2023-12-25",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Building Interactives",
|
||||||
|
"link": "https://vibes.neeldhara.blog/building-interactives/",
|
||||||
|
"date": "2023-07-30",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Course Plan Generator",
|
||||||
|
"link": "https://vibes.neeldhara.blog/course-plan/",
|
||||||
|
"date": "2023-04-25",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "13 Sheep",
|
||||||
|
"link": "https://vibes.neeldhara.blog/13sheep/",
|
||||||
|
"date": "2023-03-26",
|
||||||
|
"featured": true
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"puzzles": [
|
||||||
|
{
|
||||||
|
"title": "Dog Bunny Puzzle",
|
||||||
|
"link": "https://puzzles.neeldhara.blog/dogs-bunny-puzzle/",
|
||||||
|
"date": "2022-09-19",
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Solo Chess",
|
||||||
|
"link": "https://puzzles.neeldhara.blog/solo-chess/",
|
||||||
|
"date": "2022-03-24",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Two approaches to the 15 puzzle",
|
||||||
|
"link": "https://puzzles.neeldhara.blog/15-puzzle/",
|
||||||
|
"date": "2021-10-03",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Moving Blocks at CTIS 2021",
|
||||||
|
"link": "https://puzzles.neeldhara.blog/moving-blocks-ctis/",
|
||||||
|
"date": "2021-09-21",
|
||||||
|
"featured": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reflections": [
|
||||||
|
{
|
||||||
|
"title": "On Career Choices",
|
||||||
|
"link": "https://reflections.neeldhara.blog/on-career-choices/",
|
||||||
|
"date": "2023-05-26",
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "The Only Fair Ranking of IITs",
|
||||||
|
"link": "https://reflections.neeldhara.blog/iit-rankings/",
|
||||||
|
"date": "2022-11-24",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Exportober 2022",
|
||||||
|
"link": "https://reflections.neeldhara.blog/exportober/2022-tracker/",
|
||||||
|
"date": "2022-10-07",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "SKJ",
|
||||||
|
"link": "https://reflections.neeldhara.blog/skj/",
|
||||||
|
"date": "2022-09-23",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "On Teaching",
|
||||||
|
"link": "https://reflections.neeldhara.blog/on-teaching/",
|
||||||
|
"date": "2022-09-05",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Eight Self-Sabotaging Behaviors",
|
||||||
|
"link": "https://reflections.neeldhara.blog/eight-self-sabotaging-behaviors/",
|
||||||
|
"date": "2022-03-15",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Exportober 2021",
|
||||||
|
"link": "https://reflections.neeldhara.blog/exportober/2021-tracker/",
|
||||||
|
"date": "2021-09-25",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "About Exportober",
|
||||||
|
"link": "https://reflections.neeldhara.blog/exportober/about/",
|
||||||
|
"date": "2021-09-24",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "An Invitation to Exportober 2021",
|
||||||
|
"link": "https://reflections.neeldhara.blog/exportober/2021/",
|
||||||
|
"date": "2021-09-19",
|
||||||
|
"featured": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"poetry": [
|
||||||
|
{
|
||||||
|
"title": "Winning",
|
||||||
|
"link": "https://poetry.neeldhara.blog/poems/winning/",
|
||||||
|
"date": "2024-01-13",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Poetry",
|
||||||
|
"link": "https://poetry.neeldhara.blog/poems/poetry/",
|
||||||
|
"date": "2024-01-01",
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Letting Go",
|
||||||
|
"link": "https://poetry.neeldhara.blog/poems/letting-go/",
|
||||||
|
"date": "2023-05-11",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Dad",
|
||||||
|
"link": "https://poetry.neeldhara.blog/poems/dad/",
|
||||||
|
"date": "2012-08-27",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "On the Fence",
|
||||||
|
"link": "https://poetry.neeldhara.blog/poems/on-the-fence/",
|
||||||
|
"date": "2012-04-23",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Sprinkles of the Sky",
|
||||||
|
"link": "https://poetry.neeldhara.blog/poems/sprinkles-of-the-sky/",
|
||||||
|
"date": "2011-01-05",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Seek",
|
||||||
|
"link": "https://poetry.neeldhara.blog/poems/seek/",
|
||||||
|
"date": "2007-04-03",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Bloom",
|
||||||
|
"link": "https://poetry.neeldhara.blog/poems/bloom/",
|
||||||
|
"date": "2005-02-03",
|
||||||
|
"featured": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"reviews": [
|
||||||
|
{
|
||||||
|
"title": "Exportober 2023",
|
||||||
|
"link": "https://reviews.neeldhara.blog/2023-exportober-workflows/",
|
||||||
|
"date": "2023-10-01",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Note Taking Resources",
|
||||||
|
"link": "https://reviews.neeldhara.blog/note-taking-resources/",
|
||||||
|
"date": "2023-06-02",
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Letters with Pandoc",
|
||||||
|
"link": "https://reviews.neeldhara.blog/pandoc-letters/",
|
||||||
|
"date": "2022-11-27",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Women in Mathematics",
|
||||||
|
"link": "https://reviews.neeldhara.blog/women-in-mathematics/",
|
||||||
|
"date": "2022-02-21",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "New Mac",
|
||||||
|
"link": "https://reviews.neeldhara.blog/new-mac/",
|
||||||
|
"date": "2021-09-30",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Envelope Budgeting with Notion",
|
||||||
|
"link": "https://reviews.neeldhara.blog/envelope-budgeting-notion/",
|
||||||
|
"date": "2021-09-18",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Actually Building a Website with Notion",
|
||||||
|
"link": "https://reviews.neeldhara.blog/building-websites-with-notion/",
|
||||||
|
"date": "2021-09-12",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Notion-powered websites",
|
||||||
|
"link": "https://reviews.neeldhara.blog/notion-powered-websites/",
|
||||||
|
"date": "2021-09-11",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Massren for fast file renaming",
|
||||||
|
"link": "https://reviews.neeldhara.blog/massren/",
|
||||||
|
"date": "2020-09-11",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "Building a first Django App",
|
||||||
|
"link": "https://reviews.neeldhara.blog/django-app/",
|
||||||
|
"date": "2018-06-12",
|
||||||
|
"featured": false
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"art": [
|
||||||
|
{
|
||||||
|
"title": "Creating a Gallery of Solved Crosswords",
|
||||||
|
"link": "https://art.neeldhara.blog/indiamini-crosswords/",
|
||||||
|
"date": "2024-05-10",
|
||||||
|
"featured": true
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "External Communications",
|
||||||
|
"link": "https://art.neeldhara.blog/comms/",
|
||||||
|
"date": "2022-09-23",
|
||||||
|
"featured": false
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"title": "On the Communication Complexity of Equality",
|
||||||
|
"link": "https://art.neeldhara.blog/communication-complexity-equality/",
|
||||||
|
"date": "2021-10-04",
|
||||||
|
"featured": false
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
@ -4,6 +4,7 @@
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"build": "node scripts/build.mjs"
|
"build": "node scripts/build.mjs",
|
||||||
|
"refresh:index": "node scripts/build.mjs --write-content-index"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,4 +1,13 @@
|
||||||
import { copyFile, mkdir, writeFile } from "node:fs/promises";
|
import {
|
||||||
|
access,
|
||||||
|
copyFile,
|
||||||
|
mkdir,
|
||||||
|
readdir,
|
||||||
|
readFile,
|
||||||
|
writeFile,
|
||||||
|
} from "node:fs/promises";
|
||||||
|
import path from "node:path";
|
||||||
|
import { fileURLToPath } from "node:url";
|
||||||
|
|
||||||
const blogs = [
|
const blogs = [
|
||||||
{
|
{
|
||||||
|
|
@ -77,10 +86,13 @@ const socialLinks = [
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rssHostSuffix = process.env.RSS_HOST_SUFFIX ?? "neeldhara.email";
|
|
||||||
const publicHostSuffix = process.env.PUBLIC_HOST_SUFFIX ?? "neeldhara.blog";
|
const publicHostSuffix = process.env.PUBLIC_HOST_SUFFIX ?? "neeldhara.blog";
|
||||||
|
const allblogsRoot = new URL("../../allblogs/", import.meta.url);
|
||||||
|
const contentIndex = new URL("../content/blog-index.json", import.meta.url);
|
||||||
const outDir = new URL("../dist/", import.meta.url);
|
const outDir = new URL("../dist/", import.meta.url);
|
||||||
const fontDir = new URL("../webfonts/", import.meta.url);
|
const fontDir = new URL("../webfonts/", import.meta.url);
|
||||||
|
const shouldWriteContentIndex = process.argv.includes("--write-content-index");
|
||||||
|
const contentSource = process.env.BLOG_HOME_CONTENT_SOURCE ?? "auto";
|
||||||
const fontFiles = [
|
const fontFiles = [
|
||||||
"heliotrope_3_regular.woff2",
|
"heliotrope_3_regular.woff2",
|
||||||
"heliotrope_3_italic.woff2",
|
"heliotrope_3_italic.woff2",
|
||||||
|
|
@ -88,50 +100,145 @@ const fontFiles = [
|
||||||
"heliotrope_3_bold_italic.woff2",
|
"heliotrope_3_bold_italic.woff2",
|
||||||
];
|
];
|
||||||
|
|
||||||
function decodeXml(value) {
|
|
||||||
return value
|
|
||||||
.replace(/'/g, "'")
|
|
||||||
.replace(/"/g, '"')
|
|
||||||
.replace(/>/g, ">")
|
|
||||||
.replace(/</g, "<")
|
|
||||||
.replace(/&/g, "&");
|
|
||||||
}
|
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return value
|
return value
|
||||||
|
.toString()
|
||||||
.replace(/&/g, "&")
|
.replace(/&/g, "&")
|
||||||
.replace(/</g, "<")
|
.replace(/</g, "<")
|
||||||
.replace(/>/g, ">")
|
.replace(/>/g, ">")
|
||||||
.replace(/"/g, """);
|
.replace(/"/g, """);
|
||||||
}
|
}
|
||||||
|
|
||||||
function textFrom(block, tag) {
|
function parseScalar(value) {
|
||||||
const match = block.match(new RegExp(`<${tag}[^>]*>([\\s\\S]*?)</${tag}>`, "i"));
|
const trimmed = value.trim();
|
||||||
return match ? decodeXml(match[1].trim()) : "";
|
|
||||||
|
if (trimmed === "true") return true;
|
||||||
|
if (trimmed === "false") return false;
|
||||||
|
|
||||||
|
if (
|
||||||
|
(trimmed.startsWith('"') && trimmed.endsWith('"')) ||
|
||||||
|
(trimmed.startsWith("'") && trimmed.endsWith("'"))
|
||||||
|
) {
|
||||||
|
return trimmed.slice(1, -1);
|
||||||
}
|
}
|
||||||
|
|
||||||
function parseItems(xml) {
|
if (trimmed.startsWith("[") && trimmed.endsWith("]")) {
|
||||||
return [...xml.matchAll(/<item>([\s\S]*?)<\/item>/gi)].slice(0, 2).map((match) => {
|
return trimmed
|
||||||
const block = match[1];
|
.slice(1, -1)
|
||||||
const pubDate = new Date(textFrom(block, "pubDate"));
|
.split(",")
|
||||||
|
.map((item) => parseScalar(item))
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return trimmed;
|
||||||
title: textFrom(block, "title"),
|
}
|
||||||
link: textFrom(block, "link"),
|
|
||||||
date: Number.isNaN(pubDate.valueOf()) ? "" : pubDate.toISOString().slice(0, 10),
|
function parseFrontmatter(markdown) {
|
||||||
};
|
if (!markdown.startsWith("---")) return {};
|
||||||
|
|
||||||
|
const end = markdown.indexOf("\n---", 3);
|
||||||
|
if (end === -1) return {};
|
||||||
|
|
||||||
|
const yaml = markdown.slice(3, end).trim();
|
||||||
|
const data = {};
|
||||||
|
|
||||||
|
for (const line of yaml.split(/\r?\n/)) {
|
||||||
|
const match = line.match(/^([A-Za-z0-9_-]+):\s*(.*)$/);
|
||||||
|
if (!match) continue;
|
||||||
|
|
||||||
|
data[match[1]] = parseScalar(match[2]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
async function pathExists(filePath) {
|
||||||
|
try {
|
||||||
|
await access(filePath);
|
||||||
|
return true;
|
||||||
|
} catch {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function findMarkdownFiles(dir) {
|
||||||
|
const files = [];
|
||||||
|
const entries = await readdir(dir, { withFileTypes: true });
|
||||||
|
|
||||||
|
for (const entry of entries) {
|
||||||
|
const fullPath = path.join(dir, entry.name);
|
||||||
|
|
||||||
|
if (entry.isDirectory()) {
|
||||||
|
files.push(...(await findMarkdownFiles(fullPath)));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.isFile() && /\.(md|mdx)$/i.test(entry.name)) {
|
||||||
|
files.push(fullPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return files;
|
||||||
|
}
|
||||||
|
|
||||||
|
function slugFromFile(filePath, contentDir) {
|
||||||
|
const relative = path.relative(contentDir, filePath).replaceAll(path.sep, "/");
|
||||||
|
return relative.replace(/\/index\.mdx?$/i, "").replace(/\.mdx?$/i, "");
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getLocalPosts(blog) {
|
||||||
|
const contentDir = fileURLToPath(
|
||||||
|
new URL(`sites/${blog.slug}/src/content/${blog.slug}/`, allblogsRoot),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!(await pathExists(contentDir))) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
const files = await findMarkdownFiles(contentDir);
|
||||||
|
const posts = [];
|
||||||
|
|
||||||
|
for (const file of files) {
|
||||||
|
const data = parseFrontmatter(await readFile(file, "utf8"));
|
||||||
|
const pubDate = new Date(`${data.pubDate}T00:00:00Z`);
|
||||||
|
|
||||||
|
if (!data.title || Number.isNaN(pubDate.valueOf())) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const slug = slugFromFile(file, contentDir);
|
||||||
|
|
||||||
|
posts.push({
|
||||||
|
title: data.title,
|
||||||
|
link: `https://${blog.slug}.${publicHostSuffix}/${slug}/`,
|
||||||
|
date: pubDate.toISOString().slice(0, 10),
|
||||||
|
featured: data["main-feature"] === true || data.mainFeature === true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPosts(slug) {
|
return posts.sort((a, b) => b.date.localeCompare(a.date));
|
||||||
const url = `https://${slug}.${rssHostSuffix}/rss.xml`;
|
|
||||||
const response = await fetch(url);
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new Error(`${url} returned ${response.status}`);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return parseItems(await response.text());
|
async function getIndexedPosts() {
|
||||||
|
const index = JSON.parse(await readFile(contentIndex, "utf8"));
|
||||||
|
|
||||||
|
return blogs.map((blog) => [blog, index[blog.slug] ?? []]);
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getEntries() {
|
||||||
|
if (contentSource === "index") {
|
||||||
|
return getIndexedPosts();
|
||||||
|
}
|
||||||
|
|
||||||
|
const localEntries = await Promise.all(
|
||||||
|
blogs.map(async (blog) => [blog, await getLocalPosts(blog)]),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (localEntries.every(([, posts]) => posts !== null)) {
|
||||||
|
return localEntries;
|
||||||
|
}
|
||||||
|
|
||||||
|
return getIndexedPosts();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderPost(post) {
|
function renderPost(post) {
|
||||||
|
|
@ -143,6 +250,12 @@ function renderPost(post) {
|
||||||
return `<li class="post"><span class="date">${escapeHtml(dateLabel)}</span><a href="${escapeHtml(post.link)}">${escapeHtml(post.title)}</a></li>`;
|
return `<li class="post"><span class="date">${escapeHtml(dateLabel)}</span><a href="${escapeHtml(post.link)}">${escapeHtml(post.title)}</a></li>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderFeaturedPost(post) {
|
||||||
|
if (!post) return "";
|
||||||
|
|
||||||
|
return `<p class="featured-post"><span>Featured</span><a href="${escapeHtml(post.link)}">${escapeHtml(post.title)}</a></p>`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderRssIcon(blog) {
|
function renderRssIcon(blog) {
|
||||||
const href = `https://${blog.slug}.${publicHostSuffix}/rss.xml`;
|
const href = `https://${blog.slug}.${publicHostSuffix}/rss.xml`;
|
||||||
|
|
||||||
|
|
@ -177,6 +290,9 @@ function renderSocialLinks() {
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderBlog(blog, posts) {
|
function renderBlog(blog, posts) {
|
||||||
|
const featured = posts.find((post) => post.featured);
|
||||||
|
const latest = posts.filter((post) => post !== featured).slice(0, 2);
|
||||||
|
|
||||||
return `<details class="blog">
|
return `<details class="blog">
|
||||||
<summary>
|
<summary>
|
||||||
<span>${escapeHtml(blog.label)}</span>
|
<span>${escapeHtml(blog.label)}</span>
|
||||||
|
|
@ -186,15 +302,26 @@ function renderBlog(blog, posts) {
|
||||||
</span>
|
</span>
|
||||||
</summary>
|
</summary>
|
||||||
<p>${escapeHtml(blog.summary)}</p>
|
<p>${escapeHtml(blog.summary)}</p>
|
||||||
|
${renderFeaturedPost(featured)}
|
||||||
<ul>
|
<ul>
|
||||||
${posts.map(renderPost).join("\n ")}
|
${latest.map(renderPost).join("\n ")}
|
||||||
</ul>
|
</ul>
|
||||||
</details>`;
|
</details>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const entries = await Promise.all(
|
const entries = await getEntries();
|
||||||
blogs.map(async (blog) => [blog, await getPosts(blog.slug)]),
|
|
||||||
|
if (shouldWriteContentIndex) {
|
||||||
|
await mkdir(new URL("../content/", import.meta.url), { recursive: true });
|
||||||
|
await writeFile(
|
||||||
|
contentIndex,
|
||||||
|
`${JSON.stringify(
|
||||||
|
Object.fromEntries(entries.map(([blog, posts]) => [blog.slug, posts])),
|
||||||
|
null,
|
||||||
|
2,
|
||||||
|
)}\n`,
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const html = `<!doctype html>
|
const html = `<!doctype html>
|
||||||
<html lang="en">
|
<html lang="en">
|
||||||
|
|
@ -393,6 +520,18 @@ const html = `<!doctype html>
|
||||||
margin-top: 9px;
|
margin-top: 9px;
|
||||||
color: #555;
|
color: #555;
|
||||||
}
|
}
|
||||||
|
.featured-post {
|
||||||
|
display: flex;
|
||||||
|
align-items: baseline;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
.featured-post span {
|
||||||
|
color: #666;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.featured-post a {
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
ul {
|
ul {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 7px;
|
gap: 7px;
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue