Sabotage

Role
Front-end development
Design
CCT
Stack
Next.js, Sanity, Stripe
Built
August 2026, 24 days

CCT designed a new website for Sabotage, a London creative studio that works with Adidas, Nothing, Nike, Stone Island and Moncler, and brought me in to build it. I was the only developer, from CCT's Figma file to the last commit in 24 days.

The design is loud on purpose. My job was to make it hold exactly as drawn at every screen size, and to put every word and picture in a Sanity Studio inside the site, so the studio never needs a developer to change anything.

Impact

~3 seconds from Publish to the live site, 100+ fields the studio edits itself, 93 with help text written for them, 24 days from the first commit to the last, as the only developer.

The homepage strip

The strip is CCT's own Figma outlines, packed to within half a unit of the drawing.

The Sabotage homepage on a Studio Display on a long wooden table against a brick wall: the pink wordmark and the strip of 45° project blocks
The Sabotage homepage on an iPhone lying on red and orange light: the wordmark, the S-pattern and a 45° project block
lib/strip-geometry.ts
let x = 0;for (let i = 0; i < n; i++) {  const key = shapes[i];  const s = SHAPES[key];  blocks.push({ shape: key, left: x,    top: s.top, width: s.width,    height: s.height });  // Advance to the next block, wrapping so  // the loop seam is gapped correctly too.  x += ADVANCE[`${key}>${shapes[(i + 1) % n]}`]    ?? s.width;}const total = x;
One block of the homepage strip on its own, cut to its real notched 45° outline: a young man in a navy jacket holding a red phone

Project pages and the Smoking Area

Every size on the site is a unit of a Figma frame, with one breakpoint.

A Sabotage project page on a MacBook on a wooden desk against a brick wall: the rail with the big S, and the photographs running off to the right at one height
The Smoking Area held up on an iPhone in front of a plant and a brick wall: the wordmark, the flaming running man and stacked photographs
app/globals.css
/* homepage frame 1728 × 1117 */--u:  calc(100dvh / 1117);/* project frames */--pu: calc(100dvh / 1126);/* phone store, scrolls down */--su: calc(100vw / 402);/* legal page, scrolls down */--lu: calc(100vw / 1728);/* phone nav = desktop HUD × 343/449 */--mu: calc(0.76391 * var(--su));

The Studio

Press Publish and the live site changes in about three seconds.

Hands typing on a MacBook showing the Sanity Studio's Presentation view: the live Sabotage page beside the form that edits it
Two fields of a project in the Sabotage Studio, set on paper: Project title and Web address, each with the help text written for the studio
app/api/revalidate/route.ts
const { isValidSignature, body } =  await parseBody(request, secret); if (isValidSignature === false) {  return Response.json(    { revalidated: false }, { status: 401 });} // expire: 0, so the first visitor after a// publish waits for a fresh render, not the// old page. That visitor is almost always// the client.revalidateTag(SANITY_TAG, { expire: 0 });
The Projects list in the Sabotage Studio, set on paper: ADIDAS x GIGGS to MONCLER, each with its cover, photo count and published dot

The store

The browser says what, never how much.

The Sabotage store in a browser window: the giant white wordmark on blue, and t-shirts and black Stone Island books with prices on grey and blue panels
A Sabotage product page on an iPhone lying on an orange cord chair: the Los Pikes book on blue and a pink Add to cart bar
app/api/checkout/route.ts
const products = await client.fetch(  productsForCheckoutQuery, { ids },  { cache: "no-store" },);const byId = new Map(  products.map((p) => [p._id, p])); for (const item of wanted) {  const product = byId.get(item.productId);  lineItems.push({    quantity: item.qty,    price_data: {      currency: "gbp",      unit_amount: toPence(product.price),    },  });}
The Store list in the Sabotage Studio, set on paper: six products, each with its price and its spot in the store

Search and sharing

Every page draws its own share card from the same data as the page.

lib/seo.ts
const filled = template.replace(  /\{\{\s*([\w.-]+)\s*\}\}/g,  (_, token: string) =>    lookup.get(token.toLowerCase())      ?? "\u0000",); const tidied = filled  // 1. The sentence a trailing hole  // was the end of.  .replace(/[^.!?]*\u0000[ \t]*[.!?]+/g, "")
The Settings document in the Sabotage Studio, set on paper: the Site, Store and Search + sharing tabs over the site title and description fields

Looking back

The design was CCT's. The part of the build I'm proudest of is the part nobody sees: a studio that changes its own site without asking anyone.

Connections

What I owned

The whole front end, from CCT's Figma file: the homepage strip, project pages, store, Smoking Area, legal page and 404.

The Sanity Studio: every schema, field and line of help text, four custom inputs, and Presentation for editing on the page.

Publishing: the signed webhook that puts a change live in about three seconds.

The Stripe store and the newsletter.

Search and sharing: the sitemap, structured data, and a share card drawn for every page.

The handover: a guide to the Studio, field by field, and the system documented as built.

What I contributed

Responsive behaviour between CCT's desktop and phone frames, proposed to CCT, who owned those calls.

What I learned

Measure, don't eyeball. Every size on the site is a unit of a Figma frame, and that is why it holds at every screen.

Write the CMS for the people using it. Field names and help text in the page's own words do more than a training call.

A client site is finished when the client stops needing you.

Figma to handover

Kickoff: A call with Ray at CCT on 3 August: the scope, a week-by-week plan, and who owns what.

Figma: CCT's file measured to the unit: the frames, the block outlines and the colours taken straight from it.

Build: Next.js on Vercel, with the Sanity Studio inside the site and Stripe behind the store.

Rounds: Each round went to CCT as collages, screen recordings and a short changelog.

Handover: A guide to the Studio, field by field, and the site handed to Sabotage.

The homepage strip, Problem

CCT's homepage is a band of project blocks turned 45°, their corners touching the top and bottom of the screen and locked together with 10-unit gaps. Scaled naively, the corners clip or float as soon as the window changes shape.

The studio picks the projects and their covers, so the strip has to stay composed for any number of them.

The homepage strip, Decision

I read the five outlines straight from the Figma file's vector data and sized everything in units of screen height, so a block's corners sit on the screen's edges at every size with no JavaScript doing the layout.

Blocks are packed by a no-fit test along each scanline for exactly 10 units of clearance, from a table that covers every pair of shapes, and the loop's seam is gapped the same way.

By default project n takes the nth shape of the design's own sequence, so a fresh strip reproduces the Figma layout. The studio can switch a project to Best fit, which picks the outline closest to its cover by comparing aspect ratios in log space.

It loops forever, mounts only the blocks near the screen, and drags with the same feel on a phone: the gearing scales so a swipe covers the same share of the composition as on a desktop.

The homepage strip, Results

The packed strip lands within 0.55 units of the Figma layout, on a frame 1117 units tall.

Whatever the studio publishes lands in the strip in the right shape, with no layout shift as it loads.

The homepage strip, Tradeoffs

One clip path per block. Chrome failed to repaint an SVG clip under transforms, and clipping each piece on its own tripled the image work.

Project pages and the Smoking Area, Problem

CCT drew every page at a desktop size and a phone size. Media queries for every width in between would drift away from the drawings.

Project pages and the Smoking Area, Decision

Each kind of page has its own unit: the homepage is screen height over 1117, project pages over 1126, the phone store screen width over 402. Every size is written in those units.

The phone project frame matches the desktop one to four figures, so project pages need no breakpoint at all. The whole site has one, at 767px.

The phone nav is the desktop one scaled by exactly 343/449.

Project photographs sit at one height and take the width their own shape asks for, so nothing is cropped. Video plays silently among them while it is on screen.

The Smoking Area's photographs come from the Studio or from an are.na channel, whichever the studio picks.

Project pages and the Smoking Area, Results

Desktop and phone match CCT's frames with six units and one breakpoint for the whole site.

The Studio, Problem

A studio site goes stale the moment every edit needs its developer, and the people editing this one are designers.

Before the webhook, a change took up to a minute to show. The client refreshed, saw yesterday's strip, and fairly concluded publishing hadn't worked.

The Studio, Decision

The Studio lives at the site's own address: one deploy, one domain. Presentation opens the real page beside the form, and clicking anything on the page jumps to its field.

Eleven schema types and more than a hundred fields, 93 with help text in the words the page uses. Soft limits are warnings, not errors: a fifth tag gets "Four is the drawing."

Four custom inputs show the choices instead of naming them, like the mark picker, and the share preview draws the page's card under its empty field.

Pages that exist once are locked: they can't be duplicated, deleted or made again from the + menu.

Publish fires a signed webhook that clears the site's cache. Visitors read a CDN query under one site-wide tag, and every page refreshes within 60 seconds anyway if the webhook ever fails.

Editing previews hide invisible markers in text. I kept them out of four lookup fields, where they quietly swapped the mark the client chose for the default one.

The Studio, Results

Publish to live in about three seconds, and never more than a minute.

The site writes nothing to Sanity, and without its keys it runs on sample data with a plain message instead of an error.

The Studio, Tradeoffs

One tag for the whole site means a publish re-renders every page on its next visit. For a studio's site that is cheap, and no corner of it can go stale.

Vision, Sanity's query tool, is gone from the client's Studio: a tab that could only confuse.

The store, Problem

The studio sells a handful of things and changes them in the Studio. A second copy of the catalogue in Stripe would drift, and a basket that sends its own prices can be edited.

The store, Decision

Products and prices live only in the Studio. The browser sends ids, sizes and quantities; the server reads each price, name, image and stock back from Sanity, uncached, and builds the Stripe Checkout session from that.

Pounds typed in the Studio become pence in exactly one place.

The basket lives in the browser and is read through one store, so the first render and a second tab always agree with it.

CCT's store is a composition of six placed products, so I measured six slots off the Figma frames and gave each product a Position field. A seventh starts a second composition and the page drags sideways.

The store, Results

Turning payments on took one key. There are no Stripe products to create and no sync to maintain.

The store, Tradeoffs

No accounts and no order database: Stripe holds the orders. Right for a studio selling a few things, not for a big shop.

Search and sharing, Problem

Before launch the site had no sitemap, robots file, canonical links, structured data, 404 or descriptions, and a link posted anywhere showed one image for every page.

Search and sharing, Decision

Every route draws its own 1200×630 card with next/og, rebuilt from the site's parts to match CCT's card in Figma and read from the same Sanity data as the page, so a new cover changes the card.

Titles and descriptions come from templates in Settings, like {{title}} and {{year}}. A sentence whose value is empty drops out rather than printing braces.

The sitemap is built from the same queries as the pages, with structured data for the studio, each project and its credits, and each product and its price. One switch in Settings takes the whole site out of search.

About 37 KB came off the store, Smoking Area and project pages by keeping Sanity's browser client out of them, and the pattern artwork was redrawn at screen precision, from 238 KB to 149 KB.

Search and sharing, Results

All 19 projects and both products went out with search titles and descriptions, and seven pages with cards of their own.