Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
93 changes: 93 additions & 0 deletions web/apps/web/app/resources/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
import { Badge, Eyebrow, Prose, Section } from "@nativelink/ui";
import { marked } from "marked";
import { notFound } from "next/navigation";
import { formatPostDate, getAllPosts, getPost } from "../../../../lib/posts";

export function generateStaticParams() {
return getAllPosts().map((post) => ({ slug: post.slug }));
}

export async function generateMetadata({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const post = getPost((await params).slug);
if (!post) return {};
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
type: "article",
...(post.image ? { images: [{ url: post.image }] } : {}),
},
};
}

export default async function BlogPostPage({
params,
}: {
params: Promise<{ slug: string }>;
}) {
const post = getPost((await params).slug);
if (!post) notFound();

const html = marked.parse(post.body, { async: false });

return (
<>
<section className="relative overflow-hidden">
<div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[400px] bg-[radial-gradient(ellipse_900px_400px_at_50%_-10%,rgb(var(--nl-color-brand)/0.10),transparent_70%)]" />
<Section width="default" className="pt-24 pb-10 md:pt-32">
<div className="mx-auto max-w-[760px]">
<a
href="/resources/blog"
className="font-mono text-sm text-brand transition-colors hover:text-foreground"
>
← All posts
</a>
<div className="mt-8 flex flex-wrap items-center gap-2">
{post.tags.map((tag) => (
<Badge key={tag} variant="outline">
{tag}
</Badge>
))}
</div>
<h1 className="mt-5 text-balance text-[34px] font-semibold leading-[1.1] tracking-[-0.03em] md:text-[46px]">
{post.title}
</h1>
<p className="mt-5 font-mono text-sm text-muted">
{formatPostDate(post.pubDate)}
{post.readTime ? ` · ${post.readTime}` : null}
</p>
</div>
</Section>
</section>

<Section width="default" className="pb-28">
<div className="mx-auto max-w-[760px]">
<Prose
className="max-w-none [&_img]:my-6 [&_img]:max-w-full [&_img]:rounded-xl"
// biome-ignore lint/security/noDangerouslySetInnerHtml: Post bodies are trusted repo content, rendered from markdown at build time.
dangerouslySetInnerHTML={{ __html: html }}
/>
<div className="mt-16 border-t border-border pt-8">
<Eyebrow tone="muted" className="text-[10px]">
NativeLink Blog
</Eyebrow>
<p className="mt-3 text-sm text-muted-foreground">
<a
href="/resources/blog"
className="text-brand transition-colors hover:text-foreground"
>
← Back to all posts
</a>
</p>
</div>
</div>
</Section>
</>
);
}
49 changes: 49 additions & 0 deletions web/apps/web/app/resources/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import { Eyebrow, Reveal, Section } from "@nativelink/ui";
import { getAllPosts } from "../../../lib/posts";
import { PostSection, postToCard } from "../post-sections";

export const metadata = { title: "Blog" };

export default function BlogIndexPage() {
const posts = getAllPosts();
const caseStudies = posts.filter((p) => p.tags.includes("case-studies"));
const announcements = posts.filter(
(p) => p.tags.includes("announcements") && !p.tags.includes("case-studies"),
);
const others = posts.filter(
(p) => !p.tags.includes("case-studies") && !p.tags.includes("announcements"),
);

return (
<>
<section className="relative overflow-hidden">
<div className="pointer-events-none absolute inset-x-0 top-0 -z-10 h-[400px] bg-[radial-gradient(ellipse_900px_400px_at_50%_-10%,rgb(var(--nl-color-brand)/0.13),transparent_70%)]" />
<Section width="default" className="pt-24 pb-14 md:pt-32">
<Reveal>
<div className="mx-auto max-w-[820px] text-center">
<Eyebrow className="mb-5">Blog</Eyebrow>
<h1 className="text-balance text-[40px] font-semibold leading-[1.05] tracking-[-0.04em] md:text-[56px]">
Writing from the team
</h1>
<p className="mx-auto mt-5 max-w-[640px] text-[17px] leading-relaxed text-muted-foreground">
Tutorials, case studies, and announcements from the people building NativeLink.
</p>
</div>
</Reveal>
</Section>
</section>

<PostSection title="Case studies" cards={caseStudies.map(postToCard)} className="pb-16" />
<PostSection
title="Announcements"
cards={announcements.map(postToCard)}
className="border-t border-border/60 pt-16 pb-16"
/>
<PostSection
title="Blog"
cards={others.map(postToCard)}
className="border-t border-border/60 pt-16 pb-28"
/>
</>
);
}
142 changes: 54 additions & 88 deletions web/apps/web/app/resources/page.tsx
Original file line number Diff line number Diff line change
@@ -1,13 +1,6 @@
import {
Badge,
Eyebrow,
Reveal,
Section,
YouTubeEmbed,
cn,
} from "@nativelink/ui";

// (Badge is used in featured + announcement cards below.)
import { Badge, Eyebrow, Reveal, Section } from "@nativelink/ui";
import { getAllPosts } from "../../lib/posts";
import { type PostCardData, PostSection, postToCard } from "./post-sections";

export const metadata = { title: "Resources" };

Expand All @@ -21,18 +14,30 @@ const featuredPost = {
href: "https://reidkleckner.dev/posts/llvm-recc-nativelink/",
};

const announcements = [
{
tag: "Talk",
title: "Hermetic toolchain creation with LRE & Nix",
excerpt: "Aaron Mondal walks through Local Remote Execution — running fully hermetic Bazel builds on your own laptop, no Docker required.",
date: "April 18, 2026",
readingTime: "32 min video",
accent: "default",
},
];
// The BazelCon talk lives among the post cards rather than as an
// embedded video so the page stays light and the blog content leads.
const talkCard: PostCardData = {
key: "talk-hermetic-toolchains",
href: "https://www.youtube.com/watch?v=uokjTev8myk",
external: true,
tags: ["talk"],
meta: "BazelCon 2024 · 32 min video",
title: "Hermetic toolchain creation with LRE & Nix",
excerpt:
"Aaron Mondal walks through Local Remote Execution — running fully hermetic Bazel builds on your own laptop, no Docker required.",
cta: "Watch talk",
};

export default function ResourcesPage() {
const posts = getAllPosts();
const caseStudies = posts.filter((p) => p.tags.includes("case-studies"));
const announcements = posts.filter(
(p) => p.tags.includes("announcements") && !p.tags.includes("case-studies"),
);
const others = posts.filter(
(p) => !p.tags.includes("case-studies") && !p.tags.includes("announcements"),
);

return (
<>
{/* HERO */}
Expand All @@ -50,8 +55,8 @@ export default function ResourcesPage() {
.
</h1>
<p className="mx-auto mt-6 max-w-[640px] text-[17px] leading-relaxed text-muted-foreground md:text-lg">
Case studies, conference talks, and write-ups from the team building
NativeLink — plus highlights from the broader community.
Case studies, conference talks, and write-ups from the team building NativeLink —
plus highlights from the broader community.
</p>
</div>
</Reveal>
Expand Down Expand Up @@ -85,7 +90,10 @@ export default function ResourcesPage() {
</div>
<div className="mt-8 inline-flex items-center gap-1.5 font-mono text-sm text-brand">
Read the write-up{" "}
<span aria-hidden="true" className="transition-transform group-hover:translate-x-1">
<span
aria-hidden="true"
className="transition-transform group-hover:translate-x-1"
>
</span>
</div>
Expand Down Expand Up @@ -120,74 +128,32 @@ export default function ResourcesPage() {
</Reveal>
</Section>

{/* ANNOUNCEMENTS + VIDEO */}
<Section width="default" className="border-t border-border/60 bg-surface-elevated/40 py-24">
<Reveal>
<div className="mb-12 flex items-end justify-between">
<div>
<Eyebrow className="mb-4">Announcements</Eyebrow>
<h2 className="text-balance text-3xl font-semibold leading-[1.1] tracking-[-0.025em] md:text-4xl">
From the team
</h2>
</div>
<a href="#" className="hidden font-mono text-sm text-brand md:inline-flex">
All posts →
</a>
</div>
</Reveal>

<div className="grid gap-8 lg:grid-cols-[1.2fr_1fr]">
{/* FROM THE TEAM — full blog listing */}
<section className="border-t border-border/60 bg-surface-elevated/40">
<Section width="default" className="pt-24 pb-4">
<Reveal>
<div className="overflow-hidden rounded-2xl">
<YouTubeEmbed
id="uokjTev8myk"
title="Hermetic Toolchain Creation with Local Remote Execution (LRE) & Nix"
/>
</div>
<p className="mt-4 text-base leading-relaxed text-foreground">
<span className="font-mono text-xs uppercase tracking-widest text-muted">
Featured talk ·{" "}
</span>
Hermetic Toolchain Creation with Local Remote Execution & Nix
</p>
<p className="mt-1 text-sm text-muted-foreground">
Aaron Mondal, Trace Machina — 32 min
</p>
<h2 className="text-balance text-3xl font-semibold leading-[1.1] tracking-[-0.025em] md:text-4xl">
From the team
</h2>
</Reveal>
</Section>

<div className="flex flex-col gap-3">
{announcements.map((p, i) => (
<Reveal key={p.title} delay={i * 0.05}>
<a
href="#"
className={cn(
"group block rounded-2xl border p-6 transition-all hover:-translate-y-0.5",
p.accent === "brand"
? "border-brand/40 bg-brand-soft/30 hover:border-brand"
: "border-border bg-surface hover:border-border-strong",
)}
>
<div className="mb-3 flex items-center gap-2">
<Badge variant={p.accent === "brand" ? "brand" : "default"}>
{p.tag}
</Badge>
<span className="font-mono text-xs text-muted">
{p.date} · {p.readingTime}
</span>
</div>
<h3 className="text-lg font-semibold leading-tight tracking-tight text-foreground">
{p.title}
</h3>
<p className="mt-2 text-sm leading-relaxed text-muted-foreground">
{p.excerpt}
</p>
</a>
</Reveal>
))}
</div>
</div>
</Section>

<PostSection
title="Case studies"
cards={caseStudies.map(postToCard)}
className="pt-12 pb-16"
/>
<PostSection
title="Announcements"
cards={announcements.map(postToCard)}
className="border-t border-border/60 pt-16 pb-16"
/>
<PostSection
title="Blog"
cards={[...others.map(postToCard), talkCard]}
className="border-t border-border/60 pt-16 pb-28"
/>
</section>
</>
);
}
Loading
Loading