Skip to content
Engineering4 min read

How byteshades.com is built

How the ByteShades website is built: Next.js 16 with Cache Components, one interface for product data, small JavaScript islands and pages made for search.

· Co-Founder & CTO

A build log of the site's static and partially prerendered routes and Lighthouse scores of 99, 100, 100 and 100, beside the headline How our site is built

This website is the front door of the studio, so we held it to the same rules as our products: it should be fast, it should say plainly what we make, and it should not need rewriting when things change behind it. This post walks through how it is put together.

The stack

Layer Choice
Framework Next.js 16, App Router, with Cache Components switched on
UI React 19, server components by default
Language TypeScript in strict mode
Styling Tailwind CSS 4, mapped onto our brand kit's design tokens
Data validation zod

Every page is generated ahead of time. Nothing on the site depends on JavaScript to show its content: the products, prices, release notes and this post are all in the HTML the server sends.

One interface for product data

Today, each product is a JSON file in the repository. Later, a backend will publish them. We did not want pages to care which, so every read goes through one small interface:

ts
export interface ProductSource {
  list(opts?: { includeDrafts?: boolean }): Promise<Product[]>;
  get(slug: string): Promise<Product | null>;
  releases(limit?: number): Promise<Release[]>;
}

There are two implementations: one reads content/products/*.json, the other calls the API. An environment variable picks one. Both validate every product with the same zod schema, so a typo fails the build with the file name and the field instead of shipping a broken page. A test checks that both sources return the same products, and a build against a mock API produced the same HTML as the build from files.

Posts like this one work the same way: Markdown files with front matter today, the same fields from an API later.

Static pages that can still change

With Cache Components, data functions declare how long their result may be reused:

ts
export async function getProducts() {
  "use cache";
  cacheLife("hours");
  cacheTag("products");
  return getProductSource().list();
}

Pages built from this are static, refreshed at most hourly, and the products tag lets the future backend expire them on demand. After publishing, it calls one webhook and the next visitor sees the change, with no redeploy.

Small islands of JavaScript

The interactive parts are separate client components, each with one job:

  • the terminal in the hero, which answers commands such as status, open and whoami from real data;
  • the ⌘K command palette;
  • the product filter, which hides server-rendered cards and keeps the choice in the URL;
  • the dotted background, a canvas that reacts to the pointer and pauses when it is off-screen.

Each one renders something useful on the server first. If JavaScript is off, you still get the terminal's output, every product and a static dot grid. With reduced motion turned on, nothing animates.

The theme switch

Switching between light and dark uses the View Transitions API: the new theme grows out of the button you clicked as a circle.

ts
const transition = document.startViewTransition(applyTheme);
transition.ready.then(() =>
  document.documentElement.animate(
    { clipPath: [`circle(0px at ${x}px ${y}px)`, `circle(${radius}px at ${x}px ${y}px)`] },
    { duration: 560, pseudoElement: "::view-transition-new(root)" },
  ),
);

Browsers without the API, and visitors who prefer reduced motion, get an instant switch.

Written for search engines and assistants

People increasingly find software by asking an assistant, so the site describes itself in ways machines read well:

  • structured data (JSON-LD) for the organisation, its founders, each product and every FAQ;
  • /llms.txt and /llms-full.txt, plain-text summaries generated from the same data as the pages;
  • an RSS feed of releases and one for this blog;
  • a robots.txt that explicitly allows the main AI crawlers.

Because all of it is generated from one set of data, the page, the structured data and the summaries cannot drift apart.

What we could not hit

We set ourselves a budget of 90 KB of JavaScript on first load. React and the Next.js router alone come to roughly 139 KB compressed before any of our own code, so that number was never reachable with this framework. We kept our own islands small instead and measured the result: Lighthouse scores the homepage 99 for performance and 100 for accessibility, best practices and SEO.

We will write more about the individual products the same way: what they do, how they are built, and the trade-offs we made.

  • #nextjs
  • #react
  • #performance
  • #seo
  • #web

Written by Souvik Samanta · Co-Founder & CTO, ByteShades

Software developer who builds LLM-powered web, Android and backend apps, with a background in NLP.

LinkedIn

Keep reading

All posts