sanity-io

Activity

Stars and mentions · Last 30 days, Oct 8 – Nov 6

GitHub stars5
Mentions5
01234Oct 8Oct 22Nov 6
UTC days · repositories sanity-io owns9 repositories

Top repositories

sanity-io’s repositories ranked by pull request authors, as on trending.

Loading

Contributes to

Repositories outside sanity-io’s account where sanity-io opened or reviewed pull requests in the past 365 days.

Who noticed sanity-io

Everyone who starred or forked sanity-io’s repositories since the archive began in 2011.

Notable accounts

Ordered by the stars their own repositories have received.

Biggest fans

Accounts that starred the most of these repositories.

Breakout weeks

Each repository’s best week of new stars, with the release published around it.

Mentions

5posts
Nov 70 posts
  1. How I Built This Blog --- title: How I Built This Blog published: true date: 2026-11-05 16:10:00 UTC tags: canonical_url: zachscroggins.com --- My first blog post, back in 2021, was a demo. It dropped a React component into the middle of some Markdown to prove that MDX worked. It did work, and then I wrote almost nothing else. The setup was fun to build and tedious to write in. This is the rebuild. The goal is the same: writing a post should take less effort than putting it off. Nearly everything underneath has changed, and so has the way I built it. The stack - Next.js 16 with the App Router, Cache Components and the React Compiler - React 19 and TypeScript , with types generated from the content schema - Sanity for content, edited in a Studio that lives in its own repo - Tailwind CSS 4 and shadcn/ui , with shadcn's Typeset styles for post bodies - sugar-high for syntax highlighting, rendered on the server - Vercel for hosting, preview deployments and production Content lives outside the code The old blog stored MDX strings in Sanity and compiled them on the server with mdx-bundler. It worked, but the content was really code. A typo in a JSX tag broke the page, and the CMS had no way to warn me first. Posts are now Portable Text, Sanity's JSON format for rich text. A paragraph is data. A code block is data with a language and a filename. An image is data with required alt text. The site decides how each piece renders, and the Studio checks each piece before I can publish it. I wrote up the Sanity side in [Using Sanity to Manage Blog Content](/blog/using-sanity-to-manage-blog-content). The site and the Studio are two repos in one folder. The Studio generates TypeScript types straight into the site repo, so a schema change shows up as a type error in the code that reads it. The Studio deploys itself, too. Merging a pull request into the Studio repo runs a GitHub Actions workflow that installs the dependencies and runs the repo's own sanity deploy, so the hosted Studio always matches main: on: push: branches: [main] paths-ignore: [' **.md', 'functions/**', 'sanity.blueprint.ts'] jobs: deploy: runs-on: ubuntu-latest steps: - uses: actions/checkout@v7 - uses: actions/setup-node@v7 with: node-version: 24 cache: npm - run: npm ci - run: npx sanity deploy --yes env: SANITY_AUTH_TOKEN: ${{ secrets.SANITY_AUTH_TOKEN }} Cache Components: static by default, dynamic on purpose Next.js 16 changes the default. With cacheComponents on, data is only cached where I say so with 'use cache', and anything that reads the request (cookies, headers, search params) has to sit inside a Suspense boundary. That is stricter, and that's the point: every page is a static shell plus the parts I chose to leave dynamic. import type { NextConfig } from 'next'; import { sanity } from 'next-sanity/live/cache-life'; const nextConfig: NextConfig = { cacheComponents: true, cacheLife: { default: sanity }, partialPrefetching: true, reactCompiler: true, experimental: { turbopackRustReactCompiler: true, }, }; export default nextConfig; Blog posts and tag pages are prerendered at build time from generateStaticParams. Every Sanity fetch is tagged, so a publish can invalidate only the pages that used that document instead of rebuilding the site. The React Compiler handles memoization, so none of my own components need useMemo or useCallback. Two lessons from the strict model With partialPrefetching on, /blog/[slug] failed to prerender with an error about reading URL data outside of Suspense. The fix was a loading.tsx next to the page, which gives the route a boundary to stream into, and awaiting params in the page itself. The same rule applies to client hooks. The Blog link in the header is underlined on any blog page, which means reading the pathname with usePathname. For a slug that wasn't known at build time, that hook suspends during prerendering, so the link wraps itself in Suspense and uses the plain link as the fallback: export function NavLink({ activeClassName, ...props }: NavLinkProps) { return ( <Suspense fallback={<Link {...props} />}> <ActiveNavLink activeClassName={activeClassName} {...props} /> </Suspense> ); } function ActiveNavLink({ href, className, activeClassName, ...props }: NavLinkProps) { const pathname = usePathname(); const isCurrent = pathname === href; const isActive = isCurrent || pathname.startsWith(`${href}/`); return ( <Link href={href} aria-current={isCurrent ? 'page' : isActive ? 'true' : undefined} className={cn(className, isActive && activeClassName)} {...props} /> ); } Styling: Tailwind 4, shadcn/ui and Typeset The site uses shadcn/ui components and theme tokens throughout, so light and dark mode come from the same set of CSS variables. For post bodies I chose shadcn's Typeset over @tailwindcss/typography. Typeset is one CSS file that lives in my repo, and a few custom properties tune it: .typeset-blog { --typeset-size: 1.0625rem; --typeset-leading: 1.8; --typeset-flow: 1.25em; color: var(--color-muted-foreground); } Code blocks are highlighted on the server with sugar-high, using colors from the same theme. That has its own post: [Syntax Highlighting with sugar-high](/blog/syntax-highlighting-examples). SEO without a plugin Next.js covers all of this with file conventions: - app/sitemap.ts lists every post and tag from Sanity. - A route handler serves an RSS feed at /blog/rss.xml. - Each post and tag gets an Open Graph image from next/og, with my logo, the post type and the title. - generateMetadata builds titles, descriptions and canonical URLs from the same queries the pages use. Every metadata query runs with stega: false. Visual Editing hides invisible characters in strings so the Studio can map text back to fields, and those characters don't belong in a page title or a search result. Built with an AI pair programmer I built this with Claude Code, and the way we worked mattered as much as the stack. 1. Plan before code. We went back and forth on the content model, the tag pages and the preview setup before writing anything. Most of my input went into decisions, not syntax. 2. Give the agent current docs. Next.js 16 ships its docs inside node_modules, and the repo's AGENTS.md tells agents to read them before writing code, because a model's training data trails the framework. Sanity's Agent Toolkit adds skills for schema design and for wiring Sanity Live into Cache Components, and Sanity's MCP server lets the agent query and edit content directly. 3. An AGENTS.md in every folder. The parent folder, the site and the Studio each explain what they are and how they connect, so any agent picks up the context, not just Claude. 4. Check it in a real browser. A passing build isn't the bar. Every change gets checked in the running app: light and dark mode, desktop and phone widths, hover states included. 5. Keep notes outside the chat. The plan, decisions and a work log live in my Obsidian vault, so the next session starts where the last one stopped. Everything lands on a feature branch, Vercel builds a preview, and I review the pull request before it merges. Publishing without a rebuild When I publish in the Studio, a Sanity Function receives the sync tags for the content that changed and sends them to a route on this site. The route checks a shared secret and expires just those cache entries. Once the function reports back, any open page refreshes itself. A typo fix is live on the site a few seconds after I click Publish, with no build and no deploy. What's next The real goal now is simply writing more posts.

  2. Using Sanity to Manage Blog Content --- title: Using Sanity to Manage Blog Content published: true date: 2026-11-05 16:05:00 UTC tags: canonical_url: zachscroggins.com --- I first wrote about using Sanity for this blog in 2021. My goals then were simple: writing should be enjoyable, content should live apart from the code so a typo fix doesn't need a deploy, and everything should be typed. Those goals still hold. Almost everything about how I meet them has changed. Portable Text instead of MDX The old setup stored MDX in a Sanity Markdown field and compiled it on the server. Dropping a React component into the middle of a post felt like magic. It also meant my content was code. A missing closing tag broke the page, and nothing in the CMS could catch it before I published. This time the post body is Portable Text: an array of typed blocks stored as JSON. A code block in the Studio is saved as plain data: { "_type": "code", "language": "typescript", "filename": "sanity/live.ts", "code": "export const { SanityLive, sanityFetch } = defineLive({ client, strict: true });" } The site maps each block type to a React component. Here's what I gained and gave up: - No inline JSX. For a callout, I add a callout type to the schema and a component that renders it. That's a bit more work once, and none after that. - Validation before publishing. Images need alt text. Portfolio articles need their accomplishment line. The Studio won't publish a document that breaks a rule. - One source, many outputs. The same body renders on the page, in the RSS feed and anywhere else, without parsing or executing anything. One content model for the blog and the portfolio There are four document types: post, tag, company and role. A post has a kind field, either a normal post or a portfolio article. A portfolio article also has the one-sentence accomplishment shown on my home page, and the field only appears when it's needed: defineField({ name: 'accomplishment', type: 'text', rows: 3, group: 'portfolio', description: 'The line shown under the role on the home page. It links to this article.', hidden: ({document}) => !isPortfolioArticle(document), validation: (rule) => rule.custom((value, {document}) => isPortfolioArticle(document) && !value ? 'Portfolio articles need an accomplishment' : true, ), }), A company holds an ordered list of roles, and each role holds an ordered list of portfolio articles. The Portfolio section of the home page is one GROQ query that follows those references, and each accomplishment links to its full article. Reordering my work history happens with drag and drop in the Studio, not in a pull request. Tags are documents too, each with a description, links to the project's homepage, repo and docs, and an icon. Every tag gets its own page listing the posts that use it. Typed queries with TypeGen Queries are wrapped in defineQuery, and Sanity TypeGen reads both the schema and the queries to generate a type for each query's result. The Studio repo generates the types straight into the site repo: typegen: { path: '../my-website/{app,components,lib,sanity}/**/*.{ts,tsx}', schema: 'schema.json', generates: '../my-website/sanity/types.ts', }, After a schema change I run one script, and any component that reads a renamed or removed field fails to type-check: npm run typegen Fetching content with Cache Components The site runs Next.js 16 with Cache Components, and next-sanity's defineLive connects the two. Strict mode makes every fetch state its perspective (published or drafts) and whether to add stega, the hidden markers that power Visual Editing. That way nothing quietly falls back to the wrong one. export const { SanityLive, sanityFetch } = defineLive({ client, serverToken: token, browserToken: token, strict: true, }); // The app's one shared 'use cache' boundary. export const cachedSanity: StrictDefinedFetchType = async (options) => { 'use cache'; return sanityFetch(options); }; Pages check for draft mode once, then pass perspective and stega down to the components that fetch. Visitors get published content from the static shell. I get drafts with click-to-edit markers, from the same components. Live preview with the Presentation Tool The 2021 post called Sanity's preview the best of any headless CMS because changes showed up as I typed. That's still true, and the Presentation Tool goes further. The Studio shows the real site beside the editor, and clicking any text on the page jumps to the field it came from. Setting it up takes a few lines in the Studio config: presentationTool({ resolve, previewUrl: { initial: process.env.SANITY_STUDIO_PREVIEW_URL || 'http://localhost:3000', previewMode: {enable: '/api/draft-mode/enable', disable: '/api/draft-mode/disable'}, }, }), On the site, it takes one route handler that checks the Studio's preview secret and turns on draft mode: import { client } from '@/sanity/client'; import { defineEnableDraftMode } from 'next-sanity/draft-mode'; export const { GET } = defineEnableDraftMode({ client: client.withConfig({ token: process.env.SANITY_API_READ_TOKEN }), }); The resolve option maps routes to documents both ways. Opening /blog/some-post in the preview selects that post. Opening a post in the editor lists every page it appears on, including the home page for portfolio articles. Sanity and AI agents Sanity has leaned into AI tooling, and it paid off on this project. The Agent Toolkit gives coding agents skills for schema design, GROQ and framework integrations, and the MCP server lets them query and change content directly. While I built this site with Claude Code, it read the dataset to check its own work and seeded the tags and the first drafts of my portfolio articles with scripts. I then review and edit everything in the Studio. If you're starting today 1. Model the content, not the page. Tags, companies and roles as documents made both the blog and the portfolio easy to build. 2. Turn on strict in defineLive. Being explicit about perspective prevents a whole class of preview bugs. 3. Keep stega out of metadata, sitemaps and feeds with stega: false, and run stegaClean() on any Sanity string before comparing it. 4. Generate types, and regenerate them every time the schema changes. Next, I'll walk through how this blog's code blocks work in [Syntax Highlighting with sugar-high](/blog/syntax-highlighting-examples).

  3. @wesbos @sanity_io custom fields etc is basically all that sanity is — and with PTE's new Container API you can even have those embeds inline in the block editor. skillz github.com/sanity-io/agent-toolkit/blob/main/skil… sanity.io if you venture out on this - plz let me know of any gaps and i'll spend our tokens helping you out 🫡

    3 likes · 369 views
  4. @juristr aight, so this github.com/sanity-io/agent-toolkit/blob/main/skil… ...and not this sanity-io/sanity-template-nextjs-clean right?

    58 views
  5. No more `sanity typegen generate` commands you have to remember running (preferably with —watch) so types update after editing your query 😮‍💨 template-nextjs-personal-website#730

    1 like · 18 views

Achievements

Awards earned by the repositories sanity-io owns. Select an award to embed its badge.

No awards yet

Repositories earn awards by reaching the top three in a completed trending day or week.

A repository earns an award for finishing first, second or third in a completed trending day or week, ranked by new stars, new forks or pull request authors. Awards follow a repository through renames and transfers and count toward its current owner. Weeks run Monday to Sunday, UTC.