Lab
The new Suits & Sandals CMS
We moved our site off Webflow and onto Payload CMS, installed right inside our Next.js app. The bigger change was the Content Hub: the facts, stories, quotes and files live as records, and pages point at them instead of keeping their own copies.
- Editor
- Miles Roxas
- Published
- 21 September 2026
- Built with
- Payload CMS, Next.js, TypeScript+5
- Read
- 22 MIN
Legacy
A portfolio CMS on Webflow
Our old site ran on Webflow, with the kind of CMS any small portfolio site has: collections for services, portfolios, categories, blog posts and episodes.
Portfolios carried the most fields: client and project overviews, a client review, a hero video and image, portfolio samples, related clients and a few switches for the homepage. Every one of them filled a slot in the same case study template.

Our old Webflow CMS: five collections.

The Webflow Portfolios collection: one field for every slot in the case study template.
The problem
One template for every case study
Webflow held every case study to one template. The deeper problem was that the page was the only copy of the story.
Webflow has no matrix fields or dynamic content areas. Each collection is limited to one structured template, so it is not possible to compose case study pages with a unique layout per entry.
We had already solved that limit in another project: Beacon, a hybrid Webflow app that unlocks composable content areas. Even with the core limitation removed, the new site had requirements Webflow would not meet, around true headless content management, digital asset management, developer experience, scalability and price.

Beacon, our hybrid Webflow app, choosing which props of a component become editable.
Payload CMS
Enter Payload
Rebuild on Payload CMS and split the site into two layers: a Content Hub that holds the work, and a website that presents it.
For most clients looking beyond Webflow we would usually recommend Sanity. For our own site we chose Payload CMS. We have used it since version 2 and supported the project for a long time, and it is extremely developer friendly.

Payload CMS.
Collections
A collection is just a TypeScript object
Collections are the content. Blocks are the layouts. One config file ties them together. It is all code: it lives in git, it goes through code review, and we own every piece of it.
What got us hooked on Payload: there is no schema builder, no clicking around adding fields one at a time. A collection is a TypeScript object. Give it a slug and some fields, and it is done.
From that one object Payload builds everything else: the database table, the admin screen the team edits in, a REST API, GraphQL and fully generated TypeScript types. Nobody writes a line of the types file.
So if a field is renamed and a component is not updated, the build fails before anything ships. All of it comes out of the box.
Description and steps
A flow from one collection config, a single TypeScript object. Payload builds the database table, the admin screen and the REST and GraphQL APIs from it, and generates the types file from the same object. Those generated types type check our components. The takeaway is that one object produces the database, the admin, the APIs and the types, so a renamed field breaks the build instead of the live site.
- One collection config (start or end)
- Table, admin, REST, GraphQL
- Types
- Our components (start or end)
Connections
- One collection config to Table, admin, REST, GraphQL: builds
- One collection config to Types: generates
- Types to Our components: type checks
Access control
Access control returns a query
Access control is also just a function. If you are on the team you see everything. If you are the public you only get what is published.
The return value is not a yes or a no, it is a query. Payload bakes it into the database call, so a draft cannot leak.
The setup
Two layers: a Content Hub and a website
The collections fall into two layers. The first is the Content Hub, the source of truth: clients, projects, case studies, lab projects and testimonials. It has no opinion about what a web page looks like. It holds the facts.
The second is the website: everything with a URL. A work page does not own its story. It points at a case study and decides how to present it.
Description and steps
A work page presents one Case Study Content record inside the Content Hub. The case study is about a project and quotes testimonials. The project is for a client, and the testimonials are said by that client. The project is also tagged from a shared vocabulary of capabilities, industries and platforms. The takeaway is that the page holds no story of its own: its one arrow ends in the hub, and everything it shows about the client, the quotes and the tags comes from records it points at.
- Work page (start or end)
- Case Study Content (Content Hub: the source of truth)
- Testimonials (Content Hub: the source of truth)
- Project (Content Hub: the source of truth)
- Client (Content Hub: the source of truth)
- Shared tags (Content Hub: the source of truth)
Connections
- Work page to Case Study Content: presents
- Case Study Content to Testimonials: quotes
- Case Study Content to Project: about
- Testimonials to Client: said by
- Project to Client: for
- Project to Shared tags: tagged
The same case study can feed the website today and a newsletter tomorrow, and the assistant on the site reads from it too. Write it once, use it everywhere. That is where headless actually pays off.
Editorial flow
Publishing rebuilds one page
Pages and every Content Hub record have drafts turned on. Work autosaves while you type, live preview shows it on the real site, and it can be published now or scheduled for later.
The site is essentially static, so it is fast. The moment someone publishes, a hook fires and tells Next.js to rebuild just that one page. No full redeploy, no waiting around.
Description and messages
A sequence between an editor, Payload, the Next.js site and a visitor. The editor edits and the draft autosaves, and Payload answers with a live preview of the real page. The editor publishes, now or on a schedule. An afterChange hook in Payload asks Next.js to revalidate that page’s path, and Next.js rebuilds just that one page. When a visitor requests the page, Next.js serves the static page with the published copy only. The takeaway is that publishing is one hook and one rebuilt page, not a redeploy, and a draft never reaches a visitor.
- Editor to Payload: Edit, the draft autosaves
- Payload to Editor: Live preview of the real page (reply)
- Editor to Payload: Publish now or schedule
- Payload to Next.js: afterChange: revalidate the path
- Next.js, to itself: Rebuild that page
- Visitor to Next.js: Request the page
- Next.js to Visitor: Static page, published copy (reply)
Blocks
Blocks fix the one-template problem
Blocks are the fix for the Webflow problem, where every case study was stuck in the same template, and they are built into Payload. A block is a mini collection: a slug and some fields.
A page has one field that holds a list of blocks and names the ones allowed there. Editors stack them in whatever order they want, so every page can be different without anyone building a template.
Rendering
One map from block type to component
On the frontend it is boring, in a good way. Every block is saved with its type, and a map points each type at a React component.
Description and steps
A left to right chain. An editor stacks blocks in the admin, and they are saved as a layout array where every item is tagged with its blockType. When the page loads, RenderBlocks reads that array and looks each blockType up in the blockComponents map, which returns the React component for it with typed props. The takeaway is that rendering is one lookup per block, with nothing page specific in between.
- Editor stacks blocks in the admin (start or end)
- layout array, each item tagged with a blockType
- RenderBlocks
- blockComponents map
- React component with typed props (start or end)
Connections
- Editor stacks blocks in the admin to layout array, each item tagged with a blockType: saved as
- layout array, each item tagged with a blockType to RenderBlocks: page loads
- RenderBlocks to blockComponents map: looks up
- blockComponents map to React component with typed props: returns
So every block is two files: a config that says what the editor fills in, and a component that says what it looks like. Because the types are generated, the component’s props come straight from the config.
Sections
Sections own the rhythm
On top of that we added a Section block: a block that holds other blocks. The Section owns the background, the theme and the spacing, and the blocks inside only worry about their own content. Editors build a page out of Sections, and the vertical rhythm stays consistent whatever goes inside.
Payload config
The whole system in one file
All of it is wired up in one config file. Read it top to bottom and you know the whole system: the database is Postgres, email goes through Resend and media lives on Cloudflare R2. Then come every collection, the globals for one-off content like the header and footer, the plugins and the background jobs. Swapping the database is one line. Moving the files somewhere else is one line.
One app
No separate service, no network hop
What really sold us: Payload is not a separate service we pay for and call over the network. It installs into our Next.js app. Same repository, same deploy, and the admin panel is just a route at /admin.
Description and steps
One Next.js app, deployed as one unit. Inside it, the frontend pages make a function call to Payload and its Local API, and the admin at /admin edits through the same Payload instance. Payload reaches outside the app only for Postgres over SQL, for files on Cloudflare R2 and for email through Resend. The takeaway is that the site reads its content with a function call, not an HTTP request to a separate CMS.
- Frontend pages (One Next.js app, one deploy)
- /admin (One Next.js app, one deploy)
- Payload and its Local API (One Next.js app, one deploy)
- Postgres (start or end)
- R2 and Resend (start or end)
Connections
- Frontend pages to Payload and its Local API: function call
- /admin to Payload and its Local API: edits
- Payload and its Local API to Postgres: SQL
- Payload and its Local API to R2 and Resend: files, email
So when a page needs its content there is no fetch, no API key and no network hop. It is a function call straight to the database, typed the whole way through: the page knows every block its layout could possibly hold.
The Content Hub
The reason for the move
Collections, blocks and a config file are plumbing. The reason for the whole move was the Content Hub: one place for the work to live, apart from any page.
A website is a menu. The Content Hub is the kitchen. In Webflow the menu was the kitchen: the only place the food existed was on the plate already served. To serve the same dish somewhere else, in a proposal, a pitch deck, an email or a slide, we copied it off the plate.
And copies drift. The case study on the site says one thing, the deck says a slightly better thing, the proposal has a number nobody can source anymore, and somewhere in a Slack thread there is a client quote nobody is sure we are allowed to use. That is not exactly a Webflow problem. It is what happens when the only copy of the truth is also the marketing layout.
Description and steps
A chain in two stages. Before: the case study page was the only copy of the story, and the deck, the proposal and the email were each copied from it and drifted on their own. Those scattered copies are replaced by one Case Study Content record in the hub, now the only copy, which is read by the work page, by Ask and by agents drafting decks and proposals. The takeaway is that nothing leaves the record anymore: every use points back to it.
- Case study page, the only copy (Before: the page owns the story)
- Deck, proposal, email, each drifting (Before: the page owns the story)
- Case Study Content, the only copy (Now: the hub owns the story)
- Work page, Ask, agent drafts (Now: the hub owns the story)
Connections
- Case study page, the only copy to Deck, proposal, email, each drifting: copied
- Deck, proposal, email, each drifting to Case Study Content, the only copy: replaced by
- Case Study Content, the only copy to Work page, Ask, agent drafts: read by
The difference comes down to one kind of arrow. When the page owns the story, copies leave home and start aging the moment they are pasted. When the hub owns the story, nothing leaves: every use points back to the record, and fixing a fact in one place fixes it everywhere it is cited.
The easiest way to show it is to follow one real engagement all the way through, so everything below is Vault Workforce Screening.
The records
Each record type has one job
Each record type in the hub has its own job. Clients are who: name, logo, site, industries.
Projects are what happened: the plain, factual record of an engagement. Who it was for, when it ran, whether it was a project or a retainer, what was in scope, what we delivered and what constrained us. No adjectives. There is a project for every engagement, even one that will never become a case study, because the file has to live somewhere.

The Projects list: one factual record per engagement.
Case Study Content is the story. It is the writing, and it is separate from the project on purpose. It does not need a web page to exist, so a story can be usable long before anyone designs a page for it.
Testimonials are quotes, with an approval state attached so we always know which ones we are cleared to use. Asset Libraries are the files, grouped by client and project instead of dumped in one big media bin.
Taxonomy
Tag a project once
Projects carry the tags, and the tags are a shared vocabulary rather than free text, so Brand Strategy means the same thing on every record. Vault is tagged with its capabilities and its industry, and that is the whole input.
The project fills in the client, industry and capabilities on the live page, and its tags power the related work at the bottom of a case study, which asks for pages that share a capability with this one. Nobody picks those by hand unless they want to. Tag a project once and it starts showing up in the right places on its own.

Vault's project, tagged from the shared vocabulary.
The story model
The story model is the whole trick
Every case study is written in six sections: context, challenge, strategy, approach, outcomes and learnings. That is not a template invented for the website. It is how work gets explained out loud, and it is stated in exactly one place in the code. Lab projects, this one included, use the same six.
Inside each section you can write one continuous overview, or break it into Story Beats: small, self-contained ideas. Each beat has a key that never changes, an internal label so it is findable in a dropdown, an optional public heading and the copy itself.
Vault’s Approach has one overview paragraph up top and the beats under it: Introduction, Connecting Solutions, Brand Foundation Elements, Web Design, Social Assets and more.

Vault's Approach: an overview, then Story Beats with stable keys.
It sounds fussy. It is not, and here is the payoff: copy written this way is channel-neutral. It does not know it is on a website. The same beat can be a paragraph next to a photo on the work page, a bullet in a deck, or the passage the assistant quotes when someone asks how we approach a redesign. Same words, no retyping, no drift.
Presentation
The page owns the layout, not the words
The work page is where presentation happens, and it starts by naming the story it is telling. One field holds the canonical Case Study Content record the page renders, and Vault’s work page points at Vault’s case study and nothing else. The page owns layout, order, art direction, media and SEO. It does not own the words.

Vault's work page names the one case study it presents.
The page is composed out of blocks, and the rows say what they are. Stacked, Challenge: Repositioning Vault. Split narrow, Challenge: Communications. Pair offset, Challenge: Impact and Value. Section, Approach. Every row is a layout choice on the left and a piece of the canonical story on the right. Whoever builds the page decides how it looks, not what it says.

Composition rows: a layout choice paired with a piece of the canonical story.
Wiring
A block points at a beat
Open one of those rows and the wiring is plain. The source is the challenge section, the story content is a Story Beat, and the beat is Repositioning Vault. The block renders that beat, whatever the copy says after the next edit in the hub. Underneath, the block picks its own media, because the image is a page decision.

A row pointed at the Repositioning Vault beat, with its own media below.
Choose custom instead and the copy is written right there on the page, for the times a page genuinely needs its own line. The resolution rule is written once and tested: whatever the block’s author typed wins, and the moment that field is empty the canonical copy falls through into its place. No stale duplicate sits underneath.
The hub protects the pages too. Rename or delete a beat that a page presents, and publishing the record is refused, with a message naming the page to update first.
Description and messages
A sequence between Vault’s work page, Vault’s case study record, an agent and an editor. The work page asks the record for the Repositioning Vault beat and gets the beat as it reads today. An agent reads the Web Design beat over MCP and gets copy for a deck or a proposal. The editor then tries to publish a rename of the Repositioning Vault beat, and the record refuses because a work page presents it. The takeaway is that the page, the agent and every other reader share one copy, and the page is protected from losing the passage it depends on.
- Work page to Vault record: Present the Repositioning Vault beat
- Vault record to Work page: The beat as it reads today (reply)
- Agent to Vault record: Read the Web Design beat over MCP
- Vault record to Agent: Copy for a deck or proposal (reply)
- Editor to Vault record: Publish a rename of that beat
- Vault record to Editor: Refused: a work page presents it (reply)
Summaries
Short versions, written once
The summaries live on the record too, at three lengths: one line for a card, short for a listing, medium for a hero. Vault’s one-liner is "Making workforce screening matter by showing the people doing high-stakes work." That exact sentence is available to a card, a listing, a search result, or a channel nobody has built yet.
Write the short versions once, with the story in front of you, instead of improvising a new one at 11pm inside a deck.

Vault's summaries, written once on the record.
Guardrails
Nothing goes out by accident
This is what makes the hub safe for material that is not public. Every record can hold internal notes, provenance and an internal claim log, all invisible outside the team, because access is set on the field, not only on the document.
Client quotes carry an approval state, and only approved-public quotes can ever render. Media carries a usage status, and anonymous visitors cannot query anything that is not public-approved.
Numbers get the strictest treatment, because numbers are what get us in trouble. A metric cannot be marked public without a label, a value, and either a source or a qualifier. If one is missing, the CMS refuses the publish.
Worth saying plainly: none of this is a check someone remembered to write in a component. Document rules are conditions Payload adds to the database query itself, and field rules apply on every read, so the unapproved version cannot be fetched, let alone rendered.
Description and steps
A flow from a hub record holding facts, internal notes, quotes, files and numbers. A decision asks who is asking. A signed in team member gets everything. A public request goes through the access rules: what is published and approved is shown on the site, and the rest, such as drafts, unapproved quotes, internal media and internal fields, stays private. The takeaway is that the guardrail is part of reading the data, so there is nothing for a component to forget.
- Hub record: facts, notes, quotes, files, numbers (start or end)
- Who is asking? (decision)
- Everything (start or end)
- Access rules
- Shown on the site (start or end)
- Stays private (start or end)
Connections
- Hub record: facts, notes, quotes, files, numbers to Who is asking?
- Who is asking? to Everything: team
- Who is asking? to Access rules: public
- Access rules to Shown on the site: approved
- Access rules to Stays private: the rest (optional or async)
Assets
Files carry their own paperwork
Files get the same treatment as the words. Every client has a library, organized by project, and every asset carries its own paperwork: the client, the project, what it documents, whether it is cleared for public use and which channels it is approved for.
When someone needs the Vault banner mockup for a proposal, they go straight to the project library and check what it is cleared for, instead of scrolling through a shared drive and asking around.

Media, filed by client and project.

Vault's library.

Every asset carries its own paperwork.
What it feeds
One record, four readers
Different readers now work from the same records: the website, the assistant on the site, AI crawlers and the agents we work with.
The website. Each work page presents a case study it does not own. The Vault page is a layout, and the words live in the hub.
Ask, the assistant on the site. When a hub record is published it is turned into embeddings alongside the page that presents it, so a visitor asking a plain question gets an answer grounded in our actual writing, with links. Every question is logged, redacted and tagged with what happened.

Every question asked of Ask, with its outcome.
The quietly most useful thing in the CMS is the outcome recorded on every question asked of Ask. A question marked no sources is someone asking for something we have not written yet. It is a content brief generated by real visitors instead of by us guessing.
AI crawlers. The same registry that decides which pages are public also generates llms.txt, so when someone’s assistant goes looking for us it reads structured text instead of scraping a layout.
Agents, through MCP. The CMS exposes itself as a tool server, so we can ask Claude to draft a case study from a project record, audit which stories are missing summaries, or find every engagement tagged for an industry. Authoring is drafts only, visitor data with contact details is read only, and publishing stays a human decision.
Why it was worth it
The story no longer waits for the page
We can finish telling a story without finishing the design of its page. The writing and the website do not have to move at the same speed anymore.
On Vault, the story blocks pull their copy straight from Story Beats. The page arranges the story without keeping its own copy of every passage, so when a passage changes in the hub and is published, the page picks it up. Nobody goes looking for the same paragraph inside a layout to change it again. And when the layout changes, the story stays put.
Not retyping a passage sounds like a small thing to describe. It was the thing we kept running into with the old setup, where every new use of the work meant pulling it apart and putting it back together somewhere else.
The project, the story, the short version, the quote and the files already exist as records, so an agent can read them through MCP and work from what is actually there.
The result is a working site where changing the presentation does not mean rewriting the content, and writing the content does not mean building a page. The next case study starts with the work itself, and the next place we want to use it has something to start from.
Learnings
What we took from it
The website used to be the thing we owned. Now it is the first thing we do with what we own.
About this project
- Status
- In progress
- Built with
- Payload CMSNext.jsTypeScriptPostgreSQLpgvectorCloudflare R2ResendModel Context Protocol

