Skip to main content
ASoc
Comparison

Storyblok vs Tina CMS: Who Holds the Content, and Who Pays for Links

Both ship visual editing, so the feature grid hides the real trade. Git-backed means you build the content graph yourself — measured, with the defect that proved it.

The ASoc Team8 min read

Storyblok is a hosted headless CMS: content lives in their Content Lake, editors work in a visual editor against a live preview, and you fetch it over an API. Tina is git-backed: content stays as Markdown or JSON in your repository, and the visual editor commits to a branch. Pick Storyblok when editors outnumber developers; pick Tina when content is prose that belongs beside the code.

Both ship visual editing, so the feature grid makes them look closer than they are. The real question is who holds the content — and the answer changes what you have to build yourself.

The comparison that matters

StoryblokTina CMS
Content lives inStoryblok's Content LakeYour git repository
FormatJSON, served over an APIMarkdown/MDX or JSON files
EditingVisual editor over a live previewVisual editor over a live preview, commits to git
PublishingImmediate, via the APIA commit — then your CI builds
VersioningTheir revision historyYour git history, same as code
Non-technical editorsStrong, matureWorkable; a CMS-hosted branch flow makes it safer
Roles and permissionsGranular, per spaceWhatever your git host and Tina Cloud give you
LocalisationFirst-class, per-fieldConvention you design (folders or fields)
Offline / local devNeeds the APIFiles are right there
Cost shapeSeats and plan tiersFree self-hosted; Tina Cloud for hosted auth
Vendor riskExport via APINone — the files are already yours
Relationships between itemsReference fields, queryableYou build the graph yourself

That last row is the one nobody puts in a comparison, and it is where a git-backed CMS quietly bills you.

Visual editing works the same way in both

Both render your actual site in an iframe and let editors click a region to edit it. The wiring differs:

// Storyblok — the bridge maps a rendered block back to its story field
import { storyblokEditable } from "@storyblok/react";

export function Feature({ blok }) {
  return (
    <div {...storyblokEditable(blok)}>
      <h3>{blok.headline}</h3>
      <p>{blok.body}</p>
    </div>
  );
}
// Tina — the same idea, but the source of truth is a file in your repo
import { useTina } from "tinacms/dist/react";

export default function Page(props) {
  const { data } = useTina(props);
  return <article data-tina-field={data.post.title}>{data.post.body}</article>;
}

If you evaluate them by clicking around a demo, they will feel equivalent. They are equivalent, for that. The divergence starts the moment there is more than one piece of content and they have to point at each other.

The bill a git-backed CMS sends later

A hosted CMS gives you a reference field. You pick related items from a dropdown, the API returns them, and the relationship is somebody else's data model to maintain. A git-backed CMS gives you files, which means the graph between them is yours to write — and yours to get wrong.

This site is git-backed in the strictest sense: 277 posts as MDX files, a typed registry in src/data/blog.ts, no CMS at all. Here is the defect that cost us, stated with the numbers.

Every post closes with a "Keep reading" rail of three related posts. The original rule was the obvious one — show the newest posts in the same cluster. It is what a reference field would have made unnecessary, and it is what a hosted CMS's "related content" widget would have let a human curate. Left to run, it produced this:

  • 12 of 178 posts received every rail link on the site.
  • Three of those twelve collected 111 inbound rail links each.
  • 166 posts had zero.

Google Search Console in September 2026 reported 35 posts sitting uncrawled, and the overlap with that zero-inbound set was not a coincidence. A page nothing links to is a page the crawler has little reason to schedule.

The fix was to stop asking "which posts are newest" and start asking "which posts have not had a turn" — a wrapping sliding window over the cluster, walked in slug order:

// src/lib/blog.ts — every post gets a turn, and gives exactly three
function ring(posts: BlogPostMeta[], slug: string, limit: number): BlogPostMeta[] {
  const pool = [...posts].sort((a, b) => a.slug.localeCompare(b.slug));
  const start = pool.findIndex((p) => p.slug === slug);
  const others = start === -1 ? pool.length : pool.length - 1;
  if (others <= 0) return [];
  return Array.from(
    { length: Math.min(limit, others) },
    (_, i) => pool[(start + 1 + i) % pool.length],
  ).filter((p) => p.slug !== slug);
}

Slug order rather than date order is deliberate, and we measured why. In date order a new post's inbound links come almost entirely from the batch published beside it — pages Google has not crawled either, so the links are worth little: only 20% of a new post's inbound rail links came from outside the uncrawled set, against 85% when the window walks slug order.

And because a rule you cannot see is a rule that regresses, it is now an assertion:

// src/data/__tests__/blog.test.ts
it("spreads inbound rail links evenly instead of concentrating them", () => { /* ... */ });

npm test fails if any post drops to zero inbound rail links or if one collects more than six. That test is the reference field, rebuilt by hand, after the outage it should have prevented.

None of this is an argument against Tina. It is the actual scope of "content lives in your repo": you own the relationships, the ordering, the link equity, and the tests that keep them honest. Budget for it, or take Storyblok's reference fields and spend the time elsewhere.

Pick Storyblok when editors outnumber developers

Storyblok's case is operational. Granular roles, a real approval workflow, per-field localisation, an asset CDN and revision history that a marketing lead can use without a git client. Publishing is immediate — no CI, no build, no waiting for a pipeline that someone broke this morning.

You pay in seats, in a runtime dependency your pages now carry, and in the fact that your content sits in an account. Export is possible and the API is good, but "possible" is doing work in that sentence.

Pick Tina when content is prose next to the code

Tina's case is that your content is already in the repository and you want to stop being the person who pastes other people's copy into it. The editor writes through a UI; the result is a commit; review, history, rollback and branch previews are the ones you already have.

Its limits are the ones above: you build the content graph, localisation is a convention you invent, and a contributor who cannot be given repository access needs Tina Cloud's hosted flow. For a team of two to ten technical people, none of that bites. For thirty editors in four languages, all of it does.

Sanity vs TinaCMS is this same trade with a different hosted backend, and MDX vs a headless CMS is the version where you skip the editor entirely.

Mistakes and how they show up

SymptomCauseFix
A handful of posts get all the internal linksThe "related" rule sorts by date or popularityWalk a wrapping window over a stable order; assert nobody hits zero
New posts stay uncrawled for weeksTheir only inbound links are from equally new pagesLink them from older, already-indexed posts in body prose
Tina edits land on main unreviewedThe editorial branch was never configuredPoint Tina at a branch and merge through a pull request
Storyblok preview shows stale contentThe bridge is loaded but the draft version isn't requestedFetch version: "draft" inside the preview, published elsewhere
Storyblok bill grows faster than trafficUncached API calls on dynamically rendered routesPrerender, and revalidate from a publish webhook
Editors cannot find where a field is definedGit-backed schema lives in code they don't readKeep the schema in one file and name the fields the way editors say them
Content changes don't appear after publishingStatic pages were built before the changeWebhook → revalidation route, for either CMS

Frequently asked questions

Does Tina work with the Next.js App Router? Yes. The content is files, so a Server Component reads and renders them directly; the editing layer is a client component mounted only on the routes that need it. The thing to check is that your preview route can read the draft branch.

Is Storyblok's visual editor better than Tina's? More mature, and more forgiving for people who do not think in components — nested blocks, drag-and-drop, and a preview that survives odd layouts. Tina's is good and closer to the code. If your editors are not technical, this gap is larger than a demo suggests.

Can I move off Tina later? Easily — the content is already Markdown in your repository, which is the best migration position available. Moving off Storyblok means an API export and a transform into whatever shape the next system wants. Neither is a crisis; only one is a weekend.

Which is cheaper? Tina, self-hosted, is free and the content costs nothing to store. Storyblok charges per seat and plan. The honest comparison adds the engineering you will do in Tina to build what Storyblok ships — the related-content graph above is a real example, and it took a defect, a rewrite and a test.

Templates in this post

ASoc Ignite — an AI-app marketing site that ships a blog — ASoc Iris, a computer-vision studio site with case studies, and ASoc Keystone, a mortgage-lender site, are typed Next.js and Tailwind source, so the article and listing components are ready for whichever editor you put in front of them.

Browse the full sets: Next.js landing page templates, Tailwind landing page templates.

Keep reading

Comparison9 min read

Strapi vs Headless WordPress: Two Servers, One Unasked Question

Both put a server and a database between your pages and your words. Measured against a build that serves 702 static pages in 38s with no content server at all.

Read more
Comparison8 min read

Strapi vs Payload CMS: Where the Content Model Lives

Strapi's GUI writes your schema; Payload's schema is TypeScript you review. That one difference decides the rest — measured against 391 tests and no CMS at all.

Read more