Skip to content

Commit 2ef5881

Browse files
Add Start launch video and polish blog headers (#1370)
* Add Cloudflare-hosted Start launch video and poster * Polish blog headers and Start launch video poster
1 parent fbdd516 commit 2ef5881

4 files changed

Lines changed: 175 additions & 36 deletions

File tree

‎content-collections.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@ const posts = defineCollection({
3434
rss: z.boolean().optional(),
3535
excerpt: z.string(),
3636
authors: z.string().array(),
37+
contributors: z.string().array().optional(),
3738
library: libraryListSchema.optional(),
3839
headerImage: z.string().optional(),
3940
headerVideo: z.string().optional(),

‎src/blog/tanstack-start-1-0.md‎

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,26 @@
11
---
2-
title: TanStack Start 1.0
2+
title: TanStack Start 1.0 is Stable
33
published: 2026-10-13
44
draft: true
55
excerpt: TanStack Start 1.0 is stable and ready for production.
66
authors:
77
- Tanner Linsley
8+
contributors:
9+
- Manuel Schiller
10+
- Florian Pellet
11+
- Christopher Horobin
812
library: start
13+
headerImage: https://media.tanstack.com/blog/tanstack-start-1-0/v11-title-poster.jpg
14+
headerVideo: https://media.tanstack.com/blog/tanstack-start-1-0/v11.mp4
915
---
1016

11-
# TanStack Start 1.0
12-
13-
_With contributions from [Manuel Schiller](https://github.com/schiller-manuel), [Florian Pellet](https://github.com/sheraff), and [Christopher Horobin](https://github.com/chorobin)._
17+
# TanStack Start 1.0 is Stable
1418

1519
TanStack Start 1.0 is finally stable. And we're really excited to officially call it ready for production.
1620

1721
I can't begin to say how proud I am of what we've built here and of the people who have built it. For a long time, I'd hear from people already running Start in production and have to remind them we were still in RC (that conversation happened a lot more than you'd think). I'm really glad we can finally drop that caveat.
1822

19-
We've had tons of people put Start into production through pretty much every stage of its development, from small projects and startups to entire enterprise teams and Fortune 500 companies. To our surprise, so many of these companies insisted on using it even when it was in beta. We'd explain where things were, and they'd basically tell us we're going to use it anyway.
23+
We've had tons of people put Start into production through pretty much every stage of its development, from small projects and startups to entire enterprise teams and Fortune 500 companies. To our surprise, so many of these companies insisted on using it even when it was in beta. We'd explain where things were, and they'd basically tell us, "we're going to use it anyway."
2024

2125
When enterprise teams are insisting on putting your beta software into production before it's even a release candidate, it's difficult to not feel the massive level of trust everybody is putting in you. So even though people have been successfully using Start in production for a long time, this is us making our formal commitment to the APIs we've built and the people building on them. You can now choose Start, learn it, ship something with it, and continue to build without wondering whether we're going to change the entire way your application works.
2226

‎src/routes/blog.$.tsx‎

Lines changed: 163 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { createFileRoute } from '@tanstack/react-router'
22
import { PostNotFound } from './blog'
33
import * as React from 'react'
4+
import { PlayIcon } from '@phosphor-icons/react'
45
import { MarkdownContent } from '~/components/markdown'
56
import { Card } from '~/components/Card'
67
import { LibrariesWidget } from '~/components/LibrariesWidget'
@@ -12,6 +13,7 @@ import { useMediaQuery } from '~/utils/useMediaQuery'
1213

1314
import { Toc } from '~/components/Toc'
1415
import { Breadcrumbs } from '~/components/Breadcrumbs'
16+
import { findMaintainerByAuthorName } from '~/utils/authors'
1517
import { CoverFallback } from '~/components/CoverFallback'
1618
import { fetchBlogPost } from '~/utils/blog.functions'
1719
import { parseSiteMarkdown } from '~/utils/markdown'
@@ -42,6 +44,9 @@ export const Route = createFileRoute('/blog/$')({
4244
function BlogPost() {
4345
const {
4446
content,
47+
authors,
48+
contributors,
49+
published,
4550
filePath,
4651
title,
4752
headerImage,
@@ -50,7 +55,13 @@ function BlogPost() {
5055
library,
5156
} = Route.useLoaderData()
5257
const { _splat: slug } = Route.useParams()
53-
const markdown = React.useMemo(() => parseSiteMarkdown(content), [content])
58+
const videoRef = React.useRef<HTMLVideoElement>(null)
59+
const [startedVideo, setStartedVideo] = React.useState<string>()
60+
const hasStartedVideo = startedVideo === headerVideo
61+
const markdown = React.useMemo(
62+
() => parseSiteMarkdown(content.replace(/^# [^\n]+\n*/m, '')),
63+
[content],
64+
)
5465
const headings = markdown.headings
5566

5667
const isTocVisible = headings.length > 1
@@ -146,23 +157,157 @@ function BlogPost() {
146157
].join(' ')}
147158
>
148159
<div className="flex overflow-auto flex-col w-full p-2 lg:p-4 xl:p-6 pt-0">
149-
{headerVideo ? (
150-
<video
151-
aria-label={title}
152-
controls
153-
playsInline
154-
preload="none"
155-
poster={headerImage}
156-
src={headerVideo}
157-
className="aspect-video w-full rounded-2xl mb-6"
160+
<header className="mb-8">
161+
<time
162+
dateTime={published}
163+
className="text-sm text-text-secondary"
158164
>
159-
<track
160-
kind="captions"
161-
src={headerVideoCaptions}
162-
srcLang="en"
163-
label="English"
164-
/>
165-
</video>
165+
{new Intl.DateTimeFormat('en-US', {
166+
weekday: 'long',
167+
month: 'long',
168+
day: 'numeric',
169+
year: 'numeric',
170+
timeZone: 'UTC',
171+
}).format(new Date(`${published}T00:00:00Z`))}
172+
</time>
173+
<h1 className="mt-5 mb-8 text-4xl md:text-5xl font-extrabold tracking-tight leading-tight">
174+
{title}
175+
</h1>
176+
<div className="flex flex-wrap items-start gap-x-10 gap-y-6 border-b border-gray-500/20 pb-7">
177+
<div>
178+
<div className="mb-3 text-sm text-text-secondary">
179+
Posted by
180+
</div>
181+
<div className="flex flex-wrap gap-6">
182+
{authors.map((name) => {
183+
const author = findMaintainerByAuthorName(name)
184+
return author ? (
185+
<a
186+
key={name}
187+
href={`https://github.com/${author.github}`}
188+
className="flex items-center gap-3 hover:underline"
189+
>
190+
<img
191+
src={author.avatar}
192+
alt=""
193+
width={44}
194+
height={44}
195+
className="size-11 rounded-full"
196+
/>
197+
<div>
198+
<div className="font-semibold">
199+
{name}
200+
</div>
201+
<div className="text-sm text-text-secondary">
202+
@{author.github}
203+
</div>
204+
</div>
205+
</a>
206+
) : (
207+
<span key={name} className="font-semibold">
208+
{name}
209+
</span>
210+
)
211+
})}
212+
</div>
213+
</div>
214+
{contributors.length ? (
215+
<div>
216+
<div className="mb-3 text-sm text-text-secondary">
217+
With contributions from
218+
</div>
219+
<div className="flex items-center gap-3">
220+
<div className="flex -space-x-3">
221+
{contributors.map((name) => {
222+
const person =
223+
findMaintainerByAuthorName(name)
224+
return person ? (
225+
<a
226+
key={name}
227+
href={`https://github.com/${person.github}`}
228+
aria-label={name}
229+
title={name}
230+
className="relative hover:z-10"
231+
>
232+
<img
233+
src={person.avatar}
234+
alt=""
235+
width={44}
236+
height={44}
237+
className="size-11 rounded-full ring-2 ring-background"
238+
/>
239+
</a>
240+
) : null
241+
})}
242+
</div>
243+
<div className="text-sm">
244+
{contributors.map((name, index) => {
245+
const person =
246+
findMaintainerByAuthorName(name)
247+
return (
248+
<React.Fragment key={name}>
249+
{index ? ', ' : ''}
250+
{person ? (
251+
<a
252+
href={`https://github.com/${person.github}`}
253+
className="hover:underline"
254+
>
255+
{name}
256+
</a>
257+
) : (
258+
name
259+
)}
260+
</React.Fragment>
261+
)
262+
})}
263+
</div>
264+
</div>
265+
</div>
266+
) : null}
267+
</div>
268+
</header>
269+
{headerVideo ? (
270+
<div className="relative mb-6 overflow-hidden rounded-2xl">
271+
<video
272+
ref={videoRef}
273+
aria-label={title}
274+
controls={hasStartedVideo}
275+
onPlay={() => setStartedVideo(headerVideo)}
276+
playsInline
277+
preload="none"
278+
poster={headerImage}
279+
src={headerVideo}
280+
className="aspect-video w-full"
281+
>
282+
<track
283+
kind="captions"
284+
src={headerVideoCaptions}
285+
srcLang="en"
286+
label="English"
287+
/>
288+
</video>
289+
{!hasStartedVideo ? (
290+
<button
291+
type="button"
292+
aria-label={`Play ${title} video`}
293+
onClick={() => {
294+
setStartedVideo(headerVideo)
295+
void videoRef.current
296+
?.play()
297+
.catch(() => setStartedVideo(undefined))
298+
}}
299+
className="group absolute inset-0 flex items-end justify-end p-4 md:p-6 bg-black/10 transition-colors hover:bg-black/20 focus-visible:outline-none focus-visible:ring-4 focus-visible:ring-inset focus-visible:ring-blue-500"
300+
>
301+
<span className="flex size-16 md:size-20 items-center justify-center rounded-full border border-white/30 bg-black/70 text-white shadow-xl backdrop-blur-sm transition-transform group-hover:scale-110 group-focus-visible:scale-110">
302+
<PlayIcon
303+
weight="fill"
304+
className="ml-1 size-7 md:size-9"
305+
aria-hidden="true"
306+
/>
307+
</span>
308+
</button>
309+
) : null}
310+
</div>
166311
) : null}
167312
{!headerImage && slug ? (
168313
<CoverFallback
@@ -172,11 +317,7 @@ function BlogPost() {
172317
/>
173318
) : null}
174319
<MarkdownContent
175-
title={
176-
headings.some((heading) => heading.level === 1)
177-
? ''
178-
: title
179-
}
320+
title=""
180321
markdown={markdown}
181322
preserveTabPanels
182323
repo={repo}

‎src/utils/blog.functions.ts‎

Lines changed: 2 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -13,8 +13,6 @@ import {
1313
} from '~/utils/blog'
1414
import {
1515
type BlogCardPost,
16-
formatAuthors,
17-
formatPublishedDate,
1816
getBlogAuthorIdentities,
1917
getBlogLibraries,
2018
isBlogPostUnpublished,
@@ -121,18 +119,13 @@ export const fetchBlogPost = createServerFn({ method: 'GET' })
121119
)
122120

123121
const authors = normalizeBlogAuthors(post.authors)
124-
const blogContent = `<small><em>by ${formatAuthors(authors)} on ${formatPublishedDate(
125-
post.published || '1970-01-01',
126-
)}.</em></small>
127-
128-
${post.content}`
129-
130122
const isUnpublished = isBlogPostUnpublished(post)
131123

132124
return {
133125
authorIdentities: getBlogAuthorIdentities(authors, allMaintainers),
134126
authors,
135-
content: blogContent,
127+
content: post.content,
128+
contributors: post.contributors ?? [],
136129
description: post.excerpt,
137130
filePath: `src/blog/${data}.md`,
138131
headerImage: post.headerImage,

0 commit comments

Comments
 (0)