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
1 change: 1 addition & 0 deletions content-collections.ts
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ const posts = defineCollection({
rss: z.boolean().optional(),
excerpt: z.string(),
authors: z.string().array(),
contributors: z.string().array().optional(),
library: libraryListSchema.optional(),
headerImage: z.string().optional(),
headerVideo: z.string().optional(),
Expand Down
14 changes: 9 additions & 5 deletions src/blog/tanstack-start-1-0.md
Original file line number Diff line number Diff line change
@@ -1,22 +1,26 @@
---
title: TanStack Start 1.0
title: TanStack Start 1.0 is Stable
published: 2026-10-13
draft: true
excerpt: TanStack Start 1.0 is stable and ready for production.
authors:
- Tanner Linsley
contributors:
- Manuel Schiller
- Florian Pellet
- Christopher Horobin
library: start
headerImage: https://media.tanstack.com/blog/tanstack-start-1-0/v11-title-poster.jpg
headerVideo: https://media.tanstack.com/blog/tanstack-start-1-0/v11.mp4
---

# TanStack Start 1.0

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

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

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.

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.
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."

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.

Expand Down
185 changes: 163 additions & 22 deletions src/routes/blog.$.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { createFileRoute } from '@tanstack/react-router'
import { PostNotFound } from './blog'
import * as React from 'react'
import { PlayIcon } from '@phosphor-icons/react'
import { MarkdownContent } from '~/components/markdown'
import { Card } from '~/components/Card'
import { LibrariesWidget } from '~/components/LibrariesWidget'
Expand All @@ -12,6 +13,7 @@ import { useMediaQuery } from '~/utils/useMediaQuery'

import { Toc } from '~/components/Toc'
import { Breadcrumbs } from '~/components/Breadcrumbs'
import { findMaintainerByAuthorName } from '~/utils/authors'
import { CoverFallback } from '~/components/CoverFallback'
import { fetchBlogPost } from '~/utils/blog.functions'
import { parseSiteMarkdown } from '~/utils/markdown'
Expand Down Expand Up @@ -42,6 +44,9 @@ export const Route = createFileRoute('/blog/$')({
function BlogPost() {
const {
content,
authors,
contributors,
published,
filePath,
title,
headerImage,
Expand All @@ -50,7 +55,13 @@ function BlogPost() {
library,
} = Route.useLoaderData()
const { _splat: slug } = Route.useParams()
const markdown = React.useMemo(() => parseSiteMarkdown(content), [content])
const videoRef = React.useRef<HTMLVideoElement>(null)
const [startedVideo, setStartedVideo] = React.useState<string>()
const hasStartedVideo = startedVideo === headerVideo
const markdown = React.useMemo(
() => parseSiteMarkdown(content.replace(/^# [^\n]+\n*/m, '')),
[content],
)
const headings = markdown.headings

const isTocVisible = headings.length > 1
Expand Down Expand Up @@ -146,23 +157,157 @@ function BlogPost() {
].join(' ')}
>
<div className="flex overflow-auto flex-col w-full p-2 lg:p-4 xl:p-6 pt-0">
{headerVideo ? (
<video
aria-label={title}
controls
playsInline
preload="none"
poster={headerImage}
src={headerVideo}
className="aspect-video w-full rounded-2xl mb-6"
<header className="mb-8">
<time
dateTime={published}
className="text-sm text-text-secondary"
>
<track
kind="captions"
src={headerVideoCaptions}
srcLang="en"
label="English"
/>
</video>
{new Intl.DateTimeFormat('en-US', {
weekday: 'long',
month: 'long',
day: 'numeric',
year: 'numeric',
timeZone: 'UTC',
}).format(new Date(`${published}T00:00:00Z`))}
</time>
<h1 className="mt-5 mb-8 text-4xl md:text-5xl font-extrabold tracking-tight leading-tight">
{title}
</h1>
<div className="flex flex-wrap items-start gap-x-10 gap-y-6 border-b border-gray-500/20 pb-7">
<div>
<div className="mb-3 text-sm text-text-secondary">
Posted by
</div>
<div className="flex flex-wrap gap-6">
{authors.map((name) => {
const author = findMaintainerByAuthorName(name)
return author ? (
<a
key={name}
href={`https://github.com/${author.github}`}
className="flex items-center gap-3 hover:underline"
>
<img
src={author.avatar}
alt=""
width={44}
height={44}
className="size-11 rounded-full"
/>
<div>
<div className="font-semibold">
{name}
</div>
<div className="text-sm text-text-secondary">
@{author.github}
</div>
</div>
</a>
) : (
<span key={name} className="font-semibold">
{name}
</span>
)
})}
</div>
</div>
{contributors.length ? (
<div>
<div className="mb-3 text-sm text-text-secondary">
With contributions from
</div>
<div className="flex items-center gap-3">
<div className="flex -space-x-3">
{contributors.map((name) => {
const person =
findMaintainerByAuthorName(name)
return person ? (
<a
key={name}
href={`https://github.com/${person.github}`}
aria-label={name}
title={name}
className="relative hover:z-10"
>
<img
src={person.avatar}
alt=""
width={44}
height={44}
className="size-11 rounded-full ring-2 ring-background"
/>
</a>
) : null
})}
</div>
<div className="text-sm">
{contributors.map((name, index) => {
const person =
findMaintainerByAuthorName(name)
return (
<React.Fragment key={name}>
{index ? ', ' : ''}
{person ? (
<a
href={`https://github.com/${person.github}`}
className="hover:underline"
>
{name}
</a>
) : (
name
)}
</React.Fragment>
)
})}
</div>
</div>
</div>
) : null}
</div>
</header>
{headerVideo ? (
<div className="relative mb-6 overflow-hidden rounded-2xl">
<video
ref={videoRef}
aria-label={title}
controls={hasStartedVideo}
onPlay={() => setStartedVideo(headerVideo)}
playsInline
preload="none"
poster={headerImage}
src={headerVideo}
className="aspect-video w-full"
>
<track
kind="captions"
src={headerVideoCaptions}
srcLang="en"
label="English"
/>
</video>
{!hasStartedVideo ? (
<button
type="button"
aria-label={`Play ${title} video`}
onClick={() => {
setStartedVideo(headerVideo)
void videoRef.current
?.play()
.catch(() => setStartedVideo(undefined))
}}
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"
>
<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">
<PlayIcon
weight="fill"
className="ml-1 size-7 md:size-9"
aria-hidden="true"
/>
</span>
</button>
) : null}
</div>
) : null}
{!headerImage && slug ? (
<CoverFallback
Expand All @@ -172,11 +317,7 @@ function BlogPost() {
/>
) : null}
<MarkdownContent
title={
headings.some((heading) => heading.level === 1)
? ''
: title
}
title=""
markdown={markdown}
preserveTabPanels
repo={repo}
Expand Down
11 changes: 2 additions & 9 deletions src/utils/blog.functions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,6 @@ import {
} from '~/utils/blog'
import {
type BlogCardPost,
formatAuthors,
formatPublishedDate,
getBlogAuthorIdentities,
getBlogLibraries,
isBlogPostUnpublished,
Expand Down Expand Up @@ -121,18 +119,13 @@ export const fetchBlogPost = createServerFn({ method: 'GET' })
)

const authors = normalizeBlogAuthors(post.authors)
const blogContent = `<small><em>by ${formatAuthors(authors)} on ${formatPublishedDate(
post.published || '1970-01-01',
)}.</em></small>

${post.content}`

const isUnpublished = isBlogPostUnpublished(post)

return {
authorIdentities: getBlogAuthorIdentities(authors, allMaintainers),
authors,
content: blogContent,
content: post.content,
contributors: post.contributors ?? [],
description: post.excerpt,
filePath: `src/blog/${data}.md`,
headerImage: post.headerImage,
Expand Down
Loading