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.

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

RouteFileNotes
/blogsrc/pages/blog/index.astroPost listing, sorted by publishedAt descending
/blog/[slug]src/pages/blog/[slug].astroIndividual post, rendered from MDX via Content Collections
/ca/blog, /es/blog, /en/blog, /fr/blog301 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)
/keystaticKeystatic serverless handlerAuth-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:
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',
  }),
}
FieldTypeNotes
titlefields.slugDual-purpose: display title + auto-generates the URL slug
summaryfields.textMax 160 chars; used as meta description and card excerpt
publishedAtfields.dateRequired; used for sorting and formatted display
coverImagefields.imageStored in public/blog/images/; referenced at /blog/images/...
contentfields.markdocFull post body (Markdoc format), rendered via render(post) from astro:content
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].

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

1. Open the editor

Belén navigates to borrissol.com/keystatic and signs in via Keystatic Cloud authentication.

2. Create or edit a post

The Keystatic UI presents the schema fields (title, summary, date, cover image, Markdoc body editor). Belén fills them in and saves.

3. Commit to GitHub

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

4. Auto-deploy

Netlify detects the push and triggers a production build. The new post is live within ~2 minutes.

Storage mode by environment

// keystatic.config.ts
const isDev = process.env.NODE_ENV !== 'production';

storage: isDev
  ? { kind: 'local' }
  : { kind: 'github', repo: 'constanza101/borrissol', branchPrefix: 'keystatic/' },
EnvironmentStorage modeAuth required
NODE_ENV !== 'production' (local dev)local — reads/writes directly to the filesystemNone
Production (NODE_ENV === 'production')github — commits to constanza101/borrissol via Keystatic CloudKeystatic Cloud OAuth
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.

Netlify deploy credits

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

Build docs developers (and LLMs) love