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

# Blog: Keystatic CMS content and routing

> The Borrissol blog is Catalan-only, powered by Keystatic CMS with Astro Content Collections. Covers post routing, slug structure, and author workflow.

The blog is intentionally Catalan-only. Belén (the studio founder and sole author) writes in Catalan, and maintaining translated versions of every post would add unsustainable editorial overhead for a one-person operation. Non-Catalan visitors who navigate to `/es/blog`, `/en/blog`, or `/fr/blog` are silently redirected to `/blog` via 301 redirects configured in `astro.config.mjs`. This keeps the URL structure clean without surfacing broken or empty pages.

## Routes

| Route                                                                      | File                          | Notes                                                      |
| -------------------------------------------------------------------------- | ----------------------------- | ---------------------------------------------------------- |
| `/blog`                                                                    | `src/pages/blog/index.astro`  | Post listing, sorted by `publishedAt` descending           |
| `/blog/[slug]`                                                             | `src/pages/blog/[slug].astro` | Individual post, rendered from MDX via Content Collections |
| `/ca/blog`, `/es/blog`, `/en/blog`, `/fr/blog`                             | —                             | 301 redirect → `/blog` (in `astro.config.mjs`)             |
| `/ca/blog/[slug]`, `/es/blog/[slug]`, `/en/blog/[slug]`, `/fr/blog/[slug]` | —                             | 301 redirect → `/blog/[slug]` (in `astro.config.mjs`)      |
| `/keystatic`                                                               | Keystatic serverless handler  | Auth-gated CMS panel (production only)                     |

## Content storage

Blog posts are MDX files stored in `src/content/blog/`. Each file is named after the post slug (e.g. `com-funciona-el-tufting.mdx`). The Keystatic CMS creates and commits these files automatically when Belén publishes via the editor UI — no manual file creation is needed.

```
src/
└── content/
    └── blog/
        ├── com-funciona-el-tufting.mdx
        ├── historia-de-borrissol.mdx
        └── ...
```

Cover images are stored separately in `public/blog/images/[post-slug]/` and referenced at `/blog/images/[post-slug]/filename.jpg`. They are served as static assets, not processed through the Astro image pipeline.

## Post schema

The schema is defined in `keystatic.config.ts`. Every field maps directly to frontmatter in the generated MDX file:

```typescript theme={null}
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',
  }),
}
```

| Field         | Type             | Notes                                                                             |
| ------------- | ---------------- | --------------------------------------------------------------------------------- |
| `title`       | `fields.slug`    | Dual-purpose: display title + auto-generates the URL slug                         |
| `summary`     | `fields.text`    | Max 160 chars; used as meta description and card excerpt                          |
| `publishedAt` | `fields.date`    | Required; used for sorting and formatted display                                  |
| `coverImage`  | `fields.image`   | Stored in `public/blog/images/`; referenced at `/blog/images/...`                 |
| `content`     | `fields.markdoc` | Full post body (Markdoc format), rendered via `render(post)` from `astro:content` |

<Note>
  The `title` field uses `fields.slug` rather than `fields.text`. Keystatic automatically derives the slug from the title, lowercasing and hyphenating it. The slug becomes the file name *and* the URL path segment — `/blog/[slug]`.
</Note>

## Blog index page

`src/pages/blog/index.astro` fetches all published posts with `getCollection('blog')`, sorts them newest-first, and renders a 3-column card grid (2 columns at ≤840px, 1 column at ≤540px):

```astro theme={null}
const posts = (await getCollection('blog'))
  .sort((a, b) =>
    new Date(b.data.publishedAt).getTime() -
    new Date(a.data.publishedAt).getTime()
  );
```

Each card displays:

* Cover image (600×400, `loading="lazy"`, `aspect-ratio: 3/2`)
* Publication date formatted as `d de MMMM de YYYY` in Catalan (`ca-ES` locale)
* Post title
* Summary excerpt
* "Llegir →" CTA link

If `posts.length === 0`, a placeholder `<p>Pròximament…</p>` is shown instead of the grid — useful during initial setup before any posts are published.

The page head metadata is hardcoded in `ca` (not driven by `ui.ts`):

```astro theme={null}
<Layout
  title="Blog"
  description="Articles sobre tufting, tèxtils i creativitat de Borrissol Espai Creatiu, Mataró."
  caOnly
>
```

The `caOnly` prop on `Layout.astro` signals that the page should not be indexed by non-Catalan crawlers and omits `hreflang` alternate tags for the other locales.

## Individual post page

`src/pages/blog/[slug].astro` resolves the post from `getCollection('blog')` by matching `post.id === slug`, then renders it with `render(post)` from `astro:content`:

```astro theme={null}
export async function getStaticPaths() {
  const posts = await getCollection('blog');
  return posts.map(post => ({ params: { slug: post.id } }));
}

const { slug } = Astro.params;
const posts = await getCollection('blog');
const post = posts.find(p => p.id === slug);

if (!post) return Astro.redirect('/blog');

const { Content } = await render(post);
```

The post layout includes:

1. Back link (`← Blog`)
2. Publication date (formatted `ca-ES`)
3. H1 title
4. Summary paragraph
5. Cover image (1200×630, `loading="eager"`, `aspect-ratio: 16/9`)
6. MDX body content (`<Content />`)
7. In-page booking CTA aside
8. "← Tornar al blog" footer link

### In-page booking CTA

Every post ends with a styled `<aside>` that prompts visitors to book a workshop. The WhatsApp link pre-fills a message attributing the enquiry to the blog:

```astro theme={null}
<a
  href={waHref('Hola! Vinc del blog i vull informació sobre el taller de tufting.')}
  class="btn btn-primary"
  target="_blank"
  rel="noopener noreferrer"
>
  Reserva el teu taller
</a>
```

## Keystatic CMS workflow

<CardGroup cols={2}>
  <Card title="1. Open the editor" icon="pencil">
    Belén navigates to `borrissol.com/keystatic` and signs in via Keystatic Cloud authentication.
  </Card>

  <Card title="2. Create or edit a post" icon="file-plus">
    The Keystatic UI presents the schema fields (title, summary, date, cover image, Markdoc body editor). Belén fills them in and saves.
  </Card>

  <Card title="3. Commit to GitHub" icon="git-commit">
    On publish, Keystatic opens a pull request (or pushes directly) to a `keystatic/[slug]` branch on `constanza101/borrissol`. The MDX file lands in `src/content/blog/`.
  </Card>

  <Card title="4. Auto-deploy" icon="rocket">
    Netlify detects the push and triggers a production build. The new post is live within \~2 minutes.
  </Card>
</CardGroup>

### Storage mode by environment

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

storage: isDev
  ? { kind: 'local' }
  : { kind: 'github', repo: 'constanza101/borrissol', branchPrefix: 'keystatic/' },
```

| Environment                              | Storage mode                                                       | Auth required         |
| ---------------------------------------- | ------------------------------------------------------------------ | --------------------- |
| `NODE_ENV !== 'production'` (local dev)  | `local` — reads/writes directly to the filesystem                  | None                  |
| Production (`NODE_ENV === 'production'`) | `github` — commits to `constanza101/borrissol` via Keystatic Cloud | Keystatic Cloud OAuth |

<Note>
  In local development, the Keystatic panel at `localhost:4321/keystatic` is accessible without login. It reads from and writes to `src/content/blog/` on your local filesystem. There is no network call to GitHub.
</Note>

## Netlify deploy credits

<Warning>
  Each blog publish triggers a full Netlify production build, consuming approximately 15 build minutes. Deploy previews for `keystatic/*` branches are **disabled** in `netlify.toml` to avoid burning credits on draft commits. Only pushes to `main` trigger a deploy.
</Warning>

To add a post without triggering unnecessary previews, Keystatic is configured with `branchPrefix: 'keystatic/'`. The `netlify.toml` ignores these branches for preview deploys, so the credit cost per published post is exactly one production build.
