@fraza/start 0.0.0-stage → 0.3.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +186 -0
- package/LICENSE +21 -0
- package/README.md +97 -2
- package/THIRD-PARTY-NOTICES.md +11 -0
- package/bin/agents-block.md +61 -0
- package/bin/cli.js +197 -0
- package/bin/project.js +281 -0
- package/bin/version.js +2 -0
- package/changelog.json +602 -0
- package/package.json +42 -4
- package/templates/app/(admin)/admin/[...admin]/layout.js +3 -0
- package/templates/app/(admin)/admin/[...admin]/loading.js +3 -0
- package/templates/app/(admin)/admin/[...admin]/page.js +1 -0
- package/templates/app/(admin)/admin/error.js +4 -0
- package/templates/app/(admin)/admin/layout.js +17 -0
- package/templates/app/(admin)/admin/not-found.js +2 -0
- package/templates/app/(admin)/admin/page.js +5 -0
- package/templates/app/(auth)/auth/[...auth]/page.js +1 -0
- package/templates/app/(auth)/auth/page.js +5 -0
- package/templates/app/(public)/[...slug]/page.js +29 -0
- package/templates/app/(public)/error.js +6 -0
- package/templates/app/(public)/layout.js +17 -0
- package/templates/app/(public)/not-found.js +20 -0
- package/templates/app/(public)/page.js +248 -0
- package/templates/app/.fraza/blocks.client.js +8 -0
- package/templates/app/.fraza/blocks.server.js +4 -0
- package/templates/app/.fraza/modules.client.js +6 -0
- package/templates/app/.fraza/modules.generated.js +3 -0
- package/templates/app/.fraza/public-pages.server.js +4 -0
- package/templates/app/.fraza/sources.css +2 -0
- package/templates/app/.fraza/templates.server.js +5 -0
- package/templates/app/global-error.js +5 -0
- package/templates/app/icon.svg +1 -0
- package/templates/app/layout.js +31 -0
- package/templates/app/za/[...path]/not-found.js +6 -0
- package/templates/app/za/[...path]/page.js +8 -0
- package/templates/app/za/api/[...path]/route.js +1 -0
- package/templates/app/za/media/[filename]/route.js +1 -0
- package/templates/components/fraza/blocks/README.md +207 -0
- package/templates/components/fraza/blocks/accordion/fields.js +19 -0
- package/templates/components/fraza/blocks/accordion/render.js +47 -0
- package/templates/components/fraza/blocks/accordion/template.js +28 -0
- package/templates/components/fraza/blocks/callout/fields.js +15 -0
- package/templates/components/fraza/blocks/callout/render.js +28 -0
- package/templates/components/fraza/blocks/callout/template.js +37 -0
- package/templates/components/fraza/blocks/icon-list/fields.js +20 -0
- package/templates/components/fraza/blocks/icon-list/render.js +44 -0
- package/templates/components/fraza/blocks/icon-list/template.js +89 -0
- package/templates/components/fraza/blocks/three-cards/fields.js +19 -0
- package/templates/components/fraza/blocks/three-cards/render.js +33 -0
- package/templates/components/fraza/blocks/three-cards/template.js +57 -0
- package/templates/components/fraza/templates/README.md +293 -0
- package/templates/components/fraza/templates/content.js +271 -0
- package/templates/components/fraza/templates/default.js +18 -0
- package/templates/components/fraza/templates/post-blogue.js +97 -0
- package/templates/instrumentation.js +21 -0
- package/templates/jsconfig.json +9 -0
- package/templates/styles/globals.css +25 -0
- package/templates/theme.js +25 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { default, generateMetadata } from '@fraza/core/features/auth/server';
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
// Catch-all CMS : toute route non interceptée par un fichier statique
|
|
2
|
+
// (admin, auth, /api/*, _next/*, et toute page écrite manuellement dans
|
|
3
|
+
// (public)/) est dispatchée au registre `@fraza/core/public-content`.
|
|
4
|
+
// Chaque module CMS s'enregistre avec une priorité : `module-posts` réserve
|
|
5
|
+
// les URLs `/<postType>/<slug>` (priorité 10), `module-pages` sert le reste
|
|
6
|
+
// (priorité 0, fallback hiérarchique).
|
|
7
|
+
//
|
|
8
|
+
// Pour ajouter un nouveau module CMS, il suffit qu'il appelle
|
|
9
|
+
// `registerPublicContentResolver` à son boot, sans toucher au starter.
|
|
10
|
+
// Pour fournir un template custom (avec ton branding), dépose un fichier
|
|
11
|
+
// dans `components/fraza/templates/` puis lance `npx fraza-modules sync`.
|
|
12
|
+
|
|
13
|
+
import { notFound } from 'next/navigation';
|
|
14
|
+
import { resolvePublicContent, resolvePublicMetadata } from '@fraza/core/public-content';
|
|
15
|
+
|
|
16
|
+
// `[...slug]` n'est pas optionnel : `/` est servi par `(public)/page.js`, et
|
|
17
|
+
// `slug` arrive donc toujours sous forme d'un tableau non vide.
|
|
18
|
+
export default async function PublicCatchAll({ params }) {
|
|
19
|
+
const { slug } = await params;
|
|
20
|
+
const match = await resolvePublicContent(slug);
|
|
21
|
+
if (!match) notFound();
|
|
22
|
+
const { Component, props } = match;
|
|
23
|
+
return <Component {...props} />;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export async function generateMetadata({ params }) {
|
|
27
|
+
const { slug } = await params;
|
|
28
|
+
return (await resolvePublicMetadata(slug)) || {};
|
|
29
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { getPublicBrand } from "@fraza/core/shared/config";
|
|
2
|
+
|
|
3
|
+
// Titre par défaut des pages publiques : le nom du site, `FRAZA_NAME`. Une page
|
|
4
|
+
// qui déclare son propre titre le remplace. Sans `FRAZA_NAME`, aucun titre
|
|
5
|
+
// n'est inventé : un nom par défaut signerait le site de quelqu'un d'autre.
|
|
6
|
+
export function generateMetadata() {
|
|
7
|
+
const { name } = getPublicBrand();
|
|
8
|
+
return name ? { title: name } : {};
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export default function layout({ children }) {
|
|
12
|
+
return (
|
|
13
|
+
<>
|
|
14
|
+
{children}
|
|
15
|
+
</>
|
|
16
|
+
);
|
|
17
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import Button from "@fraza/core/shared/components/Button";
|
|
2
|
+
|
|
3
|
+
// Page 404 du site public. Comme la page d'accueil, c'est un gabarit : elle se
|
|
4
|
+
// remplace dès que le site a sa propre allure.
|
|
5
|
+
export default function NotFound() {
|
|
6
|
+
return (
|
|
7
|
+
<main className="flex min-h-screen items-center justify-center bg-canvas px-4 font-ui text-fg">
|
|
8
|
+
<div className="flex max-w-sm flex-col items-center gap-1 text-center">
|
|
9
|
+
<p className="font-code text-[12.5px] text-fg-subtle">404</p>
|
|
10
|
+
<h1 className="text-2xl font-bold tracking-[-0.03em]">Page introuvable</h1>
|
|
11
|
+
<p className="text-[13px] leading-relaxed text-fg-muted">
|
|
12
|
+
Cette page n'existe pas ou n'est plus en ligne.
|
|
13
|
+
</p>
|
|
14
|
+
<Button href="/" variant="link" className="mt-3">
|
|
15
|
+
Retour à l'accueil →
|
|
16
|
+
</Button>
|
|
17
|
+
</div>
|
|
18
|
+
</main>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import { connection } from "next/server";
|
|
2
|
+
import Button from "@fraza/core/shared/components/Button";
|
|
3
|
+
import FrazaLogo from "@fraza/core/shared/components/FrazaLogo";
|
|
4
|
+
import { tableExists } from "@fraza/core/database";
|
|
5
|
+
import { countUsers } from "@fraza/core/users";
|
|
6
|
+
|
|
7
|
+
// Point de départ du site public. Cette page est un gabarit : elle se remplace
|
|
8
|
+
// entièrement dès que le site prend forme. Elle ne déclare pas de métadonnées :
|
|
9
|
+
// son titre est celui du layout public, le nom du site (`FRAZA_NAME`).
|
|
10
|
+
//
|
|
11
|
+
// Elle suit la mise en route du projet : chaque étape est lue à chaque
|
|
12
|
+
// chargement (variables, tables, premier compte), et seule la première étape
|
|
13
|
+
// non faite est détaillée.
|
|
14
|
+
|
|
15
|
+
const files = [
|
|
16
|
+
"app/(public)/page.js",
|
|
17
|
+
"app/(public)/layout.js",
|
|
18
|
+
"components/fraza/templates/",
|
|
19
|
+
"components/fraza/blocks/",
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
// Code en ligne : ce qui se copie (commande, fichier, chemin) passe en mono.
|
|
23
|
+
const chip = "rounded-item bg-surface-muted px-2 py-0.5 font-code text-xs text-fg-secondary";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* État de la mise en route, lu à chaque requête. La base n'est interrogée que
|
|
27
|
+
* si ses variables sont posées : sans elles, le pool lèverait une erreur.
|
|
28
|
+
*/
|
|
29
|
+
async function readSetup() {
|
|
30
|
+
const env = {
|
|
31
|
+
database: Boolean(process.env.FRAZA_DATABASE_URL),
|
|
32
|
+
encryptionKey: Boolean(process.env.FRAZA_ENCRYPTION_KEY),
|
|
33
|
+
};
|
|
34
|
+
const state = { env, envReady: env.database && env.encryptionKey, tables: false, users: 0, unreachable: false };
|
|
35
|
+
if (!state.envReady) return state;
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
state.tables = await tableExists("fraza_auth_users");
|
|
39
|
+
if (state.tables) state.users = await countUsers();
|
|
40
|
+
} catch {
|
|
41
|
+
state.unreachable = true;
|
|
42
|
+
}
|
|
43
|
+
return state;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function StepMarker({ status, number }) {
|
|
47
|
+
if (status === "done") {
|
|
48
|
+
return (
|
|
49
|
+
<span className="flex size-5 shrink-0 items-center justify-center rounded-full bg-success-soft text-success">
|
|
50
|
+
<svg width="12" height="12" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2.4" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
|
|
51
|
+
<path d="M5 12.5l4.5 4.5L19 7.5" />
|
|
52
|
+
</svg>
|
|
53
|
+
</span>
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
if (status === "current") {
|
|
57
|
+
return (
|
|
58
|
+
<span className="flex size-5 shrink-0 items-center justify-center rounded-full border-2 border-accent text-[11px] font-semibold text-accent">
|
|
59
|
+
{number}
|
|
60
|
+
</span>
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
return (
|
|
64
|
+
<span className="flex size-5 shrink-0 items-center justify-center rounded-full border border-line-strong text-[11px] font-medium text-fg-muted">
|
|
65
|
+
{number}
|
|
66
|
+
</span>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function EnvRow({ name, filled }) {
|
|
71
|
+
return (
|
|
72
|
+
<div className="flex items-center justify-between gap-3 border-b border-line-soft px-3 py-2 last:border-b-0">
|
|
73
|
+
<span className="font-code text-[12.5px] text-fg">{name}</span>
|
|
74
|
+
<span className={`text-xs font-medium ${filled ? "text-success" : "text-fg-muted"}`}>
|
|
75
|
+
{filled ? "Renseignée" : "Vide"}
|
|
76
|
+
</span>
|
|
77
|
+
</div>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export default async function Page() {
|
|
82
|
+
// Lue à chaque requête : l'état change pendant que le développeur avance.
|
|
83
|
+
await connection();
|
|
84
|
+
const setup = await readSetup();
|
|
85
|
+
|
|
86
|
+
const steps = [
|
|
87
|
+
{ title: "Créer le projet", done: true },
|
|
88
|
+
{
|
|
89
|
+
title: "Remplir le .env.local",
|
|
90
|
+
done: setup.envReady,
|
|
91
|
+
detail: (
|
|
92
|
+
<>
|
|
93
|
+
<p className="text-[12.5px] leading-relaxed text-fg-secondary">
|
|
94
|
+
Les valeurs restent sur cette machine : git ignore le fichier.
|
|
95
|
+
</p>
|
|
96
|
+
<div className="rounded-control border border-line bg-surface">
|
|
97
|
+
<EnvRow name="FRAZA_DATABASE_URL" filled={setup.env.database} />
|
|
98
|
+
<EnvRow name="FRAZA_ENCRYPTION_KEY" filled={setup.env.encryptionKey} />
|
|
99
|
+
</div>
|
|
100
|
+
</>
|
|
101
|
+
),
|
|
102
|
+
},
|
|
103
|
+
{
|
|
104
|
+
title: "Initialiser la base",
|
|
105
|
+
done: setup.tables,
|
|
106
|
+
hint: <code className="font-code text-[12.5px]">npx fraza-db init</code>,
|
|
107
|
+
detail: setup.unreachable ? (
|
|
108
|
+
<p className="text-[12.5px] leading-relaxed text-fg-secondary">
|
|
109
|
+
La base ne répond pas. Vérifiez <code className={chip}>FRAZA_DATABASE_URL</code>, puis
|
|
110
|
+
lancez <code className={chip}>npx fraza-db init</code>.
|
|
111
|
+
</p>
|
|
112
|
+
) : (
|
|
113
|
+
<p className="text-[12.5px] leading-relaxed text-fg-secondary">
|
|
114
|
+
Crée les tables du core et des modules installés :{" "}
|
|
115
|
+
<code className={chip}>npx fraza-db init</code>
|
|
116
|
+
</p>
|
|
117
|
+
),
|
|
118
|
+
},
|
|
119
|
+
{
|
|
120
|
+
title: "Créer le premier compte",
|
|
121
|
+
done: setup.users > 0,
|
|
122
|
+
hint: "Sur /auth. Le premier compte reçoit le rôle admin.",
|
|
123
|
+
detail: (
|
|
124
|
+
<p className="text-[12.5px] leading-relaxed text-fg-secondary">
|
|
125
|
+
Sur /auth. Le premier compte reçoit le rôle admin et ouvre l'administration.
|
|
126
|
+
</p>
|
|
127
|
+
),
|
|
128
|
+
},
|
|
129
|
+
];
|
|
130
|
+
|
|
131
|
+
const currentIndex = steps.findIndex((step) => !step.done);
|
|
132
|
+
const doneCount = steps.filter((step) => step.done).length;
|
|
133
|
+
const remaining = steps.length - doneCount;
|
|
134
|
+
const ready = currentIndex === -1;
|
|
135
|
+
|
|
136
|
+
return (
|
|
137
|
+
<main className="flex min-h-screen items-center justify-center bg-canvas px-6 py-18 font-ui text-fg">
|
|
138
|
+
<div className="flex w-full max-w-[1040px] flex-wrap items-center gap-x-18 gap-y-12">
|
|
139
|
+
|
|
140
|
+
<section className="flex flex-[1_1_360px] flex-col gap-6">
|
|
141
|
+
<FrazaLogo className="h-5 w-auto self-start text-fg" />
|
|
142
|
+
|
|
143
|
+
<div className="flex flex-col gap-2.5">
|
|
144
|
+
{/* Titre d'écran de la charte, comme les pages publiques des modules. */}
|
|
145
|
+
<h1 className="text-[32px] leading-tight font-bold tracking-[-0.035em]">
|
|
146
|
+
Votre projet est prêt.
|
|
147
|
+
</h1>
|
|
148
|
+
<p className="text-sm leading-relaxed text-fg-muted">
|
|
149
|
+
{ready
|
|
150
|
+
? "Tout est en place. Le site public part de cette page : remplacez-la quand il prend forme."
|
|
151
|
+
: `Encore ${remaining} ${remaining > 1 ? "étapes" : "étape"} avant d'ouvrir l'administration. Cette page se met à jour à mesure que vous avancez.`}
|
|
152
|
+
</p>
|
|
153
|
+
</div>
|
|
154
|
+
|
|
155
|
+
<div className="flex flex-col gap-2.5 border-t border-line pt-5">
|
|
156
|
+
<h2 className="text-[12.5px] font-semibold">Ensuite, le site public</h2>
|
|
157
|
+
<p className="text-[12.5px] leading-relaxed text-fg-muted">
|
|
158
|
+
Cette page est un gabarit. Remplacez-la quand le site prend forme.
|
|
159
|
+
</p>
|
|
160
|
+
<ul className="flex flex-wrap gap-1.5">
|
|
161
|
+
{files.map((file) => (
|
|
162
|
+
<li key={file}>
|
|
163
|
+
<code className={chip}>{file}</code>
|
|
164
|
+
</li>
|
|
165
|
+
))}
|
|
166
|
+
</ul>
|
|
167
|
+
</div>
|
|
168
|
+
|
|
169
|
+
<p className="text-xs text-fg-muted">
|
|
170
|
+
Livrée par @fraza/start ·{" "}
|
|
171
|
+
<Button href="https://docs.fraza.ca" target="_blank" variant="link" size="sm">
|
|
172
|
+
docs.fraza.ca
|
|
173
|
+
</Button>
|
|
174
|
+
</p>
|
|
175
|
+
</section>
|
|
176
|
+
|
|
177
|
+
<section className="min-w-0 flex-[1_1_460px] overflow-hidden rounded-card border border-line bg-surface">
|
|
178
|
+
<div className="flex items-center gap-3 px-6 py-4">
|
|
179
|
+
<h2 className="text-[15px] font-semibold">Mise en route</h2>
|
|
180
|
+
<div className="h-1 flex-1 overflow-hidden rounded-full bg-surface-muted">
|
|
181
|
+
<div
|
|
182
|
+
className="h-full rounded-full bg-accent"
|
|
183
|
+
style={{ width: `${(doneCount / steps.length) * 100}%` }}
|
|
184
|
+
/>
|
|
185
|
+
</div>
|
|
186
|
+
<span className="text-xs text-fg-muted tabular-nums">
|
|
187
|
+
{doneCount} sur {steps.length}
|
|
188
|
+
</span>
|
|
189
|
+
</div>
|
|
190
|
+
|
|
191
|
+
<ol>
|
|
192
|
+
{steps.map((step, index) => {
|
|
193
|
+
const status = step.done ? "done" : index === currentIndex ? "current" : "todo";
|
|
194
|
+
return (
|
|
195
|
+
<li
|
|
196
|
+
key={step.title}
|
|
197
|
+
aria-current={status === "current" ? "step" : undefined}
|
|
198
|
+
className={`flex gap-3.5 border-t border-line-soft px-6 ${status === "current" ? "bg-accent-soft py-4" : "py-3.5"}`}
|
|
199
|
+
>
|
|
200
|
+
<StepMarker status={status} number={index + 1} />
|
|
201
|
+
<div className="flex min-w-0 flex-1 flex-col gap-2.5">
|
|
202
|
+
<div className="flex flex-col gap-0.5">
|
|
203
|
+
<span
|
|
204
|
+
className={`text-[13.5px] ${status === "current" ? "font-semibold" : "font-medium"} ${status === "done" ? "text-fg-muted" : "text-fg"}`}
|
|
205
|
+
>
|
|
206
|
+
{step.title}
|
|
207
|
+
</span>
|
|
208
|
+
{status === "todo" && step.hint && (
|
|
209
|
+
<span className="text-[12.5px] text-fg-muted">{step.hint}</span>
|
|
210
|
+
)}
|
|
211
|
+
</div>
|
|
212
|
+
{status === "current" && step.detail}
|
|
213
|
+
</div>
|
|
214
|
+
</li>
|
|
215
|
+
);
|
|
216
|
+
})}
|
|
217
|
+
</ol>
|
|
218
|
+
|
|
219
|
+
<div className="flex flex-wrap items-center justify-between gap-3 border-t border-line px-6 py-3.5">
|
|
220
|
+
<Button
|
|
221
|
+
href="https://docs.fraza.ca/docs/demarrage/installation"
|
|
222
|
+
target="_blank"
|
|
223
|
+
variant="link"
|
|
224
|
+
size="sm"
|
|
225
|
+
>
|
|
226
|
+
Guide d'installation ↗
|
|
227
|
+
</Button>
|
|
228
|
+
<div className="flex gap-2">
|
|
229
|
+
{!ready && (
|
|
230
|
+
<Button href="/" variant="secondary">
|
|
231
|
+
Revérifier
|
|
232
|
+
</Button>
|
|
233
|
+
)}
|
|
234
|
+
{ready ? (
|
|
235
|
+
<Button href="/admin">Ouvrir l'administration</Button>
|
|
236
|
+
) : currentIndex === steps.length - 1 ? (
|
|
237
|
+
<Button href="/auth">Créer le premier compte</Button>
|
|
238
|
+
) : (
|
|
239
|
+
<Button disabled>Ouvrir l'administration</Button>
|
|
240
|
+
)}
|
|
241
|
+
</div>
|
|
242
|
+
</div>
|
|
243
|
+
</section>
|
|
244
|
+
|
|
245
|
+
</div>
|
|
246
|
+
</main>
|
|
247
|
+
);
|
|
248
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 48 48" width="48" height="48"><rect width="48" height="48" rx="11" fill="#0a0a0a"/><g transform="translate(9 9) scale(0.625)" fill="#fafafa"><path d="M17 9H40Q46 9 43.5 14.4L40 22H11Z"/><path d="M8 26H37L31 39H8Q2 39 4.5 33.6Z"/></g></svg>
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import "@styles/globals.css";
|
|
2
|
+
// Side-effect : enregistre les blocs personnalisés dans le registre RSC serveur
|
|
3
|
+
// (composants `render.js`). Importé ici, et pas seulement via
|
|
4
|
+
// instrumentation.js, pour que les `render.js` marqués `'use client'`
|
|
5
|
+
// traversent le compilateur RSC et soient référencés dans le React Client
|
|
6
|
+
// Manifest. Sans cet import, un bloc au render client échoue avec
|
|
7
|
+
// « Could not find the module … in the React Client Manifest » au rendu.
|
|
8
|
+
import './.fraza/blocks.server.js';
|
|
9
|
+
// Side-effect : enregistre les templates publics dans le registre RSC.
|
|
10
|
+
// Importé ici (et non uniquement via instrumentation.js) pour que les
|
|
11
|
+
// fichiers `components/fraza/templates/*.js` traversent le compilateur RSC
|
|
12
|
+
// et que leurs `next/link` / `next/image` soient correctement référencés
|
|
13
|
+
// dans le React Client Manifest.
|
|
14
|
+
import './.fraza/templates.server.js';
|
|
15
|
+
import { FrazaProvider } from '@fraza/core/features/provider';
|
|
16
|
+
import { ThemeStyle } from '@fraza/core/themes/server';
|
|
17
|
+
|
|
18
|
+
export default function RootLayout({ children }) {
|
|
19
|
+
return (
|
|
20
|
+
<html lang="fr" suppressHydrationWarning>
|
|
21
|
+
<head>
|
|
22
|
+
<ThemeStyle />
|
|
23
|
+
</head>
|
|
24
|
+
<body className="antialiased">
|
|
25
|
+
<FrazaProvider>
|
|
26
|
+
{children}
|
|
27
|
+
</FrazaProvider>
|
|
28
|
+
</body>
|
|
29
|
+
</html>
|
|
30
|
+
);
|
|
31
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
// 404 des pages publiques des modules (lien de facture expiré, formulaire
|
|
2
|
+
// inactif, réservation fermée). Cette route vit hors du groupe `(public)`,
|
|
3
|
+
// dont la page 404 ne s'applique donc pas ici : sans ce fichier, Next.js
|
|
4
|
+
// afficherait sa page par défaut, en anglais et sans le thème. On reprend la
|
|
5
|
+
// 404 du site public pour qu'il n'y en ait qu'une à personnaliser.
|
|
6
|
+
export { default } from '../../(public)/not-found.js';
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
// Side-effect : fait entrer les pages publiques des modules dans le graphe
|
|
2
|
+
// RSC de cette route, pour que leurs composants `'use client'` soient
|
|
3
|
+
// référencés dans le React Client Manifest. Sans cet import, une page publique
|
|
4
|
+
// de module échoue avec « Could not find the module … in the React Client
|
|
5
|
+
// Manifest ».
|
|
6
|
+
import '../../.fraza/public-pages.server.js';
|
|
7
|
+
|
|
8
|
+
export { default, generateMetadata } from '@fraza/core/public-pages/server';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GET, POST, PUT, DELETE, PATCH } from '@fraza/core/api/handler';
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { GET } from '@fraza/core/media-registry/file-route';
|
|
@@ -0,0 +1,207 @@
|
|
|
1
|
+
# Blocs personnalisés : `components/fraza/blocks/`
|
|
2
|
+
|
|
3
|
+
Ce dossier contient les **blocs personnalisés** que tu ajoutes au
|
|
4
|
+
`BlockEditor` du CMS. Un sous-dossier = un bloc. `npx fraza-modules sync` les
|
|
5
|
+
trouve et les rend disponibles dans le menu `/` de tous les éditeurs (pages
|
|
6
|
+
CMS, posts, etc.).
|
|
7
|
+
|
|
8
|
+
## Anatomie d'un bloc
|
|
9
|
+
|
|
10
|
+
Un bloc est un dossier avec **3 fichiers** :
|
|
11
|
+
|
|
12
|
+
```
|
|
13
|
+
components/fraza/blocks/<id>/
|
|
14
|
+
├── fields.js # métadonnées statiques (lues sans React)
|
|
15
|
+
├── template.js # composant d'édition, affiché dans l'administration
|
|
16
|
+
└── render.js # composant public, affiché sur le site
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
### `fields.js` : métadonnées
|
|
20
|
+
|
|
21
|
+
Exporte deux objets : `meta` (description du bloc pour le menu `/`) et
|
|
22
|
+
`defaults` (valeurs initiales d'une nouvelle instance).
|
|
23
|
+
|
|
24
|
+
```js
|
|
25
|
+
export const meta = {
|
|
26
|
+
id: 'mon-bloc', // unique dans le projet, [a-z0-9-] uniquement
|
|
27
|
+
label: 'Mon bloc', // libellé affiché dans le menu /
|
|
28
|
+
icon: 'LayersIcon', // nom d'une icône de @fraza/icons
|
|
29
|
+
keywords: ['custom', 'demo'], // termes de recherche
|
|
30
|
+
category: 'Contenu', // facultatif, regroupement
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
export const defaults = {
|
|
34
|
+
title: '',
|
|
35
|
+
// toutes les valeurs initiales du bloc...
|
|
36
|
+
};
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
> **Pas d'import React ni de JSX dans `fields.js`** : ce fichier est lu côté
|
|
40
|
+
> serveur et côté client au démarrage, pour valider et configurer le
|
|
41
|
+
> registre. Les imports lourds y sont inutiles et ralentissent le démarrage.
|
|
42
|
+
|
|
43
|
+
### `template.js` : éditeur de l'administration
|
|
44
|
+
|
|
45
|
+
Composant React qui rend le bloc **dans l'éditeur**. Il doit porter
|
|
46
|
+
`'use client'`. Il reçoit `{ block, onChange, disabled }` :
|
|
47
|
+
|
|
48
|
+
- `block` : l'instance courante (fusionnée avec `defaults`) ;
|
|
49
|
+
- `onChange(patch)` : applique une modification partielle. Passe **seulement
|
|
50
|
+
les champs modifiés**, pas le bloc complet ;
|
|
51
|
+
- `disabled` : `true` en lecture seule (aperçu).
|
|
52
|
+
|
|
53
|
+
`@fraza/core/blocks-registry/fields-ui` fournit des champs prêts à l'emploi,
|
|
54
|
+
pour garder le même aspect que le reste de l'administration :
|
|
55
|
+
|
|
56
|
+
| Composant | Usage |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `<TextField>` | champ texte |
|
|
59
|
+
| `<TextareaField>` | texte sur plusieurs lignes |
|
|
60
|
+
| `<NumberField>` | champ numérique |
|
|
61
|
+
| `<SelectField>` | menu déroulant |
|
|
62
|
+
| `<BooleanField>` | case à cocher |
|
|
63
|
+
| `<ColorField>` | sélecteur de couleur Fraza |
|
|
64
|
+
| `<IconField>` | sélecteur d'icône parmi la liste passée par `icons` (fenêtre avec recherche ; enregistre le nom de l'icône) |
|
|
65
|
+
| `<ImageField>` | sélecteur d'image (avec `MediaPicker` si module-medias est installé) |
|
|
66
|
+
| `<FrazaBlockShell>` | conteneur recommandé (cadre pointillé neutre) |
|
|
67
|
+
|
|
68
|
+
Tu peux aussi tout écrire à la main si ces champs ne suffisent pas : le seul
|
|
69
|
+
contrat est `default export = composant React`.
|
|
70
|
+
|
|
71
|
+
> **Style** : volontairement **neutre** (gris, noir, blanc, aucune classe
|
|
72
|
+
> propre au site). L'administration reste ainsi homogène, quel que soit le
|
|
73
|
+
> style du site public.
|
|
74
|
+
|
|
75
|
+
### `render.js` : rendu public
|
|
76
|
+
|
|
77
|
+
Composant React qui rend le bloc **sur le site public**, à la visite. Il
|
|
78
|
+
reçoit `{ block }`.
|
|
79
|
+
|
|
80
|
+
- Il utilise les classes et les styles **du site** (Tailwind, jetons du
|
|
81
|
+
thème…).
|
|
82
|
+
- Il n'a pas de `onChange` : ce composant est en lecture seule.
|
|
83
|
+
|
|
84
|
+
#### Ancre de bloc (`#slug`)
|
|
85
|
+
|
|
86
|
+
Comme les blocs internes, un bloc personnalisé peut recevoir une **ancre**
|
|
87
|
+
depuis l'éditeur (menu d'actions du bloc ▸ **Ancre**). C'est un identifiant
|
|
88
|
+
stable, enregistré dans `block.anchor`, cible d'un lien interne `#slug`. À toi
|
|
89
|
+
de la **rendre** : pose `id={block.anchor || undefined}` sur l'élément racine
|
|
90
|
+
de ton `render.js`.
|
|
91
|
+
|
|
92
|
+
```jsx
|
|
93
|
+
export default function CalloutRender({ block }) {
|
|
94
|
+
return <div id={block.anchor || undefined} className="…">{block.text}</div>;
|
|
95
|
+
}
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
> On l'écrit **directement** plutôt qu'avec `anchorProps` du core :
|
|
99
|
+
> `@fraza/core/blocks-registry/render` est réservé au serveur, et ne
|
|
100
|
+
> s'importe donc pas dans un `render.js` marqué `'use client'`. L'écriture
|
|
101
|
+
> directe marche côté serveur comme côté client. Les quatre exemples fournis
|
|
102
|
+
> la suivent.
|
|
103
|
+
|
|
104
|
+
**Server component par défaut** (recommandé) : pas de directive en tête de
|
|
105
|
+
fichier. Le composant peut lire des données et profite du rendu serveur
|
|
106
|
+
complet, référencement compris.
|
|
107
|
+
|
|
108
|
+
#### Liens saisis dans un bloc
|
|
109
|
+
|
|
110
|
+
Un lien saisi dans un bloc (`href`) passe par `sanitizeUrl` de
|
|
111
|
+
`@fraza/core/blocks-registry/url`, sous-entrée neutre, utilisable côté serveur
|
|
112
|
+
comme côté client. Elle rend `''` pour une adresse au schéma refusé
|
|
113
|
+
(`javascript:`, `data:`…) : n'écris alors aucun lien. La liste d'icônes
|
|
114
|
+
fournie le fait.
|
|
115
|
+
|
|
116
|
+
#### Lire des données dans un rendu serveur
|
|
117
|
+
|
|
118
|
+
Un bloc qui affiche du contenu (posts, pages…) lit la donnée **directement
|
|
119
|
+
côté serveur**, par les fonctions `./public` du module concerné. **Jamais** par
|
|
120
|
+
un `fetch` HTTP vers `/za/api/...`. Le bloc tourne déjà dans le même
|
|
121
|
+
processus que l'API. Un appel HTTP vers soi-même repasse par Next, refait le
|
|
122
|
+
routage, interroge de nouveau la base et sérialise du JSON pour rien. En
|
|
123
|
+
production, il ressort en plus par le proxy inverse ou le pare-feu
|
|
124
|
+
applicatif, qui bloque souvent les requêtes de serveur à serveur : la section
|
|
125
|
+
reste vide, sans erreur.
|
|
126
|
+
|
|
127
|
+
```js
|
|
128
|
+
// render.js (serveur) : lecture directe, sans fetch HTTP
|
|
129
|
+
import { getPublicPosts } from '@fraza/module-posts/public';
|
|
130
|
+
|
|
131
|
+
export default async function MonRender({ block }) {
|
|
132
|
+
const { posts } = await getPublicPosts(block.postType || 'blogue', { limit: 6 });
|
|
133
|
+
return (
|
|
134
|
+
<ul id={block.anchor || undefined}>
|
|
135
|
+
{posts.map((p) => <li key={p.id}>{p.title}</li>)}
|
|
136
|
+
</ul>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
Fonctions disponibles : `getPublicPosts` / `getPublicPostBySlug` /
|
|
142
|
+
`getPublicTaxonomyTerms` (`@fraza/module-posts/public`), `listPublicChildPages` /
|
|
143
|
+
`getPublishedPageBySegments` (`@fraza/module-pages/public`). Les routes HTTP
|
|
144
|
+
`/za/api/...` sont réservées au **navigateur** (fetch côté client, même
|
|
145
|
+
origine) et à l'administration.
|
|
146
|
+
|
|
147
|
+
**Rendu interactif** (bascule, accordéon, carrousel…) : deux options.
|
|
148
|
+
|
|
149
|
+
1. **Rendu entièrement client** : mettre `'use client'` en tête de
|
|
150
|
+
`render.js` et utiliser `useState` / `useEffect`. Le manifeste serveur
|
|
151
|
+
importe alors le bloc comme référence client, et React franchit la
|
|
152
|
+
frontière automatiquement. Voir l'exemple `accordion/`.
|
|
153
|
+
|
|
154
|
+
> **Branchement requis** : `app/layout.js` doit importer
|
|
155
|
+
> `./.fraza/blocks.server.js` (à côté de `templates.server.js`) pour que les
|
|
156
|
+
> `render.js` clients passent par le compilateur RSC et soient référencés
|
|
157
|
+
> dans le React Client Manifest. Les nouveaux projets l'ont **déjà**. Sur un
|
|
158
|
+
> projet plus ancien, ajoute la ligne à la main, sinon l'erreur « Could not
|
|
159
|
+
> find the module … in the React Client Manifest » apparaît.
|
|
160
|
+
2. **Îlot client dans un rendu serveur** : garder `render.js` en server
|
|
161
|
+
component (rendu serveur du contenu statique) et isoler la partie
|
|
162
|
+
interactive dans un fichier enfant `*.client.js`, importé par `render.js`.
|
|
163
|
+
La découverte ignore les fichiers autres que `fields.js`, `template.js` et
|
|
164
|
+
`render.js`, mais ils sont inclus normalement par l'import.
|
|
165
|
+
|
|
166
|
+
```js
|
|
167
|
+
// render.js (serveur)
|
|
168
|
+
import Interactif from './Interactif.client.js';
|
|
169
|
+
export default function MonRender({ block }) {
|
|
170
|
+
return <div><h2>{block.title}</h2><Interactif data={block} /></div>;
|
|
171
|
+
}
|
|
172
|
+
```
|
|
173
|
+
|
|
174
|
+
## Cycle de vie
|
|
175
|
+
|
|
176
|
+
1. Tu crées un dossier `components/fraza/blocks/mon-bloc/` avec les 3 fichiers.
|
|
177
|
+
2. Tu lances `npx fraza-modules sync`, qui génère :
|
|
178
|
+
- `app/.fraza/blocks.client.js`, qui appelle `registerBlock({ type: 'custom:mon-bloc', Template: ..., ... })` ;
|
|
179
|
+
- `app/.fraza/blocks.server.js`, qui appelle `registerCustomBlock({ type: 'custom:mon-bloc', RenderComponent: ... })`.
|
|
180
|
+
3. `app/(admin)/admin/layout.js` rend `<FrazaBlocksClient />` côté client, et `instrumentation.js` importe `blocks.server.js` côté serveur. Le registre de l'éditeur est rempli dans l'administration, celui du rendu au démarrage.
|
|
181
|
+
4. Le bloc apparaît dans le menu `/` de chaque `BlockEditor`.
|
|
182
|
+
5. Sur le site public, les blocs de type `custom:*` sont rendus par le `RenderComponent` correspondant.
|
|
183
|
+
|
|
184
|
+
## Espace de noms
|
|
185
|
+
|
|
186
|
+
Tous les blocs personnalisés reçoivent le préfixe `custom:` (`mon-bloc` →
|
|
187
|
+
`custom:mon-bloc`). Il évite toute collision avec les blocs internes
|
|
188
|
+
(`paragraph`, `heading_1`, `image`, etc.), qui n'utilisent jamais `:`.
|
|
189
|
+
|
|
190
|
+
## Dépannage
|
|
191
|
+
|
|
192
|
+
Un bloc manque dans le menu `/` ? Vérifier dans l'ordre :
|
|
193
|
+
|
|
194
|
+
1. `npx fraza-modules sync` a-t-il été lancé ? La commande affiche `◦ block:<id>` pour chaque bloc trouvé.
|
|
195
|
+
2. Le dossier contient-il bien `fields.js` ? Sans lui, le dossier est ignoré sans message.
|
|
196
|
+
3. Le `meta.id` est-il valide ? Seulement `[a-z0-9-]`, et différent d'un bloc interne. Deux dossiers sous le même `meta.id` font échouer le démarrage, avec les deux dossiers nommés dans l'erreur.
|
|
197
|
+
4. Y a-t-il une erreur d'import dans `app/.fraza/blocks.client.js` ? Regarder la console du navigateur.
|
|
198
|
+
5. `app/(admin)/admin/layout.js` rend-il bien `<FrazaBlocksClient />` ? Sans lui, le manifeste n'est pas exécuté dans le navigateur.
|
|
199
|
+
|
|
200
|
+
## Exemples fournis
|
|
201
|
+
|
|
202
|
+
- **`callout/`** : encadré avec un message et 4 variantes de couleur. Montre `<TextareaField>` et `<SelectField>`. Rendu **serveur** statique.
|
|
203
|
+
- **`three-cards/`** : rangée de 3 cartes avec image, titre et texte. Montre `<ImageField>` (avec `MediaPicker`) et une structure imbriquée modifiée par `patchCard()`. Rendu **serveur** statique.
|
|
204
|
+
- **`accordion/`** : en-tête cliquable qui révèle un contenu. Montre un **rendu public interactif** (`render.js` marqué `'use client'`, avec `useState`).
|
|
205
|
+
- **`icon-list/`** : liste verticale d'éléments (icône, texte, lien facultatif) avec un titre et une couleur d'icône commune. Montre `<IconField>` et une **liste dynamique** (ajout et retrait d'éléments). L'éditeur propose toutes les icônes de `@fraza/icons` ; le rendu **serveur** affiche l'icône choisie avec `<Icon name>` de `@fraza/core/shared/components/Icon`, qui ne charge qu'elle.
|
|
206
|
+
|
|
207
|
+
Tu peux les supprimer ou t'en inspirer librement.
|