Skip to content
Closed
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
9 changes: 7 additions & 2 deletions docs/app/(home)/sections/Footer/Footer.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,8 @@
import mascotDarkSvgPaths from "@/imports/svg-mascot-dark";
import svgPaths from "@/imports/svg-urruvoh2be";
import mascotSvgPaths from "@/imports/svg-xeurqn3j1r";
import { useId } from "react";
import { useTheme } from "next-themes";
import { useEffect, useId, useState } from "react";
import styles from "./Footer.module.css";

// ---------------------------------------------------------------------------
Expand Down Expand Up @@ -193,7 +194,11 @@ function HandcraftedMascot({ isDark }: { isDark: boolean }) {
// ---------------------------------------------------------------------------

export function Footer() {
const isDark = false;
const { resolvedTheme } = useTheme();
// Resolve the theme only after mount so the server and first client render match
const [mounted, setMounted] = useState(false);
useEffect(() => setMounted(true), []);
const isDark = mounted && resolvedTheme === "dark";

return (
<footer className={styles.footer}>
Expand Down
78 changes: 55 additions & 23 deletions docs/app/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -1,33 +1,65 @@
import { blog } from "@/lib/source";
import Link from "next/link";
import { Footer } from "../(home)/sections/Footer/Footer";

function formatDate(date: string | Date) {
return new Date(date).toLocaleDateString("en-US", {
year: "numeric",
month: "short",
day: "numeric",
});
}

export default function BlogIndex() {
const posts = blog.getPages().sort((a, b) => {
const byDate = blog.getPages().sort((a, b) => {
return new Date(b.data.date).getTime() - new Date(a.data.date).getTime();
});
// Featured posts lead so their full-width cards sit on top of the grid
const posts = [
...byDate.filter((post) => post.data.featured),
...byDate.filter((post) => !post.data.featured),
];

return (
<main className="mx-auto w-full max-w-[800px] flex-1 px-4 py-16 md:px-8">
<h1 className="mb-12 text-4xl font-bold">Blog</h1>
<div className="flex flex-col divide-y divide-fd-border">
{posts.map((post) => (
<Link
key={post.url}
href={post.url}
className="group flex flex-col gap-1 py-6 no-underline"
>
<div className="flex items-baseline justify-between gap-4">
<h2 className="text-lg font-semibold transition-colors group-hover:text-fd-primary">
{post.data.title}
</h2>
<time className="shrink-0 text-sm text-fd-muted-foreground">
{new Date(post.data.date).toLocaleDateString()}
</time>
</div>
<p className="text-sm text-fd-muted-foreground">{post.data.description}</p>
</Link>
))}
</div>
</main>
<div className="flex min-h-screen flex-col bg-white [[data-theme=dark]_&]:bg-[#171717]">
<main className="mx-auto w-full max-w-[960px] flex-1 px-4 py-16 md:px-8">
<h1 className="mb-12 text-4xl font-bold text-[color:var(--openui-text-neutral-primary)]">
Blog
</h1>
<div className="grid grid-cols-1 gap-5 md:auto-rows-fr md:grid-cols-2">
{posts.map((post) => (
<Link
key={post.url}
href={post.url}
className={`group flex min-h-[12.5rem] flex-col rounded-[var(--openui-radius-4xl)] border border-[var(--openui-border-default)] bg-[var(--openui-foreground)] p-6 no-underline shadow-[var(--openui-shadow-m)] transition-[border-color,box-shadow] hover:border-[var(--openui-border-interactive-emphasis)] hover:shadow-[var(--openui-shadow-l)] ${
post.data.featured ? "md:col-span-2" : ""
}`}
>
<div className="flex items-start justify-between gap-3">
<h2 className="text-lg font-semibold leading-snug text-[color:var(--openui-text-neutral-primary)]">
{post.data.title}
</h2>
{post.data.featured && (
<span className="inline-flex h-[1.625rem] shrink-0 items-center rounded-lg bg-[color-mix(in_srgb,#7c3aed_14%,transparent)] px-2 text-xs font-semibold uppercase tracking-wide text-[#7c3aed] [[data-theme=dark]_&]:text-[#a78bfa]">
Featured
</span>
)}
</div>
<p className="mt-3 line-clamp-3 text-sm leading-relaxed text-[color:var(--openui-text-neutral-secondary)]">
{post.data.description}
</p>
<div className="mt-auto flex items-center gap-2 pt-5 text-sm text-[color:var(--openui-text-neutral-secondary)]">
<span className="font-medium text-[color:var(--openui-text-neutral-primary)]">
{post.data.author}
</span>
<span aria-hidden="true">·</span>
<time>{formatDate(post.data.date)}</time>
</div>
</Link>
))}
</div>
</main>
<Footer />
</div>
);
}
1 change: 1 addition & 0 deletions docs/content/blog/stop-making-ai-write-json.mdx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ title: "Stop making AI write JSON - Why we built OpenUI"
description: "We shipped a JSON-based Generative UI SDK. When we tried to add interactivity, JSON fell apart. A full programming language was worse. This is the story behind why we built OpenUI."
date: "2026-04-10"
author: "Thesys Engineering Team"
featured: true
---

JSON is a data format pretending to be a language. When you need a UI that can fetch data, manage state, and respond to user input, that distinction stops being theoretical.
Expand Down
1 change: 1 addition & 0 deletions docs/source.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,7 @@ export const blogPosts = defineCollections({
schema: pageSchema.extend({
author: z.string(),
date: z.string().date().or(z.date()),
featured: z.boolean().optional(),
}),
});

Expand Down
Loading