Add split-panel homepage layout
Some checks are pending
Build / build (push) Waiting to run

This commit is contained in:
Neeldhara Misra 2026-06-13 22:19:10 +02:00
parent aa78b43c34
commit 4d9bf43f75

View file

@ -1,16 +1,45 @@
import { mkdir, writeFile } from "node:fs/promises"; import { mkdir, writeFile } from "node:fs/promises";
const blogs = [ const blogs = [
["research", "research"], {
["vibes", "vibes"], slug: "research",
["puzzles", "puzzles"], label: "Research",
["reflections", "reflections"], summary: "Notes from papers, algorithms, computational social choice, and mini-surveys of themes I am learning about.",
["poetry", "poetry"], },
["reviews", "reviews"], {
["art", "art"], slug: "vibes",
label: "Vibes",
summary: "Loose experiments, AI conversations, and work-in-progress thoughts that do not need a formal frame.",
},
{
slug: "puzzles",
label: "Puzzles",
summary: "Problems, teaching prompts, contest-style curiosities, and small mathematical rabbit holes.",
},
{
slug: "reflections",
label: "Reflections",
summary: "Personal notes on teaching, academia, writing, attention, and the rhythms of intellectual work.",
},
{
slug: "poetry",
label: "Poetry",
summary: "Poems, tiny forms, and playful writing around mathematics, code, and ordinary life.",
},
{
slug: "reviews",
label: "Reviews",
summary: "Notes on tools, workflows, books, hardware, and objects that shape everyday work.",
},
{
slug: "art",
label: "Art",
summary: "Algorithmic sketches, generative art, visual experiments, and mathematical images.",
},
]; ];
const rssHostSuffix = process.env.RSS_HOST_SUFFIX ?? "neeldhara.email"; const rssHostSuffix = process.env.RSS_HOST_SUFFIX ?? "neeldhara.email";
const publicHostSuffix = process.env.PUBLIC_HOST_SUFFIX ?? "neeldhara.blog";
const outDir = new URL("../dist/", import.meta.url); const outDir = new URL("../dist/", import.meta.url);
function decodeXml(value) { function decodeXml(value) {
@ -60,20 +89,41 @@ async function getPosts(slug) {
} }
function renderPost(post) { function renderPost(post) {
const date = post.date ? `${escapeHtml(post.date)} ` : ""; const date = post.date ? new Date(`${post.date}T00:00:00Z`) : null;
return `<li>${date}<a href="${escapeHtml(post.link)}">${escapeHtml(post.title)}</a></li>`; const dateLabel = date
? date.toLocaleDateString("en", { month: "short", day: "numeric", timeZone: "UTC" })
: "";
return `<li class="post"><span class="date">${escapeHtml(dateLabel)}</span><a href="${escapeHtml(post.link)}">${escapeHtml(post.title)}</a></li>`;
} }
function renderBlog([slug, label], posts) { function renderRssIcon(blog) {
return `<li>${escapeHtml(label)} const href = `https://${blog.slug}.${publicHostSuffix}/rss.xml`;
return `<a class="rss-link" href="${escapeHtml(href)}" aria-label="${escapeHtml(blog.label)} RSS">
<svg viewBox="0 0 24 24" aria-hidden="true">
<circle cx="6.25" cy="17.75" r="1.8"></circle>
<path d="M4.5 5.5c7.7 0 14 6.3 14 14"></path>
<path d="M4.5 10.2a9.3 9.3 0 0 1 9.3 9.3"></path>
</svg>
</a>`;
}
function renderBlog(blog, posts) {
return `<details class="blog" open>
<summary>
<span>${escapeHtml(blog.label)}</span>
${renderRssIcon(blog)}
</summary>
<p>${escapeHtml(blog.summary)}</p>
<ul> <ul>
${posts.map(renderPost).join("\n ")} ${posts.map(renderPost).join("\n ")}
</ul> </ul>
</li>`; </details>`;
} }
const entries = await Promise.all( const entries = await Promise.all(
blogs.map(async (blog) => [blog, await getPosts(blog[0])]), blogs.map(async (blog) => [blog, await getPosts(blog.slug)]),
); );
const html = `<!doctype html> const html = `<!doctype html>
@ -84,42 +134,200 @@ const html = `<!doctype html>
<title>neeldhara.blog</title> <title>neeldhara.blog</title>
<style> <style>
html { color-scheme: light; } html { color-scheme: light; }
* { box-sizing: border-box; }
body { body {
margin: 0; margin: 0;
background: #fff; background: #f0f0f0;
color: #111; color: #111;
font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, "Liberation Mono", monospace; font-family: Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
font-size: 16px; font-size: 15px;
line-height: 1.55; line-height: 1.45;
} }
a {
color: inherit;
text-decoration-color: #c6c6c6;
text-underline-offset: 3px;
}
a:hover { text-decoration-color: #111; }
main { main {
width: min(640px, calc(100% - 40px)); display: grid;
margin: 86px auto; grid-template-columns: minmax(0, 1.75fr) minmax(320px, 1fr);
width: min(1220px, calc(100% - 72px));
min-height: min(860px, calc(100vh - 144px));
margin: 72px auto;
background: #d9d9d9;
} }
h1 { .left,
margin: 0 0 18px; .right {
font-size: 28px; min-height: inherit;
padding: 34px 32px;
}
.left {
display: flex;
flex-direction: column;
gap: 56px;
}
.right {
display: flex;
flex-direction: column;
justify-content: space-between;
background: #fff;
}
.mark {
display: block;
width: 13px;
height: 13px;
border: 2px solid #111;
border-radius: 50%;
}
h1,
h2,
p {
margin: 0;
}
h1,
h2 {
font-size: 15px;
font-weight: 700;
line-height: 1.2; line-height: 1.2;
} }
ul { .role {
margin-top: 0; color: #666;
margin-bottom: 0.75rem; font-weight: 500;
padding-left: 1.45rem; }
.intro {
max-width: 460px;
font-weight: 700;
}
.blog-list {
display: grid;
gap: 28px;
}
.blog {
max-width: 650px;
}
summary {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
cursor: pointer;
font-weight: 700;
list-style: none;
}
summary::-webkit-details-marker {
display: none;
}
summary::before {
content: "+";
width: 18px;
flex: 0 0 18px;
color: #666;
font-weight: 500;
}
details[open] summary::before {
content: "-";
}
summary > span {
flex: 0 1 auto;
}
.rss-link {
display: inline-flex;
align-items: center;
justify-content: center;
width: 22px;
height: 22px;
color: #111;
}
.rss-link svg {
width: 16px;
height: 16px;
fill: none;
stroke: currentColor;
stroke-width: 1.9;
stroke-linecap: round;
}
.rss-link circle {
fill: currentColor;
stroke: none;
}
.blog p {
max-width: 540px;
margin-top: 9px;
color: #555;
}
ul {
display: grid;
gap: 7px;
margin: 14px 0 0;
padding: 0;
list-style: none;
}
.post {
display: grid;
grid-template-columns: 64px minmax(0, 1fr);
gap: 0;
}
.date {
color: #666;
}
.post a {
font-weight: 700;
}
.bio {
display: grid;
gap: 24px;
max-width: 390px;
font-weight: 700;
}
.bio a {
width: fit-content;
}
@media (max-width: 860px) {
main {
grid-template-columns: 1fr;
width: min(100%, calc(100% - 28px));
margin: 14px auto;
}
.left,
.right {
min-height: auto;
padding: 28px 26px;
}
.left {
gap: 40px;
}
.right {
gap: 72px;
} }
li { margin: 0.18rem 0; }
a { color: #00e; }
a:visited { color: #551a8b; }
@media (max-width: 640px) {
main { margin: 42px auto; }
} }
</style> </style>
</head> </head>
<body> <body>
<main> <main>
<h1>neeldhara.blog</h1> <section class="left" aria-label="Blog index">
<ul> <span class="mark" aria-hidden="true"></span>
<div>
<h1>Neeldhara Misra</h1>
<p class="role">seven small blogs</p>
</div>
<p class="intro">A quiet index of notes on research, puzzles, writing, tools, art, and other recurring fascinations.</p>
<div class="blog-list">
${entries.map(([blog, posts]) => renderBlog(blog, posts)).join("\n ")} ${entries.map(([blog, posts]) => renderBlog(blog, posts)).join("\n ")}
</ul> </div>
</section>
<aside class="right" aria-label="About">
<div>
<h2>Neeldhara Misra</h2>
<p class="role">Computer Science and Engineering, IIT Gandhinagar</p>
</div>
<div class="bio">
<p>I am a computer science faculty member at IIT Gandhinagar. My academic work is broadly around algorithms, structural graph theory, parameterized complexity, computational social choice, combinatorial games, and fair allocation.</p>
<p>These blogs separate different kinds of notes: paper-reading and mini-surveys, puzzle-like teaching prompts, reflective essays, poems, reviews of tools and workflows, experiments with AI, and visual or generative art.</p>
<p>The homepage is deliberately small: each section shows the two newest posts and links to that blog's RSS feed.</p>
<a href="https://www.neeldhara.com/">neeldhara.com</a>
</div>
</aside>
</main> </main>
</body> </body>
</html> </html>