Merge pull request 'feat: pages contenu bilingues' (#36) from feat/i18n-content-pages into main
This commit is contained in:
commit
df9eb5fcbd
13 changed files with 376 additions and 40 deletions
|
|
@ -0,0 +1,8 @@
|
|||
-- Plugin i18n-fr-en + content-pages :
|
||||
-- ContentPage devient bilingue → PK composite (slug, lang)
|
||||
-- pour pouvoir stocker une version FR et une version EN du même slug.
|
||||
|
||||
ALTER TABLE "ContentPage" DROP CONSTRAINT "ContentPage_pkey";
|
||||
ALTER TABLE "ContentPage" ADD CONSTRAINT "ContentPage_pkey" PRIMARY KEY ("slug", "lang");
|
||||
|
||||
CREATE INDEX IF NOT EXISTS "ContentPage_slug_idx" ON "ContentPage" ("slug");
|
||||
|
|
@ -310,10 +310,10 @@ model Plugin {
|
|||
}
|
||||
|
||||
model ContentPage {
|
||||
slug String @id
|
||||
slug String
|
||||
lang String @default("fr")
|
||||
title String
|
||||
body String
|
||||
lang String @default("fr")
|
||||
// 'general' (about, faq, ...) ou 'legal' (cgv, mentions, ...)
|
||||
category String @default("general")
|
||||
published Boolean @default(true)
|
||||
|
|
@ -321,6 +321,8 @@ model ContentPage {
|
|||
createdAt DateTime @default(now())
|
||||
updatedAt DateTime @updatedAt
|
||||
|
||||
@@id([slug, lang])
|
||||
@@index([slug])
|
||||
@@index([category])
|
||||
@@index([published])
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { getContentPage } from "@/lib/content-pages";
|
||||
import { getLocale } from "@/lib/i18n/server";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { ContentPageRenderer } from "@/components/ContentPageRenderer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const page = await getContentPage("a-propos");
|
||||
const page = await getContentPage("a-propos", await getLocale());
|
||||
return { title: page?.title ?? "À propos" };
|
||||
}
|
||||
|
||||
export default async function AboutPage() {
|
||||
if (!(await isPluginEnabled("content-pages"))) notFound();
|
||||
const page = await getContentPage("a-propos");
|
||||
const page = await getContentPage("a-propos", await getLocale());
|
||||
if (!page) notFound();
|
||||
return <ContentPageRenderer page={page} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { getContentPage } from "@/lib/content-pages";
|
||||
import { getLocale } from "@/lib/i18n/server";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { ContentPageRenderer } from "@/components/ContentPageRenderer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const page = await getContentPage("cgv");
|
||||
const page = await getContentPage("cgv", await getLocale());
|
||||
return { title: page?.title ?? "CGV" };
|
||||
}
|
||||
|
||||
export default async function CgvPage() {
|
||||
if (!(await isPluginEnabled("legal-pages"))) notFound();
|
||||
const page = await getContentPage("cgv");
|
||||
const page = await getContentPage("cgv", await getLocale());
|
||||
if (!page) notFound();
|
||||
return <ContentPageRenderer page={page} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { getContentPage } from "@/lib/content-pages";
|
||||
import { getLocale } from "@/lib/i18n/server";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { ContentPageRenderer } from "@/components/ContentPageRenderer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const page = await getContentPage("comment-ca-marche");
|
||||
const page = await getContentPage("comment-ca-marche", await getLocale());
|
||||
return { title: page?.title ?? "Comment ça marche" };
|
||||
}
|
||||
|
||||
export default async function HowItWorksPage() {
|
||||
if (!(await isPluginEnabled("content-pages"))) notFound();
|
||||
const page = await getContentPage("comment-ca-marche");
|
||||
const page = await getContentPage("comment-ca-marche", await getLocale());
|
||||
if (!page) notFound();
|
||||
return <ContentPageRenderer page={page} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { getContentPage } from "@/lib/content-pages";
|
||||
import { getLocale } from "@/lib/i18n/server";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { ContentPageRenderer } from "@/components/ContentPageRenderer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const page = await getContentPage("devenir-loueur");
|
||||
const page = await getContentPage("devenir-loueur", await getLocale());
|
||||
return { title: page?.title ?? "Devenir loueur" };
|
||||
}
|
||||
|
||||
export default async function OwnerOnboardingPage() {
|
||||
if (!(await isPluginEnabled("content-pages"))) notFound();
|
||||
const page = await getContentPage("devenir-loueur");
|
||||
const page = await getContentPage("devenir-loueur", await getLocale());
|
||||
if (!page) notFound();
|
||||
return <ContentPageRenderer page={page} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { getContentPage } from "@/lib/content-pages";
|
||||
import { getLocale } from "@/lib/i18n/server";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { ContentPageRenderer } from "@/components/ContentPageRenderer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const page = await getContentPage("faq");
|
||||
const page = await getContentPage("faq", await getLocale());
|
||||
return { title: page?.title ?? "FAQ" };
|
||||
}
|
||||
|
||||
export default async function FaqPage() {
|
||||
if (!(await isPluginEnabled("content-pages"))) notFound();
|
||||
const page = await getContentPage("faq");
|
||||
const page = await getContentPage("faq", await getLocale());
|
||||
if (!page) notFound();
|
||||
return <ContentPageRenderer page={page} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { getContentPage } from "@/lib/content-pages";
|
||||
import { getLocale } from "@/lib/i18n/server";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { ContentPageRenderer } from "@/components/ContentPageRenderer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const page = await getContentPage("mentions-legales");
|
||||
const page = await getContentPage("mentions-legales", await getLocale());
|
||||
return { title: page?.title ?? "Mentions légales" };
|
||||
}
|
||||
|
||||
export default async function MentionsPage() {
|
||||
if (!(await isPluginEnabled("legal-pages"))) notFound();
|
||||
const page = await getContentPage("mentions-legales");
|
||||
const page = await getContentPage("mentions-legales", await getLocale());
|
||||
if (!page) notFound();
|
||||
return <ContentPageRenderer page={page} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { getContentPage } from "@/lib/content-pages";
|
||||
import { getLocale } from "@/lib/i18n/server";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { ContentPageRenderer } from "@/components/ContentPageRenderer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const page = await getContentPage("politique-de-confidentialite");
|
||||
const page = await getContentPage("politique-de-confidentialite", await getLocale());
|
||||
return { title: page?.title ?? "Politique de confidentialité" };
|
||||
}
|
||||
|
||||
export default async function PrivacyPage() {
|
||||
if (!(await isPluginEnabled("legal-pages"))) notFound();
|
||||
const page = await getContentPage("politique-de-confidentialite");
|
||||
const page = await getContentPage("politique-de-confidentialite", await getLocale());
|
||||
if (!page) notFound();
|
||||
return <ContentPageRenderer page={page} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,18 +1,19 @@
|
|||
import { notFound } from "next/navigation";
|
||||
import { getContentPage } from "@/lib/content-pages";
|
||||
import { getLocale } from "@/lib/i18n/server";
|
||||
import { isPluginEnabled } from "@/lib/plugins/server";
|
||||
import { ContentPageRenderer } from "@/components/ContentPageRenderer";
|
||||
|
||||
export const dynamic = "force-dynamic";
|
||||
|
||||
export async function generateMetadata() {
|
||||
const page = await getContentPage("pour-comites-entreprise");
|
||||
const page = await getContentPage("pour-comites-entreprise", await getLocale());
|
||||
return { title: page?.title ?? "Pour comités d'entreprise" };
|
||||
}
|
||||
|
||||
export default async function CEPage() {
|
||||
if (!(await isPluginEnabled("content-pages"))) notFound();
|
||||
const page = await getContentPage("pour-comites-entreprise");
|
||||
const page = await getContentPage("pour-comites-entreprise", await getLocale());
|
||||
if (!page) notFound();
|
||||
return <ContentPageRenderer page={page} />;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,9 @@
|
|||
* Helpers Plugin content-pages / legal-pages.
|
||||
*
|
||||
* Une `ContentPage` est une page éditable (markdown léger) servie depuis la
|
||||
* table DB. Les pages sont seedées par les hooks onEnable des plugins
|
||||
* content-pages (catégorie "general") et legal-pages (catégorie "legal").
|
||||
* table DB. Clé composite (slug, lang) : une page peut exister en plusieurs
|
||||
* langues. getContentPage(slug, lang) fait un fallback sur 'fr' si la version
|
||||
* dans la langue demandée n'existe pas ou n'est pas publiée.
|
||||
*/
|
||||
|
||||
import { prisma } from "@/lib/prisma";
|
||||
|
|
@ -18,25 +19,49 @@ export type ContentPage = {
|
|||
updatedAt: Date;
|
||||
};
|
||||
|
||||
export async function getContentPage(slug: string): Promise<ContentPage | null> {
|
||||
const row = await prisma.contentPage.findUnique({ where: { slug } });
|
||||
if (!row) return null;
|
||||
if (!row.published) return null;
|
||||
return {
|
||||
slug: row.slug,
|
||||
title: row.title,
|
||||
body: row.body,
|
||||
lang: row.lang,
|
||||
category: row.category,
|
||||
published: row.published,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
export async function getContentPage(slug: string, lang: string = "fr"): Promise<ContentPage | null> {
|
||||
// Essai dans la langue demandée
|
||||
const row = await prisma.contentPage.findUnique({
|
||||
where: { slug_lang: { slug, lang } },
|
||||
});
|
||||
if (row && row.published) {
|
||||
return {
|
||||
slug: row.slug,
|
||||
title: row.title,
|
||||
body: row.body,
|
||||
lang: row.lang,
|
||||
category: row.category,
|
||||
published: row.published,
|
||||
updatedAt: row.updatedAt,
|
||||
};
|
||||
}
|
||||
// Fallback FR si autre langue manquante
|
||||
if (lang !== "fr") {
|
||||
const fallback = await prisma.contentPage.findUnique({
|
||||
where: { slug_lang: { slug, lang: "fr" } },
|
||||
});
|
||||
if (fallback && fallback.published) {
|
||||
return {
|
||||
slug: fallback.slug,
|
||||
title: fallback.title,
|
||||
body: fallback.body,
|
||||
lang: fallback.lang,
|
||||
category: fallback.category,
|
||||
published: fallback.published,
|
||||
updatedAt: fallback.updatedAt,
|
||||
};
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export async function listContentPages(category?: string): Promise<ContentPage[]> {
|
||||
export async function listContentPages(category?: string, lang?: string): Promise<ContentPage[]> {
|
||||
const where: { category?: string; lang?: string } = {};
|
||||
if (category) where.category = category;
|
||||
if (lang) where.lang = lang;
|
||||
const rows = await prisma.contentPage.findMany({
|
||||
where: category ? { category } : undefined,
|
||||
orderBy: [{ category: "asc" }, { slug: "asc" }],
|
||||
where,
|
||||
orderBy: [{ category: "asc" }, { slug: "asc" }, { lang: "asc" }],
|
||||
});
|
||||
return rows.map((r) => ({
|
||||
slug: r.slug,
|
||||
|
|
@ -58,22 +83,22 @@ export async function upsertContentPage(input: {
|
|||
published?: boolean;
|
||||
lastEditedBy?: string;
|
||||
}): Promise<ContentPage> {
|
||||
const lang = input.lang ?? "fr";
|
||||
const row = await prisma.contentPage.upsert({
|
||||
where: { slug: input.slug },
|
||||
where: { slug_lang: { slug: input.slug, lang } },
|
||||
update: {
|
||||
title: input.title,
|
||||
body: input.body,
|
||||
category: input.category ?? "general",
|
||||
lang: input.lang ?? "fr",
|
||||
published: input.published ?? true,
|
||||
lastEditedBy: input.lastEditedBy ?? null,
|
||||
},
|
||||
create: {
|
||||
slug: input.slug,
|
||||
lang,
|
||||
title: input.title,
|
||||
body: input.body,
|
||||
category: input.category ?? "general",
|
||||
lang: input.lang ?? "fr",
|
||||
published: input.published ?? true,
|
||||
lastEditedBy: input.lastEditedBy ?? null,
|
||||
},
|
||||
|
|
@ -93,9 +118,10 @@ export async function setContentPagePublished(
|
|||
slug: string,
|
||||
category: string,
|
||||
published: boolean,
|
||||
lang?: string,
|
||||
): Promise<number> {
|
||||
const result = await prisma.contentPage.updateMany({
|
||||
where: { slug, category },
|
||||
where: lang ? { slug, category, lang } : { slug, category },
|
||||
data: { published },
|
||||
});
|
||||
return result.count;
|
||||
|
|
|
|||
|
|
@ -29,6 +29,7 @@ import {
|
|||
deactivatePirogueProviders,
|
||||
seedPirogueProviders,
|
||||
} from "./seeds/pirogue-providers-default";
|
||||
import { seedEnglishContentPages } from "./seeds/content-pages-en";
|
||||
|
||||
export const pluginHooks: Record<string, PluginHookSet | undefined> = {
|
||||
"demo-carbets-seed": {
|
||||
|
|
@ -83,4 +84,13 @@ export const pluginHooks: Record<string, PluginHookSet | undefined> = {
|
|||
console.log(`[plugin pirogue-providers] disable: ${count} partenaires désactivés`);
|
||||
},
|
||||
},
|
||||
// Quand i18n est activé, on seed les pages content + legal en EN.
|
||||
// Désactiver n'efface pas les EN pages (elles dorment juste, fallback FR
|
||||
// reprend la main au prochain getContentPage).
|
||||
"i18n-fr-en": {
|
||||
onEnable: async () => {
|
||||
const count = await seedEnglishContentPages();
|
||||
console.log(`[plugin i18n-fr-en] seed: ${count} pages EN`);
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
|||
282
src/lib/plugins/seeds/content-pages-en.ts
Normal file
282
src/lib/plugins/seeds/content-pages-en.ts
Normal file
|
|
@ -0,0 +1,282 @@
|
|||
/**
|
||||
* Seeds des pages content + legal en anglais.
|
||||
* Activé conjointement avec le plugin `content-pages` / `legal-pages` quand
|
||||
* `i18n-fr-en` est aussi enabled — sinon dort en DB sans être servi.
|
||||
*/
|
||||
|
||||
import { upsertContentPage } from "@/lib/content-pages";
|
||||
|
||||
const PAGES_EN = [
|
||||
{
|
||||
slug: "a-propos",
|
||||
category: "general",
|
||||
title: "About Karbé",
|
||||
body: `## A solidarity marketplace for French Guiana's rivers
|
||||
|
||||
Karbé connects owners of riverside carbets — individuals, social committees, associations — with travellers who are looking for something other than a hotel room: a hammock, a river, silence.
|
||||
|
||||
## Why Karbé exists
|
||||
|
||||
In French Guiana, hundreds of carbets sleep six months a year. Meanwhile, travellers look for adventure and local social committees struggle to offer affordable stays to their members. Karbé bridges those two worlds — without taking a cut along the way.
|
||||
|
||||
## Our model
|
||||
|
||||
Stay payments transit Stripe and **are paid in full to the owner**: 0 % commission. Karbé is funded by an annual subscription paid by hosts who want to list their carbet — not by taking a share of what you pay.
|
||||
|
||||
## The team
|
||||
|
||||
Karbé is run by a French Guianese digital association. We are based in Cayenne and speak French, Creole, English and Portuguese.
|
||||
|
||||
[Become a host](/devenir-loueur) · [For social committees](/pour-comites-entreprise) · [Contact](mailto:bonjour@karbe.cosmolan.fr)
|
||||
`,
|
||||
},
|
||||
{
|
||||
slug: "comment-ca-marche",
|
||||
category: "general",
|
||||
title: "How it works",
|
||||
body: `Karbé lets you book a riverside carbet in three steps.
|
||||
|
||||
## 1. Find the carbet that fits you
|
||||
|
||||
On [the search page](/carbets), filter by river, dates and travellers. Each carbet is tagged by access type:
|
||||
|
||||
- 🛣️ **Road + river** — reachable by car, ideal for an easy weekend
|
||||
- 🛶 **River expedition** — pirogue only, for those who really want to sleep far away
|
||||
|
||||
## 2. Book and pay
|
||||
|
||||
Booking is online, secure payment via Stripe. You receive an email confirmation with access details, embarkation point, and (if relevant) the skipper's contact.
|
||||
|
||||
## 3. Enjoy
|
||||
|
||||
The host hands you the karbé keys (in person or via a secure box, depending on the carbet). On arrival, the hammock is up. So is the river.
|
||||
|
||||
## And then?
|
||||
|
||||
Once you're back, you can leave a review — it helps future travellers and gives visibility to serious hosts.
|
||||
|
||||
[See all available carbets](/carbets)
|
||||
`,
|
||||
},
|
||||
{
|
||||
slug: "faq",
|
||||
category: "general",
|
||||
title: "Frequently asked questions",
|
||||
body: `## Booking
|
||||
|
||||
### When is my payment charged?
|
||||
|
||||
The payment is authorised on your card at booking time and captured when the host confirms availability (or automatically within 24 h).
|
||||
|
||||
### Does Karbé take a commission?
|
||||
|
||||
**No.** The stay is billed at the exact price set by the host, who receives it in full (minus Stripe fees).
|
||||
|
||||
### What if I have to cancel?
|
||||
|
||||
Each host sets their own cancellation policy. It's shown on the carbet page before payment.
|
||||
|
||||
## On site
|
||||
|
||||
### How does pirogue transport work?
|
||||
|
||||
Depending on the carbet, the host provides the skipper, refers you to a partner, or lets you arrange it yourself. The details are on each page.
|
||||
|
||||
### Is there mobile coverage?
|
||||
|
||||
On the upper Maroni, Oyapock and Approuague rivers: **no**. Download your offline maps before leaving.`,
|
||||
},
|
||||
{
|
||||
slug: "pour-comites-entreprise",
|
||||
category: "general",
|
||||
title: "For social committees",
|
||||
body: `## Your carbets sleep six months a year. Let's share them.
|
||||
|
||||
Many social committees already own a carbet — funded by member dues, reserved as a priority for staff, but often empty between weekends. Karbé lets you open those quiet periods to public travellers, with zero commission and no extra admin.
|
||||
|
||||
## How it works
|
||||
|
||||
1. You list your carbet on Karbé. Free for associations and committees.
|
||||
2. You block, as a priority, the dates reserved for your members.
|
||||
3. The remaining dates are offered to public travellers.
|
||||
4. Payment is collected by Stripe and remitted directly to your account.
|
||||
|
||||
## Benefits
|
||||
|
||||
- **Extra revenue**: public bookings fund maintenance.
|
||||
- **0 % commission**: Karbé takes nothing on stays.
|
||||
- **No paperwork**: Stripe collects and remits, no middleman.
|
||||
- **Members keep priority**: a dual-calendar system separates committee and public slots.
|
||||
|
||||
## Next step
|
||||
|
||||
Contact us to discuss your specific case: [bonjour@karbe.cosmolan.fr](mailto:bonjour@karbe.cosmolan.fr).
|
||||
`,
|
||||
},
|
||||
{
|
||||
slug: "devenir-loueur",
|
||||
category: "general",
|
||||
title: "Become a host",
|
||||
body: `Got a carbet? You can list it on Karbé in a few minutes.
|
||||
|
||||
## What you'll need
|
||||
|
||||
- Proof of ownership or written permission
|
||||
- Photos (interior, exterior, embarkation point)
|
||||
- Honest description: amenities, access, seasonal constraints
|
||||
- A clear **cancellation policy** travellers can see before paying
|
||||
|
||||
## Steps
|
||||
|
||||
1. [Create a host account](/connexion) with your email and phone.
|
||||
2. Add your carbet: photos, description, GPS, embarkation point, pirogue time (if relevant), amenities.
|
||||
3. Open your calendar: which weeks are available, what seasonal constraints apply (Oyapock low water? dry season only?).
|
||||
4. Activate the annual host subscription via Stripe.
|
||||
5. You're online. Bookings come in by email.
|
||||
|
||||
## Payment
|
||||
|
||||
When a traveller books, Stripe collects the stay and **pays it to you in full**. Karbé takes nothing.
|
||||
|
||||
## Questions
|
||||
|
||||
For edge cases (jointly owned carbet, ZAD plot, agreement with a town hall…), write to us: [bonjour@karbe.cosmolan.fr](mailto:bonjour@karbe.cosmolan.fr).
|
||||
|
||||
[Create my host account](/connexion)
|
||||
`,
|
||||
},
|
||||
{
|
||||
slug: "cgv",
|
||||
category: "legal",
|
||||
title: "Terms of service",
|
||||
body: `*Last updated: 2026-05-31*
|
||||
|
||||
These terms govern the use of the Karbé platform (karbe.cosmolan.fr) operated by the project's umbrella association.
|
||||
|
||||
## 1. Object
|
||||
|
||||
Karbé is a platform connecting owners of riverside carbets located in French Guiana with travellers wishing to rent them for tourism stays. Karbé does not own the carbets and is not a party to the rental contract.
|
||||
|
||||
## 2. Registration
|
||||
|
||||
Registration is free for travellers. For hosts, it is conditional upon subscribing to an annual subscription billed via Stripe.
|
||||
|
||||
## 3. Payment and remittance
|
||||
|
||||
Stays are billed via Stripe at booking time. The amount is **paid in full to the host** (minus Stripe fees), with no Karbé commission.
|
||||
|
||||
## 4. Host subscription
|
||||
|
||||
The annual subscription is billed at the start of the period and tacitly renewed unless cancelled by the user.
|
||||
|
||||
## 5. Cancellations
|
||||
|
||||
Each host sets their own cancellation policy, shown before payment.
|
||||
|
||||
## 6. Liability
|
||||
|
||||
Karbé is not liable for the state of the carbet, the pirogue transport, river conditions, or any incident during the stay. The traveller is invited to take out a suitable travel insurance.
|
||||
|
||||
## 7. Personal data
|
||||
|
||||
See the [Privacy Policy](/politique-de-confidentialite).
|
||||
|
||||
## 8. Applicable law
|
||||
|
||||
French law. Court of Cayenne (French Guiana).
|
||||
|
||||
## 9. Contact
|
||||
|
||||
[bonjour@karbe.cosmolan.fr](mailto:bonjour@karbe.cosmolan.fr)
|
||||
`,
|
||||
},
|
||||
{
|
||||
slug: "mentions-legales",
|
||||
category: "legal",
|
||||
title: "Legal notice",
|
||||
body: `*Last updated: 2026-05-31*
|
||||
|
||||
## Publisher
|
||||
|
||||
**Karbé** is published by the project's umbrella association (under formation), based in Cayenne (French Guiana).
|
||||
|
||||
- Email: [bonjour@karbe.cosmolan.fr](mailto:bonjour@karbe.cosmolan.fr)
|
||||
- Site: [karbe.cosmolan.fr](https://karbe.cosmolan.fr)
|
||||
|
||||
## Hosting
|
||||
|
||||
Servers located in mainland France, operated by the Cosmolan infrastructure.
|
||||
|
||||
## Source code
|
||||
|
||||
Open source: [git.cosmolan.fr/tarzzan/karbe](https://git.cosmolan.fr/tarzzan/karbe).
|
||||
|
||||
## Intellectual property
|
||||
|
||||
Editorial content of carbet listings belongs to the hosts who published them. The Karbé brand and proprietary visual elements remain the property of the publisher.
|
||||
|
||||
## Reporting
|
||||
|
||||
[bonjour@karbe.cosmolan.fr](mailto:bonjour@karbe.cosmolan.fr)
|
||||
`,
|
||||
},
|
||||
{
|
||||
slug: "politique-de-confidentialite",
|
||||
category: "legal",
|
||||
title: "Privacy policy",
|
||||
body: `*Last updated: 2026-05-31*
|
||||
|
||||
Karbé respects your privacy.
|
||||
|
||||
## 1. Data collected
|
||||
|
||||
- **Identification**: email, last name, first name, phone
|
||||
- **Booking**: dates, carbet, amount paid
|
||||
- **Technical**: IP, browser, pages visited (fraud prevention)
|
||||
|
||||
We **do not collect** your bank card: payments are delegated to Stripe (PCI-DSS).
|
||||
|
||||
## 2. Purposes
|
||||
|
||||
- Traveller-host matching
|
||||
- Subscription billing
|
||||
- Fraud prevention
|
||||
- Account / booking communication
|
||||
|
||||
## 3. Retention
|
||||
|
||||
- Active account: as long as it's used
|
||||
- Accounting data: 10 years
|
||||
- Technical logs: 12 months max
|
||||
|
||||
## 4. Recipients
|
||||
|
||||
Karbé team, Stripe, host (for your stay). **No resale.**
|
||||
|
||||
## 5. Your rights
|
||||
|
||||
Right of access, rectification, erasure, portability, objection.
|
||||
|
||||
Contact: [bonjour@karbe.cosmolan.fr](mailto:bonjour@karbe.cosmolan.fr) · CNIL: [cnil.fr](https://www.cnil.fr)
|
||||
|
||||
## 6. Cookies
|
||||
|
||||
Technical cookies only (session, preferences). No advertising or third-party tracking cookies.`,
|
||||
},
|
||||
];
|
||||
|
||||
export async function seedEnglishContentPages(): Promise<number> {
|
||||
let count = 0;
|
||||
for (const page of PAGES_EN) {
|
||||
await upsertContentPage({
|
||||
slug: page.slug,
|
||||
title: page.title,
|
||||
body: page.body,
|
||||
category: page.category,
|
||||
lang: "en",
|
||||
published: true,
|
||||
});
|
||||
count += 1;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue