> ## Documentation Index
> Fetch the complete documentation index at: https://constanza101-borrissol-28.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Keystatic CMS: Git-backed blog setup and author workflow

> How Keystatic powers the Borrissol blog: local vs cloud storage, blog collection schema, author workflow for publishing posts, and deploy considerations.

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`:

```typescript theme={null}
const isDev = process.env.NODE_ENV !== 'production';
```

| Mode       | When active                                      | How it works                                                                                                                                                                   |
| ---------- | ------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| **Local**  | `NODE_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.                                 |
| **GitHub** | `NODE_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](https://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

```typescript keystatic.config.ts theme={null}
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 key     | Label in UI        | Type                      | Notes                                                                                                   |
| ------------- | ------------------ | ------------------------- | ------------------------------------------------------------------------------------------------------- |
| `title`       | Títol              | `fields.slug`             | Title text + auto-generated URL slug. The slug becomes the file name and the URL path (`/blog/<slug>`). |
| `summary`     | Resum              | `fields.text` (multiline) | ≤ 160 characters. Used as the card excerpt on `/blog` and as `meta description` for the post page.      |
| `publishedAt` | Data de publicació | `fields.date`             | Required. Displayed on the post and used for sort order.                                                |
| `coverImage`  | Imatge destacada   | `fields.image`            | Saved to `public/blog/images/[slug]/`. Minimum 1200×630 px recommended for Open Graph.                  |
| `content`     | Contingut          | `fields.markdoc`          | The full post body in Markdoc (Markdown + optional custom tags).                                        |

<Note>
  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`.
</Note>

***

## Author workflow: publishing a new post

<Steps>
  <Step title="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`.)
  </Step>

  <Step title="Create a new entry">
    In the left sidebar, click **Blog**, then click **New entry** in the top-right corner.
  </Step>

  <Step title="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.
  </Step>

  <Step title="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`.
  </Step>

  <Step title="Merge the Pull Request">
    Review the PR on [github.com/constanza101/borrissol](https://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.
  </Step>
</Steps>

***

## Deploy and cost considerations

<Warning>
  **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.
</Warning>

### 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.

<Tip>
  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.
</Tip>

### 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.
