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

# Borrissol configuration reference: Astro, site, and config

> Reference for astro.config.mjs, src/config/site.ts, and src/config.ts — the config files controlling routing, SEO, redirects, and integrations.

Borrissol's project-wide settings are spread across three files that each own a clearly defined domain: `astro.config.mjs` controls the framework, adapters, integrations, routing and redirects; `src/config/site.ts` centralises every SEO token and business detail that feeds structured data; and `src/config.ts` holds the WhatsApp deep-link utility consumed by all four language variants of the site. Keeping these three files in sync is the first step before going live or after any rebrand.

***

## `astro.config.mjs`

The root framework config. It declares the deployment target, trailing-slash policy, 301 redirects, all integrations, and the i18n routing strategy.

```javascript astro.config.mjs theme={null}
// @ts-check
import { defineConfig } from 'astro/config';
import sitemap from '@astrojs/sitemap';
import netlify from '@astrojs/netlify';
import keystatic from '@keystatic/astro';
import react from '@astrojs/react';
import markdoc from '@astrojs/markdoc';

export default defineConfig({
  site: 'https://borrissol.com',
  adapter: netlify(),
  trailingSlash: 'never',
  i18n: {
    defaultLocale: 'ca',
    locales: ['ca', 'es', 'en', 'fr'],
    routing: { prefixDefaultLocale: false },
  },
  // ... redirects, integrations (see sections below)
});
```

### `site`

```
site: 'https://borrissol.com'
```

The canonical origin. Astro uses this to generate absolute URLs in the sitemap, canonical tags, and Open Graph meta. Update this value if the domain ever changes.

### `adapter`

```javascript theme={null}
adapter: netlify()
```

Provided by `@astrojs/netlify` (`^7.0.12`). All pages are statically prerendered except the Keystatic admin routes, which require SSR. The adapter wraps those in Netlify Functions automatically with no extra configuration.

### `trailingSlash`

```
trailingSlash: 'never'
```

Ensures all URLs are canonical without a trailing slash (e.g. `/es/tallers` not `/es/tallers/`). The Netlify adapter enforces this at the edge.

### `redirects`

All redirects are declarative 301s in the Astro config. They fall into two groups:

**Legacy `/ca/*` → `/*`** — Catalan is the default locale served at root (no `/ca/` prefix). Any legacy URLs with the `/ca` prefix get permanently redirected to strip it.

**`/<lang>/blog` → `/blog`** — The blog is intentionally Catalan-only. Non-Catalan blog URLs redirect to the Catalan blog so shared links and old bookmarks still resolve.

```javascript theme={null}
redirects: {
  // /ca/* → / (legacy prefix removal)
  '/ca':             { status: 301, destination: '/' },
  '/ca/gallery':     { status: 301, destination: '/gallery' },
  '/ca/press':       { status: 301, destination: '/press' },
  '/ca/blog':        { status: 301, destination: '/blog' },
  '/ca/blog/[slug]': { status: 301, destination: '/blog/[slug]' },

  // /<lang>/blog → /blog (blog is CA-only)
  '/es/blog':        { status: 301, destination: '/blog' },
  '/es/blog/[slug]': { status: 301, destination: '/blog/[slug]' },
  '/en/blog':        { status: 301, destination: '/blog' },
  '/en/blog/[slug]': { status: 301, destination: '/blog/[slug]' },
  '/fr/blog':        { status: 301, destination: '/blog' },
  '/fr/blog/[slug]': { status: 301, destination: '/blog/[slug]' },
},
```

<Note>
  Astro requires redirect destinations to match real routes. Wildcards like `/ca/*` do not work in Astro's `redirects` object — each sub-route must be listed explicitly.
</Note>

### `integrations`

```javascript theme={null}
integrations: [
  sitemap({
    // Emit hreflang cross-references between language variants of each page.
    // Keys must match the locale codes in `i18n.locales`.
    i18n: {
      defaultLocale: 'ca',
      locales: {
        ca: 'ca-ES',
        es: 'es-ES',
        en: 'en-US',
        fr: 'fr-FR',
      },
    },
    // Defensive filter: exclude any stray /ca routes from the sitemap.
    filter: (page) => !page.includes('/ca'),
  }),
  react(),
  markdoc(),
  keystatic(),
],
```

| Integration   | Package                   | Purpose                                                                                                                                            |
| ------------- | ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------- |
| `sitemap()`   | `@astrojs/sitemap ^3.7.3` | Generates `sitemap.xml` with `hreflang` `rel="alternate"` entries for all four locales. Mirrors the hreflang tags already in each page's `<head>`. |
| `react()`     | `@astrojs/react ^5.0.7`   | Required by Keystatic's admin UI, which is a React application. No React is used on public-facing pages.                                           |
| `markdoc()`   | `@astrojs/markdoc ^1.0.6` | Renders blog post bodies written in Markdoc (a superset of Markdown).                                                                              |
| `keystatic()` | `@keystatic/astro ^5.0.6` | Mounts the Keystatic admin at `/keystatic` and handles the Git-backed CMS API routes.                                                              |

### `i18n`

```javascript theme={null}
i18n: {
  defaultLocale: 'ca',
  locales: ['ca', 'es', 'en', 'fr'],
  routing: {
    prefixDefaultLocale: false,
  },
},
```

| Key                   | Value                      | Meaning                                                                                                      |
| --------------------- | -------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `defaultLocale`       | `'ca'`                     | Catalan content is served at root (e.g. `/tallers`).                                                         |
| `locales`             | `['ca', 'es', 'en', 'fr']` | The four supported languages.                                                                                |
| `prefixDefaultLocale` | `false`                    | Catalan URLs have **no** language prefix; Spanish, English and French are prefixed (`/es/`, `/en/`, `/fr/`). |

***

## `src/config/site.ts`

This file exports the `SITE` constant — a single object that every SEO and structured-data component reads from. It is the single source of truth for the business name, address, contact details, hours, and social links.

### Type definitions

```typescript theme={null}
type SchemaBusinessType = 'ProfessionalService' | 'LocalBusiness' | 'ArtGallery';

interface PostalAddress {
  streetAddress?: string;
  addressLocality?: string;
  addressRegion?: string;
  postalCode?: string;
  addressCountry: string;
}

interface GeoCoordinates {
  latitude: number;
  longitude: number;
}

interface BusinessConfig {
  type: SchemaBusinessType;
  name: string;
  description: string;
  email?: string;
  telephone?: string;
  address: PostalAddress;
  geo?: GeoCoordinates;
  priceRange?: string;
  openingHours?: string[];
  sameAs?: string[];
}

interface SiteConfig {
  name: string;
  url: string;
  locale: string;
  logo: string;
  defaultOgImage: string;
  business: BusinessConfig;
}
```

### The `SITE` export

```typescript src/config/site.ts theme={null}
export const SITE: SiteConfig = {
  name: 'Borrissol',
  url: 'https://borrissol.com',
  locale: 'es_ES',
  logo: '/images/borrissol-logo-b-512.png',
  defaultOgImage: '/og-default.jpg',

  business: {
    type: 'LocalBusiness',
    name: 'Borrissol Espai Creatiu',
    description:
      'Espai creatiu de tallers tèxtils a Mataró. Especialistes en tufting, bordats i experiències artístiques.',
    email: 'borrissolespaicreatiu@gmail.com',
    telephone: '+34673247520',
    address: {
      streetAddress: 'Carrer de Sant Antoni, 17, baix',
      addressLocality: 'Mataró',
      addressRegion: 'Catalunya',
      postalCode: '08301',
      addressCountry: 'ES',
    },
    geo: {
      latitude: 41.5373926,
      longitude: 2.4466578,
    },
    priceRange: '€€',
    openingHours: ['Mo-Sa 09:30-13:30', 'Mo-Sa 16:30-20:30'],
    sameAs: [
      'https://www.instagram.com/borrissol_espai_creatiu',
      'https://www.tiktok.com/@borrissol_espai_creatiu',
    ],
  },
};
```

### Key fields

| Field                   | Value                                        | Used by                                                             |
| ----------------------- | -------------------------------------------- | ------------------------------------------------------------------- |
| `url`                   | `https://borrissol.com`                      | Canonical URLs, Open Graph, JSON-LD                                 |
| `locale`                | `es_ES`                                      | Open Graph `og:locale`                                              |
| `logo`                  | `/images/borrissol-logo-b-512.png`           | JSON-LD `Organization.logo` (512×512px per Google spec)             |
| `defaultOgImage`        | `/og-default.jpg`                            | Fallback `og:image` for pages without a cover photo                 |
| `business.type`         | `LocalBusiness`                              | JSON-LD `@type` — controls which Google rich results are eligible   |
| `business.geo`          | `41.5373926, 2.4466578`                      | JSON-LD `geo` node — places the business in Google's local map pack |
| `business.openingHours` | `['Mo-Sa 09:30-13:30', 'Mo-Sa 16:30-20:30']` | JSON-LD `openingHours` — shown in Knowledge Panel                   |
| `business.sameAs`       | Instagram, TikTok URLs                       | JSON-LD `sameAs` — reinforces entity disambiguation                 |

<Warning>
  Update `SITE` before going live after any rebrand or change of address. The object feeds `Seo.astro`, all JSON-LD structured data blocks, and the Open Graph tags on every page — stale values will be indexed by search engines.
</Warning>

***

## `src/config.ts`

A minimal module that exposes the WhatsApp contact number and the `waHref` URL builder.

```typescript src/config.ts theme={null}
const WHATSAPP_BASE = 'https://wa.me/34673247520';

export const WHATSAPP_HREF = WHATSAPP_BASE;

export const waHref = (text: string) =>
  `${WHATSAPP_BASE}?text=${encodeURIComponent(text)}`;
```

### `waHref(text)`

Builds a WhatsApp deep link with a pre-filled message. Every CTA button and the floating WhatsApp FAB call `waHref()` with a locale-specific message string fetched from `src/i18n/ui.ts`. The resulting URL opens the WhatsApp app (or web) with the message pre-composed, ready to send.

**Example:**

```typescript theme={null}
import { waHref } from '../../config';

const href = waHref('Hola! Vull apuntar-me a un taller de tufting.');
// → 'https://wa.me/34673247520?text=Hola%21%20Vull%20apuntar-me%20a%20un%20taller%20de%20tufting.'
```

***

## Environment variables

No `.env` file is required. All project configuration is in code. Keystatic Cloud credentials (used for authenticating the `/keystatic` admin in production) are configured through the [Keystatic Cloud dashboard](https://keystatic.cloud) and injected by Netlify at build/runtime — they are never stored locally.

***

## Node version

The project requires **Node ≥ 22.12.0**, declared in `package.json` and pinned in `netlify.toml`:

```toml netlify.toml theme={null}
[build.environment]
  NODE_VERSION = "22.12.0"
```

Use [nvm](https://github.com/nvm-sh/nvm) or [fnm](https://github.com/Schniz/fnm) locally to match this version.

***

## `.npmrc`

```ini .npmrc theme={null}
legacy-peer-deps=true
```

<Note>
  `legacy-peer-deps=true` is required to resolve a peer dependency conflict between `@keystatic/astro 5.0.6` and Astro 6. Without it, `npm install` exits with an `ERESOLVE` error. Do not remove this flag until the upstream conflict is resolved in a future Keystatic release.
</Note>
