Skip to content

Commit 52f61e0

Browse files
authored
nteract 2.0 launch (#613)
* nteract 2.0 blog post with Monolith editorial design New blog layout inspired by Stitch "Monolith Editor" design system: - Space Grotesk headlines, JetBrains Mono metadata, Inter body - Tonal surface shifts, sharp corners, teal/blue/purple accents - Code blocks with black bg and teal left rail Interactive MDX components: - EnvPicker: uv/conda/pixi toggle with brand colors - Peekaboo: hover-to-reveal image with smooth animation - Kbd: platform-aware keyboard shortcuts (⌘S vs Ctrl+S) - BlogCTA: platform-aware download button + Star on GitHub Infrastructure: - shadcn setup with @nteract community registry - rehype-slug for heading anchor links - nteract-elements: selection-card, runtime-icons * Fix download button: API route for stable version, restore teal contrast - Add /api/stable-version route to proxy GitHub manifest (avoids CORS) - BlogCTA fetches version from local API, builds direct download URLs - Restore original --accent teal (#2DD4BF) for main page button contrast - Match blog CTA button to same teal with white text * Switch blog accent from teal to purple (#a993d1) Links, drop cap, code rail, inline code, blockquote border, and download CTA all use the secondary purple now. Matches the date and metadata color for a cohesive blog palette. * Redesign blog listing: hero post with video preview - Latest post is the full hero with text hierarchy cascade - Tags + "Read the post →" CTA inline - Video preview below header, clickable to post - Older posts section with compact cards below * Incorporate Anil's feedback: callout for live workspace, clarify vim line * Add LightboxImage component for click-to-expand images Click any LightboxImage to view full-size in a dark overlay. Applied to the side-by-side screenshots in the environments section. * Add peer diagram SVG for Local-first notebooks section Animated SVG showing Human, Agent, and Runtime as Automerge peers connected through a shared Notebook Document. Purple palette with pulsing rings and traveling sync dots. * Rewrite AI agents section with runt MCP setup instructions * Fix blog page test to match new hero layout * Polish blog post copy: environments, agents, ephemeral notebooks - Rewrite intro paragraph and Electron history - Add ephemeral notebooks section under local-first - Rework AI agents section: runt CLI + Claude Desktop extension paths - Minor copy tweaks throughout * Fix mobile: Peekaboo overflow and iOS overscroll background color * Peekaboo: show full image on mobile, peek effect only on desktop * Add launch callout on homepage, clean up layout hierarchy - Secondary "nteract 2.0 is here" text link below download button - Move Blog + GitHub links to footer - More breathing room between sections * Add OG image for nteract 2.0 blog post - Dynamic OG image route at /api/og?slug= with notebook mockup, syntax-highlighted code, and presence cursors - OG preview page at /blog/og-preview for all pages - Static OG image uploaded to R2, referenced via ogImage frontmatter - Added ogImage support to blog frontmatter schema * Add sandboxing description to What's next * Add agent-live video to AI agents section
1 parent 765747c commit 52f61e0

35 files changed

+4332
-290
lines changed

.voice-subs

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
# Pronunciation substitutions for voice TTS
2+
# Format: WORD=REPLACEMENT (one per line, longest keys match first)
3+
#
4+
# Text substitutions are applied before G2P (grapheme-to-phoneme).
5+
# Phoneme overrides bypass G2P entirely — wrap the value in /slashes/:
6+
# nteract=/ˈɛntəɹækt/
7+
8+
# nteract ecosystem
9+
nteract.io=enteract dot eye oh
10+
nteract=/ˈɛntəɹækt/
11+
12+
# File formats
13+
.ipynb=dot eye pie en bee
14+
.mdx=dot M D X
15+
16+
# Tools and projects
17+
JupyterLab=Jupiter Lab
18+
Jupyter=Jupiter
19+
Automerge=Auto merge
20+
runtimed=runtime dee
21+
IPython=eye Python
22+
23+
# Acronyms and initialisms
24+
MDX=M D X
25+
MCP=M C P
26+
REPL-style=repple style
27+
REPL=repple
28+
APIs=A P I s
29+
API=A P I
30+
GUI=gooey
31+
UI=U I
32+
CLI=C L I
33+
TTS=T T S
Lines changed: 197 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,197 @@
1+
import { ImageResponse } from "next/og";
2+
import { getPostBySlug, formatPostDate } from "@/lib/blog";
3+
4+
export const runtime = "nodejs";
5+
export const alt = "nteract blog post";
6+
export const size = { width: 1200, height: 630 };
7+
export const contentType = "image/png";
8+
9+
export default async function OGImage({
10+
params,
11+
}: {
12+
params: Promise<{ slug: string }>;
13+
}) {
14+
const { slug } = await params;
15+
const post = await getPostBySlug(slug);
16+
17+
if (!post) {
18+
return new ImageResponse(
19+
<div style={{ display: "flex", width: "100%", height: "100%", background: "#0e0e0e" }} />,
20+
{ ...size }
21+
);
22+
}
23+
24+
// Split description at first period for hierarchy
25+
const dot = post.description.indexOf(".");
26+
const lead = dot !== -1 ? post.description.slice(0, dot + 1) : post.description;
27+
const rest = dot !== -1 ? post.description.slice(dot + 1).trim() : "";
28+
29+
return new ImageResponse(
30+
(
31+
<div
32+
style={{
33+
height: "100%",
34+
width: "100%",
35+
display: "flex",
36+
flexDirection: "column",
37+
backgroundColor: "#0e0e0e",
38+
position: "relative",
39+
overflow: "hidden",
40+
padding: "60px 80px",
41+
}}
42+
>
43+
{/* Purple accent bar at top */}
44+
<div
45+
style={{
46+
position: "absolute",
47+
top: 0,
48+
left: 0,
49+
right: 0,
50+
height: "4px",
51+
display: "flex",
52+
background: "linear-gradient(to right, #a993d1, #a993d1 60%, transparent)",
53+
}}
54+
/>
55+
56+
{/* Decorative circles — peer diagram echo */}
57+
<div
58+
style={{
59+
position: "absolute",
60+
top: "80px",
61+
right: "80px",
62+
display: "flex",
63+
flexDirection: "column",
64+
alignItems: "center",
65+
gap: "12px",
66+
opacity: 0.15,
67+
}}
68+
>
69+
<svg width="200" height="200" viewBox="0 0 200 200">
70+
<circle cx="100" cy="100" r="80" stroke="#a993d1" strokeWidth="2" fill="none" />
71+
<circle cx="100" cy="100" r="60" stroke="#a993d1" strokeWidth="1" strokeDasharray="4 4" fill="none" />
72+
<rect x="80" y="80" width="40" height="40" rx="2" transform="rotate(45 100 100)" stroke="#a993d1" strokeWidth="2" fill="none" />
73+
</svg>
74+
</div>
75+
76+
{/* Date + nteract label */}
77+
<div
78+
style={{
79+
display: "flex",
80+
alignItems: "center",
81+
gap: "16px",
82+
marginBottom: "32px",
83+
}}
84+
>
85+
<span
86+
style={{
87+
fontSize: "16px",
88+
color: "#a993d1",
89+
fontFamily: "monospace",
90+
letterSpacing: "4px",
91+
textTransform: "uppercase",
92+
}}
93+
>
94+
{formatPostDate(post.date)}
95+
</span>
96+
<div
97+
style={{
98+
height: "1px",
99+
flex: 1,
100+
maxWidth: "120px",
101+
background: "rgba(169, 147, 209, 0.3)",
102+
display: "flex",
103+
}}
104+
/>
105+
<span
106+
style={{
107+
fontSize: "16px",
108+
color: "#484848",
109+
fontFamily: "monospace",
110+
letterSpacing: "4px",
111+
textTransform: "uppercase",
112+
}}
113+
>
114+
nteract
115+
</span>
116+
</div>
117+
118+
{/* Title */}
119+
<div
120+
style={{
121+
display: "flex",
122+
fontSize: "96px",
123+
fontWeight: 700,
124+
color: "#e5e5e5",
125+
lineHeight: 0.9,
126+
letterSpacing: "-4px",
127+
marginBottom: "28px",
128+
}}
129+
>
130+
{post.title}
131+
</div>
132+
133+
{/* Lead description */}
134+
<div
135+
style={{
136+
display: "flex",
137+
fontSize: "32px",
138+
fontWeight: 600,
139+
color: "rgba(229, 229, 229, 0.7)",
140+
lineHeight: 1.2,
141+
marginBottom: "12px",
142+
maxWidth: "800px",
143+
}}
144+
>
145+
{lead}
146+
</div>
147+
148+
{/* Rest of description — monospace tagline */}
149+
{rest && (
150+
<div
151+
style={{
152+
display: "flex",
153+
fontSize: "14px",
154+
fontFamily: "monospace",
155+
color: "#ababab",
156+
letterSpacing: "3px",
157+
textTransform: "uppercase",
158+
maxWidth: "700px",
159+
}}
160+
>
161+
{rest}
162+
</div>
163+
)}
164+
165+
{/* Tags at bottom */}
166+
<div
167+
style={{
168+
display: "flex",
169+
position: "absolute",
170+
bottom: "60px",
171+
left: "80px",
172+
gap: "12px",
173+
}}
174+
>
175+
{post.tags.map((tag) => (
176+
<span
177+
key={tag}
178+
style={{
179+
display: "flex",
180+
padding: "6px 16px",
181+
fontSize: "12px",
182+
fontFamily: "monospace",
183+
color: "#ababab",
184+
letterSpacing: "2px",
185+
textTransform: "uppercase",
186+
backgroundColor: "#1f1f1f",
187+
}}
188+
>
189+
{tag}
190+
</span>
191+
))}
192+
</div>
193+
</div>
194+
),
195+
{ ...size }
196+
);
197+
}

app/(blog)/blog/[slug]/page.tsx

Lines changed: 65 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { notFound } from "next/navigation";
44

55
import { BlogTagList } from "@/components/blog/tag-list";
66
import { Prose } from "@/components/prose";
7-
import { Container } from "@/components/site-shell";
87
import { formatPostDate, getAllSlugs, getPostBySlug } from "@/lib/blog";
98
import { absoluteUrl } from "@/lib/site";
109

@@ -46,13 +45,13 @@ export async function generateMetadata({
4645
type: "article",
4746
publishedTime: post.date,
4847
tags: post.tags,
49-
images: [absoluteUrl("/opengraph-image")],
48+
images: [post.ogImage ?? absoluteUrl("/opengraph-image")],
5049
},
5150
twitter: {
5251
card: "summary_large_image",
5352
title: post.title,
5453
description: post.description,
55-
images: [absoluteUrl("/opengraph-image")],
54+
images: [post.ogImage ?? absoluteUrl("/opengraph-image")],
5655
},
5756
};
5857
}
@@ -68,41 +67,79 @@ export default async function BlogPostPage({ params }: BlogPostPageProps) {
6867
const { default: Content } = await import(`@/content/blog/${slug}.mdx`);
6968

7069
return (
71-
<Container className="py-12 sm:py-16">
72-
<article className="mx-auto max-w-3xl">
73-
<Link
74-
href="/blog"
75-
className="inline-flex items-center gap-2 text-sm text-neutral-500 transition-colors hover:text-purple-400"
76-
>
77-
<span aria-hidden="true"></span>
78-
Back to blog
79-
</Link>
80-
81-
<header className="mt-8 border-b border-white/10 pb-8">
82-
<div className="flex flex-wrap items-center gap-3 text-sm text-neutral-500">
83-
<time dateTime={post.date}>{formatPostDate(post.date)}</time>
70+
<div className="px-6 pb-24 pt-12 md:px-12">
71+
<article className="mx-auto max-w-4xl">
72+
{/* Article Header */}
73+
<header className="mb-12">
74+
<div className="mb-6 flex items-center gap-4">
75+
<time
76+
dateTime={post.date}
77+
className="font-mono text-xs uppercase tracking-widest text-secondary"
78+
>
79+
{formatPostDate(post.date)}
80+
</time>
81+
<div className="h-px flex-grow bg-outline-variant/20" />
8482
</div>
85-
<h1 className="mt-4 text-4xl font-semibold tracking-tight text-white sm:text-5xl">
83+
84+
<h1 className="mb-6 font-headline text-6xl font-bold leading-[0.9] tracking-tighter text-on-surface md:text-8xl">
8685
{post.title}
8786
</h1>
88-
<p className="mt-4 max-w-2xl text-lg leading-8 text-neutral-400">
89-
{post.description}
90-
</p>
91-
<BlogTagList tags={post.tags} className="mt-6" />
87+
88+
{(() => {
89+
const dot = post.description.indexOf(".");
90+
if (dot === -1) {
91+
return (
92+
<p className="mb-6 max-w-2xl text-xl leading-snug text-on-surface/60">
93+
{post.description}
94+
</p>
95+
);
96+
}
97+
const lead = post.description.slice(0, dot + 1);
98+
const rest = post.description.slice(dot + 1).trim();
99+
return (
100+
<div className="mb-6 max-w-2xl space-y-2">
101+
<p className="font-headline text-2xl font-semibold tracking-tight text-on-surface/80 md:text-3xl">
102+
{lead}
103+
</p>
104+
{rest && (
105+
<p className="font-mono text-xs uppercase tracking-[0.25em] text-on-surface-variant">
106+
{rest}
107+
</p>
108+
)}
109+
</div>
110+
);
111+
})()}
112+
113+
<div className="flex flex-wrap items-center gap-6">
114+
<BlogTagList tags={post.tags} />
115+
<Link
116+
href="/blog"
117+
className="inline-flex items-center gap-2 font-mono text-[11px] uppercase tracking-widest text-outline-variant transition-colors hover:text-on-surface"
118+
>
119+
<span aria-hidden="true"></span>
120+
All posts
121+
</Link>
122+
</div>
92123
</header>
93124

125+
{/* Cover image */}
94126
{post.coverImage ? (
95-
<img
96-
alt={post.title}
97-
className="mt-10 rounded-3xl border border-white/10"
98-
src={post.coverImage}
99-
/>
127+
<section className="mb-16">
128+
<div className="aspect-video w-full overflow-hidden bg-surface-container-low">
129+
<img
130+
alt={post.title}
131+
className="h-full w-full object-cover"
132+
src={post.coverImage}
133+
/>
134+
</div>
135+
</section>
100136
) : null}
101137

102-
<Prose className="mt-10 prose-invert">
138+
{/* Body Prose */}
139+
<Prose className="prose-invert mx-auto max-w-2xl">
103140
<Content />
104141
</Prose>
105142
</article>
106-
</Container>
143+
</div>
107144
);
108145
}

0 commit comments

Comments
 (0)