Calm Content Pipelines: CMS as a Source, CDN as a Runtime

Modern frontend systems often need content that changes independently from application code.

Sometimes it is page content.
Sometimes it is translations.
Sometimes it is search metadata.
Sometimes it is configuration for a micro frontend.

The obvious solution is to call a CMS API directly from the client or from the application runtime.

But there is another approach.

A calmer one.

Use the CMS as a source of truth, but not as a runtime dependency.

Generate simple JSON artifacts from the CMS and publish them to the same CDN where the application, static pages, or micro frontend assets already live.

The client does not need to know about the CMS.
The page does not need to call Strapi directly.
The micro frontend does not need extra backend coupling.

It just loads a JSON file from a predictable URL.

That is calm architecture.

The idea

Imagine we have a CMS.

For example, Strapi.

We use it to manage content that should be editable outside of the application codebase.

Two practical examples:

  1. translations for a micro frontend
  2. content index data for MiniSearch

In both cases, the client does not really need the full CMS API.

It needs a small, prepared, predictable JSON file.

For translations, that might be:

/mfe/profile-card/i18n/en.json
/mfe/profile-card/i18n/uk.json
/mfe/profile-card/i18n/pl.json

For search, that might be:

/search/articles.json
/search/tags.json
/search/related.json

These files can be generated after CMS changes and uploaded to S3.

CloudFront serves them as static assets.

The application consumes them like any other frontend asset.

Why not call the CMS directly?

Calling a CMS directly from the client sounds simple at first.

But over time it creates hidden coupling.

The frontend becomes aware of CMS structure.
The client depends on CMS availability.
Search payloads may become too large or too raw.
Translations may need transformation before use.
The CMS API becomes part of the public runtime contract.
Caching becomes harder to reason about.

For some systems, that is acceptable.

But often the client does not need a live CMS connection.

It only needs the result.

A small JSON file is enough.

Calm architecture principle

The important boundary is this:

CMS is an editing system.
CDN is the runtime delivery system.
JSON is the contract.

That separation keeps the system easy to understand.

The CMS can change internally.
The generator can transform data.
The CDN can serve stable files.
The client can stay simple.

Each part has a clear responsibility.

Basic flow

The simplest flow looks like this:

CMS change
  ↓
CMS webhook
  ↓
API Gateway
  ↓
Lambda
  ↓
Generate JSON
  ↓
Upload to S3
  ↓
Serve through CloudFront
  ↓
Client loads JSON

For small tasks, Lambda can do everything.

For example:

  • fetch updated translations from Strapi
  • normalize the structure
  • generate locale JSON files
  • upload them to S3

For larger tasks, Lambda can start a Fargate task.

For example:

  • fetch all articles
  • parse Markdown or rich content
  • generate MiniSearch documents
  • calculate related articles
  • build multiple search indexes
  • upload the generated files to S3

The important part is that the runtime result stays simple.

Static JSON on a CDN.

Example 1: translations for MFE

Imagine a profile card micro frontend.

The JavaScript bundle is deployed to a CDN:

/mfe/profile-card/profile-card.js

Translations are also deployed as generated JSON:

/mfe/profile-card/i18n/en.json
/mfe/profile-card/i18n/uk.json

The micro frontend can load translations by locale:

const loadMessages = async (locale: string) => {
  const response = await fetch(`/mfe/profile-card/i18n/${locale}.json`);

  if (!response.ok) {
    throw new Error(`Failed to load locale: ${locale}`);
  }

  return response.json();
};

The MFE does not care whether translations were edited in Strapi, Markdown, Git, Google Sheets, or somewhere else.

Its contract is simple:

locale → JSON file

That is the boundary.

Example 2: MiniSearch index for static content

The same idea works for search.

Instead of making the browser fetch raw CMS content and build everything at runtime, we can generate a prepared index file.

For example:

/search/articles.json

The file can contain only the fields needed by the client:

[
  {
    "id": "calm-component-structure",
    "title": "Calm Component Structure",
    "description": "A simple component structure with clear boundaries.",
    "url": "/blog/calm-component-structure/",
    "tags": ["architecture", "components", "storybook"]
  }
]

The client search widget can load this file and initialize MiniSearch locally.

const response = await fetch('/search/articles.json');
const articles = await response.json();

const search = new MiniSearch({
  fields: ['title', 'description', 'tags'],
  storeFields: [
    'title',
    'description',
    'url',
    'tags',
  ],
});

search.addAll(articles);

Again, the frontend does not need to know about Strapi.

It only knows about the generated search document contract.

Optional queue layer

For small systems, CMS webhook → Lambda may be enough.

But CMS changes often come in bursts.

Someone edits five articles.
Someone updates ten translations.
Someone publishes several entries at once.

If every CMS event immediately starts generation, we may do unnecessary work.

A calmer version adds SQS between the webhook and the worker.

CMS change
  ↓
CMS webhook
  ↓
API Gateway
  ↓
SQS
  ↓
Worker Lambda or Fargate task
  ↓
Generate JSON
  ↓
Upload to S3
  ↓
CloudFront

SQS gives us a buffer.

It allows the system to aggregate events by time, quantity, or type.

For example:

  • wait a short period before rebuilding
  • process several CMS updates together
  • avoid running the same generator many times
  • retry failed jobs safely
  • move broken events to a dead-letter queue

This makes the pipeline more predictable.

Lambda or Fargate?

The decision can stay practical.

Use Lambda when the job is small and fast.

Good examples:

  • generate translation JSON
  • update one small config file
  • transform a small CMS response
  • invalidate a few CDN paths

Use Fargate when the job is heavier.

Good examples:

  • generate a full search index
  • process many articles
  • build related article suggestions
  • run larger Node.js scripts
  • use more memory or longer execution time

The architecture does not need to change dramatically.

Lambda can either perform the work directly or start a Fargate task.

The contract remains the same:

input: CMS event
output: generated static files

The safety fuse pattern

There is an important operational benefit in this architecture.

The generator is not on the user-facing runtime path.

If Lambda cannot reach Strapi, users still get the old JSON files.
If Fargate fails during generation, users still get the old JSON files.
If the CMS is temporarily unavailable, users still get the old JSON files.
If a webhook event is broken, users still get the old JSON files.

The system does not become unavailable just because fresh content could not be generated.

That is a useful safety fuse.

CMS or generator failure
  ↓
No new JSON is published
  ↓
Old JSON remains on S3
  ↓
CloudFront continues to serve the previous version
  ↓
Client keeps working

The failure is contained inside the content pipeline.

It does not automatically spread into the client runtime.

This is very different from a direct runtime CMS dependency:

Client
  ↓
CMS API
  ↓
Failure
  ↓
Broken user experience

With generated CDN artifacts, the failure path is calmer:

Client
  ↓
CloudFront
  ↓
Previous valid JSON
  ↓
Working user experience

The content may be stale.

But the system is still available.

And in many cases, stale content is much better than broken content.

Publish only valid artifacts

The safety fuse works best when publication is atomic.

The generator should not partially overwrite production files while it is still building them.

A safer flow is:

Generate files in a temporary location
  ↓
Validate generated files
  ↓
Upload versioned artifacts
  ↓
Update manifest or stable pointer

For example:

/search/articles.2026-06-03T120000Z.json
/search/manifest.json

The manifest points to the currently active version:

{
  "articles": "/search/articles.2026-06-03T120000Z.json"
}

The client loads the manifest first, then the actual index.

If generation fails, the manifest is not updated.

The old version stays active.

That gives the pipeline a simple rollback behavior without needing a complex deployment system.

CDN as the runtime boundary

The most important runtime boundary is CloudFront.

Once JSON files are uploaded to S3 and served through CloudFront, the client has a simple dependency:

GET /some/generated/file.json

This is easy to cache.
Easy to version.
Easy to debug.
Easy to serve globally.
Easy to replace later.

The CMS is no longer part of the client runtime path.

If the CMS is temporarily unavailable, existing generated JSON files still work.

That is a very important operational property.

Versioned files

For some cases, generated files can be overwritten:

/search/articles.json

For other cases, versioned files may be better:

/search/articles.2026-06-03T120000Z.json
/search/manifest.json

The manifest points to the current version:

{
  "articles": "/search/articles.2026-06-03T120000Z.json"
}

The client loads the manifest first, then the actual index.

This allows safer rollouts and better cache control.

Static assets can be cached aggressively.
Small manifests can have shorter cache lifetimes.

Clear contracts

The generated JSON files should be treated as real public contracts.

That means they should have:

  • stable field names
  • predictable paths
  • schema validation
  • tests for generators
  • clear versioning if the shape changes

For example, a search article document can have a type:

export type SearchArticle = {
  id: string;
  title: string;
  description: string;
  url: string;
  tags: readonly string[];
};

The generator owns the transformation from CMS data to this contract.

The client owns only the rendering and search behavior.

This separation keeps both sides calm.

Where Vyriy fits

This pattern fits naturally into Vyriy.

Vyriy is about small tools, explicit boundaries, and predictable runtime behavior.

A content pipeline can be split into small packages or commands:

@vyriy/search
@vyriy/i18n
@vyriy/cms
@vyriy/cdn

Or it can start even simpler:

scripts/generate-search.ts
scripts/generate-translations.ts

The important part is not the package structure.

The important part is the architectural boundary:

CMS → generator → JSON → CDN → client

That boundary is easy to explain and easy to operate.

A calm pipeline is not magic

There is no hidden runtime framework here.

No special server.
No complex CMS integration in the browser.
No client-side knowledge of internal CMS schemas.
No unnecessary live dependency on Strapi.

Just a boring pipeline:

  1. CMS changed
  2. webhook fired
  3. worker generated files
  4. files were validated
  5. files were uploaded to S3
  6. CloudFront served them
  7. client loaded them

Boring is good.

Boring systems are easier to debug.

Final thought

A CMS is a great editing interface.

But it does not always need to be a runtime API.

For many frontend systems, especially static sites and micro frontends, the calmer approach is to generate static JSON artifacts and serve them from the CDN.

This gives us clear boundaries:

CMS owns editing.
Generator owns transformation.
S3 owns storage.
CloudFront owns delivery.
Client owns rendering.

And it gives us a safety fuse:

If generation fails, the previous valid artifact remains available.

That is the kind of architecture Vyriy should encourage.

Small pieces.
Explicit contracts.
Predictable runtime.
Calm delivery.