Skip to content

Commit c5ca8cc

Browse files
committed
Add foldable table of contents to blog posts
1 parent 33f35f5 commit c5ca8cc

4 files changed

Lines changed: 179 additions & 71 deletions

File tree

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
---
2+
export interface Props {
3+
headings: {
4+
depth: number;
5+
slug: string;
6+
text: string;
7+
}[];
8+
}
9+
10+
const { headings } = Astro.props;
11+
12+
// Filter to only show h2 and h3 headings
13+
const tocHeadings = headings.filter(h => h.depth === 2 || h.depth === 3);
14+
---
15+
16+
{tocHeadings.length > 0 && (
17+
<div class="toc-container mb-8 border border-gray-200 rounded-xl overflow-hidden bg-white">
18+
<button
19+
id="toc-toggle"
20+
class="w-full flex items-center justify-between px-6 py-4 text-left hover:bg-gray-50 transition-colors"
21+
>
22+
<div class="flex items-center gap-3">
23+
<i class="fas fa-list-ul text-gray-400 text-sm"></i>
24+
<span class="font-semibold text-gray-900">Table of Contents</span>
25+
</div>
26+
<i id="toc-icon" class="fas fa-chevron-down text-gray-400 text-sm transition-transform duration-200"></i>
27+
</button>
28+
29+
<nav id="toc-content" class="px-6 pb-4 hidden">
30+
<ul class="space-y-2">
31+
{tocHeadings.map((heading) => (
32+
<li class={heading.depth === 3 ? 'ml-4' : ''}>
33+
<a
34+
href={`#${heading.slug}`}
35+
class="text-sm text-gray-600 hover:text-primary transition-colors block py-1"
36+
>
37+
{heading.text}
38+
</a>
39+
</li>
40+
))}
41+
</ul>
42+
</nav>
43+
</div>
44+
)}
45+
46+
<script>
47+
document.addEventListener('DOMContentLoaded', () => {
48+
const toggle = document.getElementById('toc-toggle');
49+
const content = document.getElementById('toc-content');
50+
const icon = document.getElementById('toc-icon');
51+
52+
if (toggle && content && icon) {
53+
toggle.addEventListener('click', () => {
54+
const isHidden = content.classList.contains('hidden');
55+
56+
if (isHidden) {
57+
content.classList.remove('hidden');
58+
icon.style.transform = 'rotate(0deg)';
59+
} else {
60+
content.classList.add('hidden');
61+
icon.style.transform = 'rotate(-90deg)';
62+
}
63+
});
64+
65+
// Set initial state
66+
icon.style.transform = 'rotate(-90deg)';
67+
}
68+
});
69+
</script>

src/content/blog/curse-of-coordination.mdx

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ authors:
77
image: "/static/images/arpan.jpg"
88
- name: "Hao Zhu"
99
image: "/static/images/hao_zhu.png"
10-
layout: "../../layouts/BlogPostLayout.astro"
1110
---
1211

1312
{/* TL;DR */}

src/layouts/BlogPostLayout.astro

Lines changed: 99 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -1,64 +1,101 @@
11
---
2-
import BaseLayout from './BaseLayout.astro';
2+
import BaseLayout from "./BaseLayout.astro";
3+
import TableOfContents from "../components/TableOfContents.astro";
34
4-
const { frontmatter } = Astro.props;
5+
const { frontmatter, headings } = Astro.props;
56
---
67

7-
<BaseLayout title={frontmatter.title} description={frontmatter.description} activeNav="Blog">
8-
<main class="pt-24 pb-20">
9-
<article class="max-w-3xl mx-auto px-6">
10-
<!-- Article Header -->
11-
<header class="mb-12">
12-
<p class="text-sm text-gray-500 mb-4">{frontmatter.pubDate}</p>
13-
<h1 class="text-4xl md:text-5xl font-bold text-gray-900 leading-tight mb-6">
14-
{frontmatter.title}
15-
</h1>
16-
<p class="text-xl text-gray-600 leading-relaxed">
17-
{frontmatter.description}
18-
</p>
19-
</header>
20-
21-
<!-- Author Info -->
22-
<div class="flex items-center gap-4 mb-12 pb-8 border-b border-gray-200">
23-
<div class="flex -space-x-2">
24-
{frontmatter.authors.map((author: any) => (
25-
<img src={author.image} alt={author.name} class="w-10 h-10 rounded-full border-2 border-white" />
26-
))}
27-
</div>
28-
<div>
29-
<p class="text-sm font-medium text-gray-900">
30-
{frontmatter.authors.map((author: any) => author.name).join(', ')}
8+
<BaseLayout
9+
title={frontmatter.title}
10+
description={frontmatter.description}
11+
activeNav="Blog"
12+
>
13+
<main class="pt-24 pb-20">
14+
<article class="max-w-3xl mx-auto px-6">
15+
<!-- Article Header -->
16+
<header class="mb-12">
17+
<p class="text-sm text-gray-500 mb-4">{frontmatter.pubDate}</p>
18+
<h1
19+
class="text-4xl md:text-5xl font-bold text-gray-900 leading-tight mb-6"
20+
>
21+
{frontmatter.title}
22+
</h1>
23+
<p class="text-xl text-gray-600 leading-relaxed">
24+
{frontmatter.description}
3125
</p>
26+
</header>
27+
28+
<!-- Author Info -->
29+
<div
30+
class="flex items-center gap-4 mb-12 pb-8 border-b border-gray-200"
31+
>
32+
<div class="flex -space-x-2">
33+
{
34+
frontmatter.authors.map((author: any) => (
35+
<img
36+
src={author.image}
37+
alt={author.name}
38+
class="w-10 h-10 rounded-full border-2 border-white"
39+
/>
40+
))
41+
}
42+
</div>
43+
<div>
44+
<p class="text-sm font-medium text-gray-900">
45+
{
46+
frontmatter.authors
47+
.map((author: any) => author.name)
48+
.join(", ")
49+
}
50+
</p>
51+
</div>
3252
</div>
33-
</div>
3453

35-
<!-- Article Body -->
36-
<div class="prose prose-lg max-w-none">
37-
<slot />
38-
</div>
54+
<!-- Table of Contents -->
55+
<TableOfContents headings={headings} />
3956

40-
<!-- Call to Action -->
41-
<div class="mt-16 pt-8 border-t border-gray-200">
42-
<div class="flex flex-wrap gap-4 justify-center">
43-
<a href="/static/pdfs/main.pdf" target="_blank" class="inline-flex items-center px-5 py-2.5 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors">
44-
<i class="fas fa-file-pdf mr-2"></i>
45-
Read the Paper
46-
</a>
47-
<a href="https://github.com/cooperbench/CooperBench" target="_blank" class="inline-flex items-center px-5 py-2.5 border border-gray-300 text-gray-700 text-sm font-medium rounded-lg hover:border-gray-400 hover:bg-gray-50 transition-colors">
48-
<i class="fab fa-github mr-2"></i>
49-
View Code
50-
</a>
51-
<a href="https://huggingface.co/CodeConflict" target="_blank" class="inline-flex items-center px-5 py-2.5 border border-gray-300 text-gray-700 text-sm font-medium rounded-lg hover:border-gray-400 hover:bg-gray-50 transition-colors">
52-
<i class="fas fa-database mr-2"></i>
53-
Dataset
54-
</a>
57+
<!-- Article Body -->
58+
<div class="prose prose-lg max-w-none">
59+
<slot />
5560
</div>
56-
</div>
5761

58-
<!-- Citation -->
59-
<div class="mt-12 p-6 bg-gray-50 rounded-xl border border-gray-200">
60-
<h3 class="text-sm font-semibold text-gray-900 mb-3">Cite this work</h3>
61-
<pre class="text-xs text-gray-600 overflow-x-auto"><code>@article&#123;cooperbench2026,
62+
<!-- Call to Action -->
63+
<div class="mt-16 pt-8 border-t border-gray-200">
64+
<div class="flex flex-wrap gap-4 justify-center">
65+
<a
66+
href="/static/pdfs/main.pdf"
67+
target="_blank"
68+
class="inline-flex items-center px-5 py-2.5 bg-gray-900 text-white text-sm font-medium rounded-lg hover:bg-gray-800 transition-colors"
69+
>
70+
<i class="fas fa-file-pdf mr-2"></i>
71+
Read the Paper
72+
</a>
73+
<a
74+
href="https://github.com/cooperbench/CooperBench"
75+
target="_blank"
76+
class="inline-flex items-center px-5 py-2.5 border border-gray-300 text-gray-700 text-sm font-medium rounded-lg hover:border-gray-400 hover:bg-gray-50 transition-colors"
77+
>
78+
<i class="fab fa-github mr-2"></i>
79+
View Code
80+
</a>
81+
<a
82+
href="https://huggingface.co/CodeConflict"
83+
target="_blank"
84+
class="inline-flex items-center px-5 py-2.5 border border-gray-300 text-gray-700 text-sm font-medium rounded-lg hover:border-gray-400 hover:bg-gray-50 transition-colors"
85+
>
86+
<i class="fas fa-database mr-2"></i>
87+
Dataset
88+
</a>
89+
</div>
90+
</div>
91+
92+
<!-- Citation -->
93+
<div class="mt-12 p-6 bg-gray-50 rounded-xl border border-gray-200">
94+
<h3 class="text-sm font-semibold text-gray-900 mb-3">
95+
Cite this work
96+
</h3>
97+
<pre
98+
class="text-xs text-gray-600 overflow-x-auto"><code>@article&#123;cooperbench2026,
6299
title=&#123;CooperBench: Why Coding Agents Cannot be Your Teammates Yet&#125;,
63100
author=&#123;Khatua*, Arpandeep and Zhu*, Hao and Tran†, Peter and Prabhudesai†, Arya
64101
and Sadrieh†, Frederic and Lieberwirth†, Johann K. and Yu, Xinkai
@@ -68,24 +105,24 @@ const { frontmatter } = Astro.props;
68105
url=&#123;https://cooperbench.com/&#125;,
69106
note=&#123;*Equal contribution (Stanford) · †Equal contribution (SAP Labs)&#125;
70107
&#125;</code></pre>
71-
</div>
72-
</article>
108+
</div>
109+
</article>
73110

74-
<!-- Scroll to Top Button -->
75-
<button
76-
class="fixed bottom-8 right-8 w-10 h-10 bg-gray-900 text-white rounded-full opacity-0 invisible transition-all duration-300 z-50 flex items-center justify-center text-sm shadow-lg hover:bg-gray-800 scroll-to-top"
77-
title="Scroll to top"
78-
>
79-
<i class="fas fa-chevron-up"></i>
80-
</button>
81-
</main>
111+
<!-- Scroll to Top Button -->
112+
<button
113+
class="fixed bottom-8 right-8 w-10 h-10 bg-gray-900 text-white rounded-full opacity-0 invisible transition-all duration-300 z-50 flex items-center justify-center text-sm shadow-lg hover:bg-gray-800 scroll-to-top"
114+
title="Scroll to top"
115+
>
116+
<i class="fas fa-chevron-up"></i>
117+
</button>
118+
</main>
82119
</BaseLayout>
83120

84121
<script>
85122
const scrollBtn = document.querySelector(".scroll-to-top");
86123
if (scrollBtn) {
87124
scrollBtn.addEventListener("click", () => {
88-
window.scrollTo({ top: 0, behavior: 'smooth' });
125+
window.scrollTo({ top: 0, behavior: "smooth" });
89126
});
90127

91128
window.addEventListener("scroll", function () {

src/pages/blog/[slug].astro

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,19 @@
11
---
2-
import { getCollection } from 'astro:content';
2+
import { getCollection } from "astro:content";
3+
import BlogPostLayout from "../../layouts/BlogPostLayout.astro";
34
45
export async function getStaticPaths() {
5-
const blogEntries = await getCollection('blog');
6-
return blogEntries.map(entry => ({
7-
params: { slug: entry.slug },
8-
props: { entry },
9-
}));
6+
const blogEntries = await getCollection("blog");
7+
return blogEntries.map((entry) => ({
8+
params: { slug: entry.slug },
9+
props: { entry },
10+
}));
1011
}
1112
1213
const { entry } = Astro.props;
13-
const { Content } = await entry.render();
14+
const { Content, headings } = await entry.render();
1415
---
1516

16-
<Content />
17+
<BlogPostLayout frontmatter={entry.data} headings={headings}>
18+
<Content />
19+
</BlogPostLayout>

0 commit comments

Comments
 (0)