Skip to main content

Documentation Index

Fetch the complete documentation index at: https://mintlify.com/constanza101/borrissol/llms.txt

Use this file to discover all available pages before exploring further.

Keystatic is a Git-backed CMS that stores content as MDX files directly in the repository — there is no external database. Blog posts live in src/content/blog/ as .mdoc files alongside their cover images in public/blog/images/. Every publish action becomes a GitHub Pull Request, and merging that PR triggers a Netlify deployment. This keeps the entire content history in git, makes rollbacks trivial, and ensures the CMS never drifts out of sync with the codebase.

Storage modes

Keystatic switches storage backends based on the NODE_ENV environment variable. The logic lives at the top of keystatic.config.ts:
const isDev = process.env.NODE_ENV !== 'production';
ModeWhen activeHow it works
LocalNODE_ENV !== 'production' (i.e. npm run dev)Reads and writes directly to the local filesystem. No authentication required — navigate to http://localhost:4321/keystatic and edit freely.
GitHubNODE_ENV === 'production' (Netlify deployment)Commits content to the constanza101/borrissol GitHub repository. New entries open a Pull Request on a branch prefixed keystatic/. Requires Keystatic Cloud authentication.

Local storage (development)

In local mode Keystatic is effectively a local GUI for editing files under src/content/blog/. Changes are written directly to disk — commit or discard them with git as you would any other file.

GitHub storage (production)

In production Keystatic authenticates via Keystatic Cloud (project borrissol/borrissol) and uses the GitHub API to:
  1. Create a new branch with the prefix keystatic/ (e.g. keystatic/blog-nou-taller-tufting)
  2. Commit the new or updated MDX file and any uploaded images to that branch
  3. Open a Pull Request against main
Merging the PR triggers the Netlify auto-deploy pipeline.

Full configuration

keystatic.config.ts
import { config, collection, fields } from '@keystatic/core';

const isDev = process.env.NODE_ENV !== 'production';

export default config({
  storage: isDev
    ? { kind: 'local' }
    : {
        kind: 'github',
        repo: 'constanza101/borrissol',
        branchPrefix: 'keystatic/',
      },
  cloud: {
    project: 'borrissol/borrissol',
  },
  ui: {
    brand: { name: 'Borrissol' },
  },
  collections: {
    blog: collection({
      label: 'Blog',
      slugField: 'title',
      path: 'src/content/blog/*',
      format: { contentField: 'content' },
      entryLayout: 'content',
      schema: {
        title: fields.slug({
          name: { label: 'Títol' },
          slug: {
            label: 'URL (slug)',
            description: "S'omple automàticament. Ex: com-funciona-el-tufting",
          },
        }),
        summary: fields.text({
          label: 'Resum',
          description: 'Apareix a la llista del blog i a Google (màx. 160 caràcters)',
          multiline: true,
          validation: { length: { max: 160 } },
        }),
        publishedAt: fields.date({
          label: 'Data de publicació',
          validation: { isRequired: true },
        }),
        coverImage: fields.image({
          label: 'Imatge destacada',
          description: "Imatge principal de l'entrada. Mínim 1200×630px recomanat.",
          directory: 'public/blog/images',
          publicPath: '/blog/images/',
        }),
        content: fields.markdoc({
          label: 'Contingut',
        }),
      },
    }),
  },
});

Blog collection schema

Each blog entry has five fields:
Field keyLabel in UITypeNotes
titleTítolfields.slugTitle text + auto-generated URL slug. The slug becomes the file name and the URL path (/blog/<slug>).
summaryResumfields.text (multiline)≤ 160 characters. Used as the card excerpt on /blog and as meta description for the post page.
publishedAtData de publicaciófields.dateRequired. Displayed on the post and used for sort order.
coverImageImatge destacadafields.imageSaved to public/blog/images/[slug]/. Minimum 1200×630 px recommended for Open Graph.
contentContingutfields.markdocThe full post body in Markdoc (Markdown + optional custom tags).
The blog is Catalan-only by design. There are no translated versions of blog posts. Non-Catalan /es/blog, /en/blog, and /fr/blog URLs 301-redirect to /blog — see the redirects in astro.config.mjs.

Author workflow: publishing a new post

1

Open the admin UI

Navigate to https://borrissol.com/keystatic in your browser. Keystatic Cloud will prompt for authentication if you are not already signed in. (In local dev, no login is required — go to http://localhost:4321/keystatic.)
2

Create a new entry

In the left sidebar, click Blog, then click New entry in the top-right corner.
3

Fill in the fields

Complete all five fields:
  • Títol — the post title. The URL (slug) is filled automatically from the title; edit it manually if needed (lowercase, hyphens, no accents).
  • Resum — a summary of up to 160 characters. This appears as the card blurb on the blog listing page and as the SEO meta description. The field validates the character limit in real time.
  • Data de publicació — choose the date the post should appear to have been published.
  • Imatge destacada — upload the cover image. Minimum recommended size: 1200 × 630 px (standard Open Graph dimensions). The image is saved to public/blog/images/ in the repository.
  • Contingut — write the post body using the rich-text Markdoc editor.
4

Save the entry

Click Save. In production mode, Keystatic creates a new branch (keystatic/...) and commits the MDX file and any uploaded images to it, then opens a Pull Request on GitHub against main.
5

Merge the Pull Request

Review the PR on github.com/constanza101/borrissol. Once satisfied, merge it. Netlify detects the push to main and automatically deploys the updated site — the new post is live within a few minutes.

Deploy and cost considerations

Each blog publish = one Netlify deploy ≈ 15 credits. On the Personal plan (1,000 credits/month) that equates to roughly 65 deploys per month across all activity. Avoid saving intermediate drafts to production — finish the post locally and only push the final version.

Cover image bandwidth

Cover images are stored in public/blog/images/[slug]/ and served directly from Netlify’s CDN. On the Personal plan, bandwidth is limited to ~50 GB/month (≈ 20 credits/GB). High-resolution images multiply quickly if the blog gets traffic. Optimise images before uploading:
  • Use WebP or JPEG at 80–85% quality
  • Resize to no wider than 2400 px before uploading
  • Astro’s <Picture> component will still generate AVIF/WebP variants at build time

Deploy previews for keystatic/* branches

Deploy previews for keystatic/ branches are disabled to avoid consuming credits on every draft save. Only merges to main trigger a production deploy.
To preview a post before merging, run npm run build && npm run preview locally after pulling the keystatic/* branch, or enable deploy previews temporarily in Netlify → Site configuration → Build & deploy → Deploy contexts and re-disable them afterwards.

Local dev workflow

In npm run dev mode, Keystatic uses local storage. Editing posts in the admin UI at http://localhost:4321/keystatic writes files directly to disk with no authentication and no Netlify credits consumed. This is the recommended way to draft and proof content before committing.

Build docs developers (and LLMs) love