# AlwaysRight — Supabase-Blog-Render in bestehenden Blog integrieren

> Diesen Prompt kopierst du in deinen Coding-Agent (Claude Code, Cursor,
> Windsurf, …) **im geöffneten Repo deiner Website**. Er ergänzt deinen
> bestehenden Magazin-/Blog-Bereich um eine **Supabase-Quelle**, aus der
> AlwaysRight die Beiträge liefert. Du brauchst dafür **keinen** GitHub-Autopilot
> und keinen Auto-Merge — du reviewst und mergest selbst.

---

Du bist ein Coding-Agent und arbeitest direkt im ausgecheckten Repo dieser
Website. Aufgabe: Baue einen Loader, der den Magazin-/Blog-Bereich **zusätzlich**
aus einer Supabase-Tabelle (`{{TABLE}}`) speist. AlwaysRight schreibt
jeden Beitrag in diese Tabelle und **überschreibt** ihn dort beim nächsten Push
(Upsert auf `slug`) — deine Seite muss also nur lesen und rendert immer den
aktuellen Stand.

**Bevor du irgendwelche Dateien schreibst:** lies das Repo und finde heraus, wie
die Integration ins bestehende Setup passt. Lege deine Änderungen auf einem
eigenen Branch ab und öffne einen PR — du (der Mensch) reviewst, bevor etwas live
geht.

## Sicherheits-Regeln (PFLICHT — vor allem anderen lesen)

Auch wenn du selbst reviewst: eine falsche Änderung kann den bestehenden
Magazin-Bereich aus der Production-Site löschen. Daher:

1. **Bestehende List-/Detail-Render-Logik NIE ersetzen** — nur ergänzen.
   Wenn das Repo schon einen Loader, eine Daten-Pipeline, CMS-Calls oder
   hardcoded Listen für den Magazin-Bereich hat, bleibt der Code-Pfad
   **unberührt**. AlwaysRight kommt als zusätzliche Quelle dazu, das Ergebnis
   wird mit dem Bestand **uniert** (slug-Kollisionen: Bestand gewinnt).
2. **Listen-/Detail-Seite darf nach deiner Änderung nicht leer/kaputt rendern.**
   Zähle vorher, wie viele Einträge die heutige Seite liefert. Nach der Änderung
   MUSS sie mindestens dieselben Einträge zeigen — plus AlwaysRight-Einträge,
   **falls vorhanden**. Sind AlwaysRights Quellen leer, fällt die Seite auf den
   **bestehenden** Loader zurück.
3. **`generateStaticParams`/Sitemap dürfen nicht schrumpfen.** Bestehende URLs
   bleiben nach dem Merge auflösbar — niemals 404.
4. **Außerhalb des Magazin-Bereichs nur additive Änderungen.** `package.json`,
   `next.config.*`, `tsconfig.json`, `tailwind.config.*`, Middleware,
   Layout-Komponenten: bestehende Werte bleiben. Nur neue Imports/Deps ergänzen.
5. **Wenn keine sichere additive Integration möglich ist → STOPP und frag nach.**
   Typische Fälle: externes CMS (Sanity/Contentful/Strapi) als Single-Source,
   monolithischer Page-Body mit hardcoded Posts, Renderer-Inkompatibilität.
   Erkläre, was du gefunden hast, statt zu raten.

## Quelle: Supabase (eine Tabelle, immer aktuell)

AlwaysRight pusht jeden Beitrag in die Supabase-Tabelle `{{TABLE}}`
(bzw. die von dir verbundene Tabelle). Frische Beiträge **und** Überarbeitungen
landen per Upsert in derselben Zeile — kein Re-Deploy nötig, die Seite rendert
beim nächsten Request/ISR-Tick den neuen Stand. Reihenfolge im Loader:

```
getPostBySlug(slug):
  1. Supabase konfiguriert?  → lies aus {{TABLE}}
  2. PFLICHT-Fallback        → bestehender Loader des Repos
```

Die Site rendert auch dann, wenn Supabase noch nicht eingerichtet ist — sie
nutzt dann ausschließlich den bestehenden Loader. Sobald Supabase verbunden ist,
kommen die AlwaysRight-Beiträge dazu.

## Tabellen-Schema (`{{TABLE}}`)

```
slug                  text primary key
title                 text not null
description           text
mdx                   text not null        -- NUR der Artikel-Body (kein Frontmatter!), beginnt mit "# Titel"
keywords              text[]
image                 text                  -- Featured-/Thumbnail-Bild (CDN-URL)
date                  timestamptz           -- Anzeige-Datum
published             boolean               -- Sichtbarkeits-Flag (true)
reading_time          integer               -- Lesezeit (Minuten)
category              text                  -- Themen-Cluster-Slug (z.B. 'ki') → URL + Menü
language              text not null         -- 'de' | 'en' | 'fr' | 'it' | 'es' — Sprache DIESER Version
translation_group_id  uuid                  -- klammert alle Sprachversionen eines Artikels
source_page_id        uuid
source_url            text                  -- voller Pfad inkl. Cluster: /{basis}/{category}/{slug}
published_at          timestamptz
updated_at            timestamptz
```

**Wichtig zur `mdx`-Spalte:** Sie enthält **ausschließlich den Body** — kein
YAML-Frontmatter. Render den Wert direkt (MDX/Markdown), **strippe kein
gray-matter**. Der Body beginnt bereits mit der H1 (`# Titel`), rendere den
`title` also nicht zusätzlich als sichtbares `<h1>`.

Das vollständige SQL (inkl. RLS-Policy + Trigger) findest du im AlwaysRight-
Dashboard direkt neben diesem Prompt zum Kopieren. **Führe es selbst im
Supabase-SQL-Editor aus** — nicht aus dem Code/CI heraus. Hast du schon eine
Beitrags-Tabelle mit anderem Namen/Schema, nutze den Prompt
`supabase-table-detect-prompt.md`, der das passende ALTER-TABLE-SQL erzeugt.

## Themen-Cluster (Kategorien) — URL-Struktur, Pillar-Seiten & Menü

Jeder Beitrag trägt eine `category`-Spalte (Cluster-Slug, z.B. `ki`). Die
`source_url` enthält den vollen Pfad `/{basis}/{category}/{slug}` (Basis z.B.
`blog` oder `magazin`). Daraus baust du:

1. **Routing:** Detail-Route `/{basis}/{category}/{slug}` (z.B.
   `app/{basis}/[category]/[slug]/page.tsx`). Lade den Post per `slug` (+ Locale),
   prüfe optional, dass `category` zum Pfad passt.
2. **Pillar-/Cluster-Index-Seite** `/{basis}/{category}` (z.B. `/magazin/ki`):
   listet alle Posts dieser Kategorie (`where category = <slug>`), dient als
   Themen-Hub. Verlinke von dort auf die Cluster-Beiträge.
3. **Themen-Cluster-Menü:** distinct `category`-Werte → Menüpunkte, je verlinkt
   auf `/{basis}/{category}`. So bekommt die Site automatisch ein Cluster-Menü.

`category` kann `null` sein (alte/ungetaggte Beiträge) — dann ohne Cluster-Segment
routen (`/{basis}/{slug}`) und im Menü überspringen. **Ändere bestehende URLs
nicht** — nur neue Beiträge tragen das Cluster-Segment.

## Mehrsprachigkeit — PFLICHT bei i18n-Sites

Jede Zeile trägt eine `language`-Spalte (`de`/`en`/…). AlwaysRight pusht jede
Sprachversion eines Artikels als **eigene Zeile** mit derselben
`translation_group_id`. **Wenn deine Site mehrere Sprachen rendert (z.B.
`/en/magazin` und `/magazin`), MUSS der Loader pro Locale nach `language`
filtern** — sonst erscheinen alle Sprachversionen auf jeder Locale-Seite.

- **Listen-Loader:** nimm die aktive Locale der Seite entgegen und filtere
  `where language = <locale>`. Hat deine Site nur eine Sprache, filtere auf
  deine Default-Sprache (i.d.R. `'de'`) oder lass den Filter weg.
- **hreflang / Sprach-Umschalter (optional):** Artikel mit gleicher
  `translation_group_id` sind Übersetzungen voneinander — daraus kannst du
  `<link rel="alternate" hreflang>` und einen Sprach-Umschalter bauen.

## Env-Variablen (Supabase optional)

Der Supabase-Client akzeptiert **beide** Schemata, mit Vorrang für die
AlwaysRight-prefixed Vars (für Projekte, die schon Supabase nutzen und isolieren
wollen). **Wenn keiner gesetzt ist, fällt der Loader stillschweigend auf den
bestehenden Loader zurück** — kein Error, kein Build-Bruch.

```ts
const url =
  process.env.NEXT_PUBLIC_ALWAYSRIGHT_SUPABASE_URL ||
  process.env.NEXT_PUBLIC_SUPABASE_URL ||
  null
const anonKey =
  process.env.NEXT_PUBLIC_ALWAYSRIGHT_SUPABASE_ANON_KEY ||
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
  null
const supabaseEnabled = Boolean(url && anonKey)
```

## Loader-Skelett (Referenz, anpassen an Repo-Konventionen)

```ts
// lib/alwaysright-posts.ts (Pfad je nach Path-Alias)
import { createClient } from '@supabase/supabase-js'

export interface AlwaysRightPost {
  slug: string
  title: string
  description: string | null
  mdx: string
  keywords: string[]
  language: string                 // 'de' | 'en' | …
  translation_group_id: string | null
  published_at: string | null
  updated_at: string | null
}

const SUPABASE_URL =
  process.env.NEXT_PUBLIC_ALWAYSRIGHT_SUPABASE_URL ||
  process.env.NEXT_PUBLIC_SUPABASE_URL ||
  null
const SUPABASE_ANON =
  process.env.NEXT_PUBLIC_ALWAYSRIGHT_SUPABASE_ANON_KEY ||
  process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY ||
  null
const supabase = (SUPABASE_URL && SUPABASE_ANON)
  ? createClient(SUPABASE_URL, SUPABASE_ANON)
  : null

const COLS = '{{SELECT_COLS}}'

// WICHTIG: bestehenden Loader des Repos hier importieren — der bleibt der
// finale Fallback (Sicherheits-Regel 1+2). Beispielname unten,
// tatsaechlichen Loader im Repo finden und imports anpassen.
import { listExistingPosts, getExistingPostBySlug } from '@/lib/existing-magazine-loader'

function dedupBySlug(posts: AlwaysRightPost[]): AlwaysRightPost[] {
  // Bei Slug-Kollision gewinnt der bestehende Repo-Eintrag (Regel 1).
  // Da `existing` als LETZTES gepushed wird, ueberschreibt es vorherige.
  const map = new Map<string, AlwaysRightPost>()
  for (const p of posts) map.set(p.slug, p)
  return Array.from(map.values())
}

// `locale` = aktive Sprache der Seite (z.B. 'en' auf /en/magazin). Bei i18n-
// Sites PFLICHT durchreichen, sonst erscheinen alle Sprachen auf jeder Locale.
// Einsprachige Sites lassen das Argument weg → kein Filter.
export async function listPosts(locale?: string): Promise<AlwaysRightPost[]> {
  const merged: AlwaysRightPost[] = []
  // 1. AlwaysRight Supabase (frischeste Quelle)
  if (supabase) {
    let q = supabase.from('{{TABLE}}').select(COLS)
      .order('published_at', { ascending: false })
    if (locale) q = q.eq('language', locale)
    const { data } = await q
    if (data) merged.push(...(data as AlwaysRightPost[]))
  }
  // 2. PFLICHT — bestehender Loader bleibt finaler Fallback.
  //    Mapping auf AlwaysRightPost-Shape ggf. anpassen.
  try {
    const existing = await listExistingPosts()
    merged.push(...existing.map(toAlwaysRightShape))
  } catch { /* bestehender Loader nicht verfuegbar — ok */ }
  return dedupBySlug(merged)
}

export async function getPostBySlug(slug: string): Promise<AlwaysRightPost | null> {
  // 1. AlwaysRight Supabase
  if (supabase) {
    const { data } = await supabase.from('{{TABLE}}').select(COLS)
      .eq('slug', slug).maybeSingle()
    if (data) return data as AlwaysRightPost
  }
  // 2. PFLICHT — bestehender Loader bleibt finaler Fallback.
  try {
    const existing = await getExistingPostBySlug(slug)
    if (existing) return toAlwaysRightShape(existing)
  } catch { /* bestehender Loader nicht verfuegbar — ok */ }
  return null
}

export async function listSlugs(): Promise<string[]> {
  const posts = await listPosts()
  return posts.map(p => p.slug)
}

// Mappt einen Post aus dem bestehenden Loader auf die AlwaysRight-Shape.
// Felder, die der bestehende Loader nicht hat, defaulten — niemals werfen.
function toAlwaysRightShape(p: any): AlwaysRightPost {
  return {
    slug: p.slug,
    title: p.title ?? p.slug,
    description: p.description ?? null,
    mdx: p.mdx ?? p.content ?? p.body ?? '',
    keywords: Array.isArray(p.keywords) ? p.keywords : [],
    language: p.language ?? p.locale ?? 'de',
    translation_group_id: p.translation_group_id ?? null,
    published_at: p.published_at ?? p.date ?? null,
    updated_at: p.updated_at ?? null,
  }
}
```

## Checkliste (in dieser Reihenfolge)

1. [ ] **Repo-Analyse** — Framework, Path-Alias-Mapping, `app/` vs `src/app/`,
       i18n, bestehender Render-Stack, vorhandene Magazin-/Blog-Strukturen.
2. [ ] **Bestehenden Magazin-Loader identifizieren UND zählen** — welche
       Datei/Funktion lädt heute die Posts? Wie viele Einträge liefert sie?
       (Wird als 2. Stufe / Fallback in den neuen Loader integriert.)
3. [ ] **Supabase-Loader** anlegen (Pfad an Path-Alias anpassen) — siehe
       Skelett oben. **Bestehenden Loader als finalen Fallback einbauen, nicht
       ersetzen.**
4. [ ] **Listen-Seite** erweitern (`listPosts()`) — bestehende Render-Logik
       bleibt, Supabase-Quelle wird **uniert** dazu.
5. [ ] **Detail-Seite** mit `[slug]`-Param erweitern (`getPostBySlug()`) — bei
       AlwaysRight-Miss fällt sie auf den bestehenden Pfad zurück. Die
       H1-Überschrift steckt als erste Zeile im Body — `title` nicht zusätzlich
       als sichtbares `<h1>` rendern (sonst doppelt).
6. [ ] **Dependencies** ergänzen (`@supabase/supabase-js`, ggf. MDX-Renderer).
       Nur was fehlt — bestehende Renderer wiederverwenden.
7. [ ] **Selbst-Check vor dem PR:** Würde die Seite leer rendern, wenn die
       AlwaysRight-Supabase-Quelle leer ist? Die Antwort muss **NEIN** sein
       (Fallback auf bestehenden Loader).
8. [ ] **Branch + PR** statt direkt auf `main` — du reviewst selbst.
9. [ ] **Danach (manuell, nicht im Code):** SQL im Supabase-SQL-Editor
       ausführen, die zwei optionalen Env-Vars setzen, `npm install` falls neue
       Deps, PR mergen.

## Renderer- & Bild-Hinweise

- **Renderer-Reihenfolge**, falls noch keiner im Repo ist:
  1. `next-mdx-remote/rsc` (Next.js App Router)
  2. `next-mdx-remote` (Pages Router)
  3. `react-markdown` mit `remark-gfm` (Pages oder Astro Fallback)
- **Tabellen (PFLICHT, sonst erscheinen rohe `| … |`-Pipes im Text):**
  AlwaysRight schreibt Tabellen als GFM-Markdown. `remark-gfm` MUSS aktiv sein —
  auch im MDX-Pfad (`options.mdxOptions.remarkPlugins`), nicht nur bei
  react-markdown. Dazu Tabellen-CSS im Corporate Design ergänzen (ohne CSS ist
  eine `<table>` randlos und unlesbar). Vollständige Anleitung inkl. CSS-Block
  und dem häufigen `node="[object Object]"`-Fehler:
  `tables-styling-snippet.md` (gleicher Ordner).
- **ISR** (Next.js App Router): `export const revalidate = 60` auf der
  Detail-Seite, damit neue/überarbeitete Supabase-Posts in ≤60s sichtbar sind,
  ohne pro Push einen Rebuild zu erzwingen.
- **Wenn das Repo schon einen Markdown-Renderer hat** (egal welchen), benutze
  den. Den Stack nicht umbauen.
- **Bilder im MDX-Body**: absolute Public-URLs auf
  `<supabase-url>/storage/v1/object/public/alwaysright-images/...` — funktionieren
  ohne `<Image />`-Konfig. Wenn du sie durch `next/image` ersetzt: den
  Supabase-Host in `next.config.js` unter `images.remotePatterns` whitelisten.
- **`generateStaticParams`**: über `listSlugs()`. Der DB-Pfad kann im Build leer
  sein (Env nicht in Build-Env), aber dann greift ISR + der bestehende Loader.
