Back to blog

Blocks vs Components: What Survives a Rebrand

A practical guide to ShipAny Next blocks and components: what to rewrite, what to keep, and how the split keeps SaaS customization clean.

May 28, 2026ShipAny TeamShipAny Team

Every SaaS template promises easy customization. The hard part is not changing the first headline. The hard part is changing the product, brand, pricing story, navigation, and landing page structure without breaking the reusable parts underneath.

ShipAny Next draws a strict line between two kinds of frontend files:

A file that reads translations is a block. A file that takes all content via props is a component.

That rule is small, but it decides what survives a rebrand.

The problem this split solves

A template usually starts as someone else's product story. It has their hero copy, their feature order, their screenshots, their pricing framing, and their default navigation. If those choices are mixed into the same files as durable UI primitives, every new project turns into a cleanup job.

You end up asking the same questions:

  • Is this file product copy or reusable UI?
  • Can I delete this section safely?
  • Why does a pricing card know about the landing page locale?
  • Why does a header component know which links this app uses?
  • Will changing this demo block break the dashboard or admin UI?

The blocks/components split keeps those answers boring.

Blocks are allowed to know the current product. Components are not.

Blocks are product wiring

Blocks live in src/blocks/. They are zero-config page sections such as <Header />, <Hero />, <Pricing />, <Blog />, and <Footer />.

A block may read translations, choose copy, assemble navigation links, pick icons, decide which pricing plans to show, and pass the final content into a durable component.

This is a real block-shaped pattern from the app:

import { m } from '@/paraglide/messages.js';
import { SiteHeader } from '@/components/site-header';

export function Header() {
  const navLinks = [
    { href: '/', label: m['landing.nav.generator']() },
    { href: '/#features', label: m['landing.nav.use_cases']() },
    { href: '/pricing', label: m['landing.nav.pricing']() },
    { href: '/blog', label: m['landing.nav.blog']() },
  ];

  return <SiteHeader navLinks={navLinks} />;
}

This file knows the landing page's labels. It knows which links matter for this product. That is fine because it is a block.

When you build a new SaaS on top of the engine, this is exactly the kind of file you should expect to rewrite.

Components are reusable UI

Components live in src/components/. They should not read product copy from translation files. They receive content as props and render it.

SiteHeader is a component. It does not decide what links exist. It only renders the navLinks it receives.

That makes the component useful across products:

  • a podcast tools site can pass generator, pricing, and blog links
  • a lead magnet tool can pass validation, examples, and billing links
  • an internal dashboard can use a different app shell entirely
  • a future rebrand can replace the whole header block without rewriting the header primitive

The same idea applies to PricingTable, BlogCard, AppSidebar, form fields, data tables, dialogs, buttons, and other primitives. They are not where your landing page story should live.

The file ownership rule

Use this rule before editing a page:

QuestionIf yes, it belongs in
Does it read m['...']() or other product text?src/blocks/
Does it choose section order, nav links, feature lists, or CTA copy?src/blocks/ or the route
Does it render a reusable shape from props?src/components/
Would it still make sense after a full rebrand?src/components/
Would you delete it when replacing the homepage?src/blocks/

The point is not folder purity. The point is making the next rewrite obvious.

What to change when starting a real project

When ShipAny Next is used as a base for a new product, the fastest clean path is:

  1. Keep src/components/* and src/components/ui/*.
  2. Rewrite the landing blocks in src/blocks/*.
  3. Rewrite the landing.* message keys in messages/en.json and messages/zh.json.
  4. Recompose the homepage in src/routes/index.tsx.
  5. Keep the SaaS engine, auth, payments, credits, admin pages, and reusable UI primitives intact.

That is much cheaper than editing a giant homepage component where business copy, layout primitives, pricing behavior, and navigation are all tangled together.

A concrete example: pricing

Pricing is the easiest place to see the split.

The pricing block may decide:

  • which plans exist
  • which features appear under each plan
  • which plan is featured
  • which payment providers are enabled
  • what the button text says
  • what happens after checkout succeeds

The pricing table component should only render a pricing table from structured props and call onCheckout when the user chooses a plan.

That keeps the billing story flexible while preserving the table UI. A new product can rewrite plan copy and product IDs without rebuilding tabs, badges, feature rows, responsive columns, or checkout button states.

A concrete example: blog cards

The blog section block can fetch or receive posts, choose how many to show on the homepage, format the locale-aware date, and decide the section heading.

The BlogCard component should only render:

  • title
  • description
  • image
  • date
  • author
  • link

If the homepage later changes from "latest articles" to "customer guides," the card does not need to know. The block changes the story; the component keeps rendering cards.

Common mistakes

The split breaks down when reusable components start reaching upward into app-specific concerns.

Avoid these patterns:

  • importing m from @/paraglide/messages.js inside a generic component
  • hardcoding homepage links inside SiteHeader
  • making PricingTable know product IDs directly
  • putting database or server-only module imports inside visual components
  • building one huge landing page component because it feels faster today

The first version may feel convenient. The second project pays the bill.

What this means for rebrands

During a rebrand, the disposable layer should absorb most of the work:

  • new headline
  • new navigation
  • new product categories
  • new feature order
  • new screenshots
  • new pricing names
  • new CTA language
  • new FAQ framing

The durable layer should barely move:

  • buttons
  • cards
  • dialogs
  • tables
  • sidebars
  • pricing table layout
  • form fields
  • authenticated app shell

That is the test. If a rebrand forces you to rewrite primitives, the primitives probably knew too much.

How page files stay small

Routes should mostly compose blocks.

For a marketing homepage, the route should read like a table of contents:

function HomePage() {
  const { posts } = Route.useLoaderData();

  return (
    <>
      <Header />
      <ShowNotesLandingPage variantKey="podcast" />
      <Blog posts={posts} />
      <Footer />
    </>
  );
}

The route decides composition. Blocks decide product content. Components render reusable UI.

That is the whole model.

The useful mental model

Think of blocks as adapters between your product story and your design system.

They translate:

  • message keys into labels
  • product strategy into page sections
  • pricing configuration into card props
  • route data into visible content
  • locale-aware copy into reusable UI

Components should not care where that content came from. They should only care whether the props are valid and how to render them accessibly.

Final rule

If you are customizing ShipAny Next, delete and rewrite blocks without guilt. They are supposed to be product-specific.

Be more careful with components. They are the parts you want to keep across launches.

That is the architectural split: blocks express this product; components survive the next one.

Related articles