# A website without a CMS: Astro Content Collections in practice

> Source: <https://dev.to/seppegadeyne/a-website-without-a-cms-astro-content-collections-in-practice-17>
> Published: 2026-10-01 07:42:14+00:00

I moved [straffesites.com](https://straffesites.com/en) from Astro with Storyblok to Astro Content Collections with local MDX and JSON files. The site still has articles, case studies, landing pages, and Dutch and English versions. What it no longer has is a separate CMS for managing that content.

I am the only person managing the content on straffesites.com, and I work with Hermes, an AI agent. Since I already work in Git, moving the content there was a useful simplification. Content changes now go through the same repository and build pipeline as template changes. I can inspect a paragraph edit next to the schema or component it depends on.

This is a case study of that setup: how the content is organized, what the schemas check, how publishing works, and which CMS features I chose to give up. It is not a recommendation to make every editor learn Git.

Astro and Storyblok worked well together. I still use that combination for client projects where an editing interface is useful. On straffesites.com, however, I am the only human editing and managing the content. Hermes helps me work with the files and run checks. I did not need a visual editor for that workflow.

Keeping a CMS meant maintaining content models in another system, managing my CMS account and its permissions, and configuring the integration that fetched content during builds. Code lived in Git; content lived in Storyblok. Changes that touched both required checking both.

Working with Hermes was another part of the decision. The agent could already read the project, edit files, and run checks. To work with CMS content, it also needed an authenticated client and a separate content workflow. That integration was possible, but it was unnecessary for this project.

Moving the content into Git removed the CMS-specific access layer. It did not remove repository permissions, deployment credentials, or the need to maintain the AI tooling.

[Astro Content Collections](https://docs.astro.build/en/guides/content-collections/) let you organize, validate, and query structured content. They can use local files, remote sources, or live data. The setup on straffesites.com uses local files loaded at build time:

Those are six collections in this project. You do not need that exact split to use the same approach.

A collection combines a loader with a schema. The loader finds the entries; the schema defines the fields the templates can expect. Here is an excerpt from the article collection configuration:

``` js
import { defineCollection, reference } from 'astro:content';
import { glob } from 'astro/loaders';
import { articleFrontmatterSchema } from './content/schemas/article';

const articles = defineCollection({
  loader: glob({ pattern: '**/*.mdx', base: './src/content/articles' }),
  schema: articleFrontmatterSchema.safeExtend({
    author: reference('people').optional(),
  }).superRefine((article, context) => {
    if (article.articleType !== 'glossary' && !article.author) {
      context.addIssue({
        code: 'custom',
        path: ['author'],
        message: 'blog and case articles must include an author',
      });
    }
  }),
});

// The full configuration also registers the people collection.
export const collections = { articles /*, pages, datasets, caseMetrics, people, settings */ };
```

This is a project-specific excerpt, not a complete starter configuration. It imports the article schema from another file, and the author reference depends on the `people` collection being registered in the full configuration.

The same article collection holds blog posts, case studies, and glossary entries. The refinement requires an author for blog posts and cases while allowing glossary entries without one.

The frontmatter schema defines fields such as the title, route, publication date, category, tags, summary, and FAQ items. Required fields must be present. A strict schema also rejects unknown field names, so a typo cannot silently become unused metadata.

That gives the templates a defined content contract. It does not make the article correct.

A schema can reject a missing title or a value that violates a configured rule. It cannot establish that an explanation is accurate, a source supports a claim, or an internal link is helpful. A nonempty summary also does not imply a sensible length unless you add a length limit.

I use additional contract tests and build-output checks for rules that go beyond frontmatter. Content Collections alone do not check every link, component choice, or editorial requirement.

There is no visual editor in this local-file setup. Content Collections do not provide editor accounts, approval screens, a media library, or a publishing calendar.

You can add previews and access controls around a Git workflow. You can also put a Git-based CMS in front of the files. That is a valid option, but it adds an editing interface; it is different from editing the files directly.

The MDX files on straffesites.com are grouped by language and content type:

```
src/content/
├── pages/            # landing pages and hub pages (MDX)
│   ├── nl/           # Dutch-language pages
│   └── en/           # English pages
└── articles/
    ├── nl/
    │   ├── blog/      # blogs
    │   ├── case/      # case studies
    │   └── begrippen/ # glossary terms
    └── en/
        ├── blog/      # blogs
        ├── case/      # case studies
        └── glossary/ # glossary terms
```

The JSON collections sit alongside these directories. The tree above only shows the MDX content.

Each MDX entry starts with frontmatter. The body uses Markdown inside typed content-block components. On the original website, for example, this article's source file contains its metadata and body, while the page template renders shared elements such as the author card and related articles. The DEV version you are reading is an adapted Markdown crosspost, not that MDX file running inside Astro.

For this project, the route comes from an explicit frontmatter field rather than the file's location. That lets me reorganize source files without treating every move as a URL change. The templates and tests still need to respect that convention; Astro does not infer the project's routing rules from the directory tree alone.

Dutch and English pages have a shared pair registry in the repository. It records which routes are counterparts.

Build checks can then verify that those routes exist and that the relationships are consistent. The files being next to each other is not enough: translation pairing is a rule I have to define and test.

This is one advantage of keeping content and code together. The content, route relationships, and checks can change in the same diff.

On straffesites.com, a push to `main` triggers a Vercel build and deployment. Astro validates the content and renders static pages. A local commit by itself does not publish anything.

The workflow is straightforward:

``` php
Edit MDX or JSON
    -> inspect the diff
    -> run content and build checks
    -> commit and push
    -> verify the deployment
```

That diagram describes the work to do, not a security guarantee. Git does not force someone to inspect a diff, and a schema does not require human approval. Repository permissions and required checks determine what can reach production.

Git also makes rollback understandable: revert the relevant change, build again, and deploy the result. Reverting a commit locally does not change the live site until the reverted version is deployed successfully.

| Concern | Local content in Git | API-first headless CMS | 
|---|---|---|
| Editing | Files and repository tooling | CMS dashboard | 
| Content validation | Configured schemas and project checks | CMS field rules; project checks can also apply | 
| Content history | Diffs for committed changes | CMS revisions, depending on the platform | 
| Static publishing | Build and deploy after a repository change | Build and deploy after a CMS content change | 
| Systems to maintain | Dependencies, CI, hosting, and repository access | Those systems plus the CMS integration and accounts | 

These are common workflows, not fixed limits. A headless CMS can feed a fully static site, and a Git-based CMS can provide a dashboard while keeping content in files.

Hermes is the AI agent I work with on this project, not another human editor. It can read an MDX file, update a paragraph, and run the same checks used for code changes. It does not need a CMS-specific API key to edit checked-out files.

For me, the useful part is how easy the changes are to inspect. A content edit can include its frontmatter, body, and related template changes in one diff. There is no separate CMS revision to reconstruct alongside the code change.

Reading a local file avoids a CMS API request. It does not make model usage free: text sent to the model still consumes tokens. Nor does it prove that agents perform better with files than with a CMS.

Project instructions can tell an agent which files to use, which commands to run, and what it must not change. They help it work within the project's conventions. They are not a security boundary.

An agent with broad repository or deployment access can still make harmful changes. Limit its permissions, make checks explicit, and verify what it changed. For straffesites.com, I remain responsible for the scope and the published result, including when I authorize Hermes to deploy after checks.

Plausible but false prose can pass every schema check. That is an editorial review problem, not something Zod can solve.

I track straffesites.com through monthly PageSpeed measurements and Search Console data on its [public case page](https://straffesites.com/en/case/straffe-sites). Those measurements describe the deployed site. They are not a controlled test of whether removing Storyblok improved performance.

A headless CMS can supply content during the build and produce the same static HTML. Visitors do not necessarily make a CMS request when they open that page.

Removing the CMS API from my build reduced an external build dependency. Visitor performance still depends on the output: JavaScript, images, fonts, caching, and how the page renders.

The change I can describe directly is operational. My content workflow no longer needs a CMS service, a separate editor account, or a CMS integration. Dependencies, hosting, and access control still need maintenance.

For a client project, I would keep a CMS when the editing interface solves a problem the client or their editorial team actually has.

If editors do not work in Git, asking them to maintain MDX may just move the burden onto them or the development team. Draft states, permissions, scheduling, and media management are useful features, especially when several people publish regularly.

Translation workflows can also be more involved than maintaining two files and a route pair. Editors may need field-level translation status or separate approval steps. A CMS can provide those workflows without requiring the team to build them.

Changing a content model is another consideration. In my setup, adding a new content type means changing a schema and its templates. A CMS dashboard may make field management easier, although frontend changes can still require development work.

You can build versions of these features around Git. But if you need an editor, scheduling, media permissions, and approval screens, consider whether you are rebuilding the CMS you removed.

For straffesites.com, direct file editing fits how I manage the content with help from Hermes. For a client with an editorial team, I may choose a CMS instead. I would start with who edits the content and what their publishing process needs, then choose the storage and tooling.

*Adapted for DEV from [A website without a CMS: Astro Content Collections in practice](https://straffesites.com/en/blog/website-without-cms), originally published on straffesites.com.*
