cinqcinqdev-seo 0.1.14 → 0.1.16

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.
Files changed (54) hide show
  1. package/README.md +554 -113
  2. package/dist/module.d.mts +6 -0
  3. package/dist/module.json +1 -1
  4. package/dist/module.mjs +14 -6
  5. package/dist/runtime/assets/admin-tw.css +1 -1
  6. package/dist/runtime/components/admin/Navbar.vue +6 -5
  7. package/dist/runtime/components/admin/Sidebar.vue +21 -8
  8. package/dist/runtime/components/sections/ContactForm.d.vue.ts +10 -0
  9. package/dist/runtime/components/sections/ContactForm.vue +138 -0
  10. package/dist/runtime/components/sections/ContactForm.vue.d.ts +10 -0
  11. package/dist/runtime/components/sections/FAQ.d.vue.ts +3 -0
  12. package/dist/runtime/components/sections/FAQ.vue +46 -0
  13. package/dist/runtime/components/sections/FAQ.vue.d.ts +3 -0
  14. package/dist/runtime/components/sections/Features.d.vue.ts +3 -0
  15. package/dist/runtime/components/sections/Features.vue +41 -0
  16. package/dist/runtime/components/sections/Features.vue.d.ts +3 -0
  17. package/dist/runtime/components/sections/HeroSection.d.vue.ts +3 -0
  18. package/dist/runtime/components/sections/HeroSection.vue +133 -0
  19. package/dist/runtime/components/sections/HeroSection.vue.d.ts +3 -0
  20. package/dist/runtime/components/sections/Pricing.d.vue.ts +3 -0
  21. package/dist/runtime/components/sections/Pricing.vue +108 -0
  22. package/dist/runtime/components/sections/Pricing.vue.d.ts +3 -0
  23. package/dist/runtime/components/sections/Process.d.vue.ts +3 -0
  24. package/dist/runtime/components/sections/Process.vue +95 -0
  25. package/dist/runtime/components/sections/Process.vue.d.ts +3 -0
  26. package/dist/runtime/components/sections/ServiceGrid.d.vue.ts +3 -0
  27. package/dist/runtime/components/sections/ServiceGrid.vue +97 -0
  28. package/dist/runtime/components/sections/ServiceGrid.vue.d.ts +3 -0
  29. package/dist/runtime/components/sections/Testimonials.d.vue.ts +3 -0
  30. package/dist/runtime/components/sections/Testimonials.vue +95 -0
  31. package/dist/runtime/components/sections/Testimonials.vue.d.ts +3 -0
  32. package/dist/runtime/components/sections/TextVisual.d.vue.ts +3 -0
  33. package/dist/runtime/components/sections/TextVisual.vue +94 -0
  34. package/dist/runtime/components/sections/TextVisual.vue.d.ts +3 -0
  35. package/dist/runtime/composables/useAdminBasePath.d.ts +1 -0
  36. package/dist/runtime/composables/useAdminBasePath.js +5 -0
  37. package/dist/runtime/composables/useAdminSections.d.ts +0 -7
  38. package/dist/runtime/composables/useAdminSections.js +68 -17
  39. package/dist/runtime/composables/useAdminSettings.d.ts +31 -0
  40. package/dist/runtime/composables/useAdminSettings.js +31 -0
  41. package/dist/runtime/middleware/admin-auth.js +3 -2
  42. package/dist/runtime/pages/admin/editor/[id].vue +10 -5
  43. package/dist/runtime/pages/admin/index.vue +3 -1
  44. package/dist/runtime/pages/admin/pages/[type].vue +5 -3
  45. package/dist/runtime/pages/admin/personas/[id].vue +3 -2
  46. package/dist/runtime/pages/admin/personas/index.vue +3 -2
  47. package/dist/runtime/pages/admin/setup.d.vue.ts +3 -0
  48. package/dist/runtime/pages/admin/setup.vue +220 -0
  49. package/dist/runtime/pages/admin/setup.vue.d.ts +3 -0
  50. package/dist/runtime/pages/admin/strategies/[id].vue +3 -2
  51. package/dist/runtime/pages/admin/strategies/index.vue +3 -2
  52. package/dist/runtime/server/api/ai/generate.post.js +8 -3
  53. package/dist/runtime/server/api/ai/seo-audit.post.js +8 -3
  54. package/package.json +1 -1
package/README.md CHANGED
@@ -1,33 +1,31 @@
1
- # nuxt-admin-cms
1
+ # cinqcinqdev-seo
2
2
 
3
- A reusable Nuxt 3 admin module with a visual 3-panel page editor, powered by Supabase.
4
-
5
- Drop it into any Nuxt project to get:
6
- - `/admin` — Dashboard with page type cards
7
- - `/admin/pages/:type` — List + create + delete pages by type
8
- - `/admin/editor/:id` — Full visual editor (structure sidebar / live preview / properties panel)
9
- - `/admin/account` — User profile + optional plan management
10
- - Auto auth middleware protecting all `/admin` routes
3
+ A Nuxt 3 admin CMS module with a visual page editor, AI content generation, and SEO tooling — powered by Supabase.
11
4
 
12
5
  ---
13
6
 
14
- ## Styling
15
-
16
- The module ships its own pre-compiled CSS — **no Tailwind setup required in your project**.
7
+ ## Features
17
8
 
18
- - The admin UI always looks identical regardless of your project's CSS framework or design system.
19
- - It uses the **Inter** font for all admin chrome (navbar, sidebars, panels).
20
- - The **section preview** in the editor inherits your project's own body font automatically, so components look exactly as they do on the live site.
9
+ - Visual 3-panel page editor (structure / live preview / properties)
10
+ - AI content generation per section via OpenRouter (fr / ar / en simultaneously)
11
+ - AI SEO audit with multilingual meta title + description suggestions
12
+ - Project setup page: AI context brief + per-component style defaults
13
+ - Multilingual content out of the box (`{ fr, ar, en }` i18n objects)
14
+ - Supabase-backed pages, brand settings, and image uploads
15
+ - Configurable `basePath` — no conflict with existing `/admin` routes
16
+ - Pre-compiled Tailwind CSS scoped to `[data-admin-cms]` — no Tailwind config needed in host app
21
17
 
22
18
  ---
23
19
 
24
- ## Prerequisites
20
+ ## Requirements
25
21
 
26
- Your project must have these modules installed and configured:
22
+ - Nuxt 3
23
+ - `@nuxtjs/supabase`
24
+ - `@pinia/nuxt`
25
+ - `@nuxt/icon`
26
+ - An [OpenRouter](https://openrouter.ai) API key (for AI features)
27
27
 
28
- ```bash
29
- pnpm add @nuxtjs/supabase @pinia/nuxt
30
- ```
28
+ ---
31
29
 
32
30
  ## Installation
33
31
 
@@ -35,168 +33,611 @@ pnpm add @nuxtjs/supabase @pinia/nuxt
35
33
  pnpm add cinqcinqdev-seo
36
34
  ```
37
35
 
38
- In `nuxt.config.ts`:
36
+ Add to `nuxt.config.js`:
39
37
 
40
- ```ts
38
+ ```js
41
39
  export default defineNuxtConfig({
42
40
  modules: [
43
41
  '@nuxtjs/supabase',
44
42
  '@pinia/nuxt',
43
+ '@nuxt/icon',
45
44
  'cinqcinqdev-seo',
46
45
  ],
47
46
 
48
47
  adminCms: {
49
- branding: {
50
- name: 'My App',
51
- logoUrl: '/logo.png', // optional
52
- },
48
+ branding: { name: 'My App' },
53
49
  loginRoute: '/login',
54
- tables: {
55
- pages: 'pages', // your Supabase table for pages
56
- users: 'users', // your Supabase table for users
57
- },
58
- storageBucket: 'site', // Supabase storage bucket for image uploads
59
- pageTypes: [
60
- { id: 'landing_page', label: 'Landing Page', icon: '🚀' },
61
- { id: 'service_page', label: 'Service Page', icon: '🛠️' },
62
- { id: 'about_page', label: 'About Page', icon: '👋' },
63
- { id: 'blog_article', label: 'Blog Article', icon: '📝' },
64
- ],
65
- // Optional: plans for the account page
66
- plans: [
67
- { value: 'free', title: 'Free', price: '$0/mo', features: ['1 site', '5 pages'] },
68
- { value: 'pro', title: 'Pro', price: '$19/mo', features: ['Unlimited sites', 'Priority support'], popular: true },
69
- ],
70
50
  },
71
51
  })
72
52
  ```
73
53
 
54
+ Add the OpenRouter key to `.env`:
55
+
56
+ ```env
57
+ OPENROUTER_API_KEY=sk-or-...
58
+ ```
59
+
74
60
  ---
75
61
 
76
- ## Supabase Setup
62
+ ## Database Setup
77
63
 
78
- ### Required table: `pages`
64
+ Run this SQL in your Supabase project (SQL Editor):
79
65
 
80
66
  ```sql
81
- create table pages (
82
- id uuid primary key default gen_random_uuid(),
83
- title text not null,
84
- slug text not null unique,
85
- type text not null,
86
- status text not null default 'draft',
87
- content jsonb not null default '[]',
88
- seo_config jsonb default '{}',
89
- content_locked boolean default false,
90
- label text,
91
- prefix text,
92
- created_at timestamptz default now(),
93
- updated_at timestamptz default now()
67
+ -- Pages table
68
+ CREATE TYPE page_type AS ENUM (
69
+ 'landing_page', 'service_page', 'about_page',
70
+ 'product_page', 'blog_article', 'portfolio_item'
94
71
  );
95
72
 
96
- -- Auto-update updated_at
97
- create or replace function update_updated_at()
98
- returns trigger as $$
99
- begin
100
- new.updated_at = now();
101
- return new;
102
- end;
103
- $$ language plpgsql;
73
+ CREATE TABLE IF NOT EXISTS public.pages (
74
+ id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
75
+ created_at timestamptz DEFAULT now(),
76
+ updated_at timestamptz DEFAULT now(),
77
+ title text NOT NULL,
78
+ slug text NOT NULL UNIQUE,
79
+ type page_type NOT NULL DEFAULT 'landing_page',
80
+ seo_config jsonb DEFAULT '{
81
+ "meta_title": {"fr": "", "ar": "", "en": ""},
82
+ "meta_description": {"fr": "", "ar": "", "en": ""},
83
+ "og_image": "",
84
+ "no_index": false
85
+ }'::jsonb,
86
+ content jsonb DEFAULT '[]'::jsonb,
87
+ status text CHECK (status IN (''draft'', ''published'', ''archived'')) DEFAULT 'draft',
88
+ content_locked boolean DEFAULT false
89
+ );
104
90
 
105
- create trigger pages_updated_at
106
- before update on pages
107
- for each row execute function update_updated_at();
108
- ```
91
+ ALTER TABLE public.pages ENABLE ROW LEVEL SECURITY;
109
92
 
110
- ### Required table: `users`
93
+ -- Public can read published pages
94
+ CREATE POLICY "public_read_published" ON public.pages
95
+ FOR SELECT USING (status = 'published');
111
96
 
112
- ```sql
113
- create table users (
114
- id uuid primary key references auth.users(id),
115
- plan text default 'free',
116
- is_subscription_active boolean default false,
97
+ -- Authenticated users have full access
98
+ CREATE POLICY "auth_full_access" ON public.pages
99
+ FOR ALL TO authenticated USING (true) WITH CHECK (true);
100
+
101
+ -- Auto-update updated_at
102
+ CREATE OR REPLACE FUNCTION update_updated_at_column()
103
+ RETURNS TRIGGER AS $$ BEGIN NEW.updated_at = now(); RETURN NEW; END; $$ language 'plpgsql';
104
+
105
+ CREATE TRIGGER update_pages_modtime
106
+ BEFORE UPDATE ON public.pages
107
+ FOR EACH ROW EXECUTE PROCEDURE update_updated_at_column();
108
+
109
+ -- Users table (for plan management)
110
+ CREATE TABLE IF NOT EXISTS public.users (
111
+ id uuid PRIMARY KEY REFERENCES auth.users(id),
112
+ plan text DEFAULT 'free',
113
+ is_subscription_active boolean DEFAULT false,
117
114
  trial_expires_at timestamptz,
118
- created_at timestamptz default now()
115
+ created_at timestamptz DEFAULT now()
116
+ );
117
+
118
+ -- Brand settings (AI context, component presets, colors, fonts)
119
+ CREATE TABLE IF NOT EXISTS public.brand_settings (
120
+ id INT PRIMARY KEY DEFAULT 1,
121
+ brand_name TEXT DEFAULT '',
122
+ tagline TEXT,
123
+ primary_color TEXT DEFAULT '#000000',
124
+ secondary_color TEXT DEFAULT '#ffffff',
125
+ accent_color TEXT DEFAULT '#F0F0F3',
126
+ font_headline TEXT DEFAULT 'Inter',
127
+ font_body TEXT DEFAULT 'Inter',
128
+ ai_context TEXT DEFAULT '',
129
+ section_presets JSONB DEFAULT '{}'::jsonb,
130
+ updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
131
+ CONSTRAINT one_row_only CHECK (id = 1)
119
132
  );
133
+
134
+ INSERT INTO public.brand_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
120
135
  ```
121
136
 
122
137
  ### Storage bucket
123
138
 
124
- Create a bucket named `site` (or whatever you set as `storageBucket`) in Supabase Storage with public read access.
139
+ Create a bucket named `site` (or your `storageBucket` value) in Supabase Storage. Add these policies:
140
+
141
+ ```sql
142
+ CREATE POLICY "Public read" ON storage.objects
143
+ FOR SELECT USING (bucket_id = 'site');
144
+
145
+ CREATE POLICY "Authenticated upload" ON storage.objects
146
+ FOR INSERT TO authenticated WITH CHECK (bucket_id = 'site');
147
+
148
+ CREATE POLICY "Authenticated delete" ON storage.objects
149
+ FOR DELETE TO authenticated USING (bucket_id = 'site');
150
+ ```
151
+
152
+ ---
153
+
154
+ ## Admin Routes
155
+
156
+ With default `basePath: '/admin'`:
157
+
158
+ | Route | Description |
159
+ |---|---|
160
+ | `/admin` | Dashboard — page type cards with counts |
161
+ | `/admin/setup` | Project setup — AI context + component style defaults |
162
+ | `/admin/pages/:type` | Page list for a given type (create / delete) |
163
+ | `/admin/editor/:id` | Visual editor for a page |
164
+ | `/admin/account` | User profile + subscription plan |
165
+
166
+ ---
167
+
168
+ ## Configuration Reference
169
+
170
+ All options go under the `adminCms` key in `nuxt.config.js`.
171
+
172
+ | Option | Type | Default | Description |
173
+ |---|---|---|---|
174
+ | `basePath` | `string` | `'/admin'` | Base path for all admin routes |
175
+ | `loginRoute` | `string` | `'/login'` | Redirect unauthenticated users here |
176
+ | `branding.name` | `string` | `'Admin CMS'` | Name shown in the sidebar |
177
+ | `branding.logoUrl` | `string` | `''` | Logo URL shown in the sidebar |
178
+ | `pageTypes` | `PageTypeConfig[]` | 6 defaults | Page type cards on the dashboard |
179
+ | `plans` | `PlanConfig[]` | `[]` | Subscription plans on the account page |
180
+ | `tables.pages` | `string` | `'pages'` | Supabase table name for pages |
181
+ | `tables.users` | `string` | `'users'` | Supabase table name for users |
182
+ | `storageBucket` | `string` | `'site'` | Supabase storage bucket for image uploads |
183
+ | `navSections` | `NavSectionItem[]` | `[]` | Extra sidebar links |
184
+ | `extraSections` | `Record<string, SectionConfig>` | `{}` | Extra section types for the visual editor |
185
+
186
+ ### Default page types
187
+
188
+ If `pageTypes` is empty, these are used:
189
+
190
+ ```js
191
+ [
192
+ { id: 'landing_page', label: 'Landing Pages' },
193
+ { id: 'service_page', label: 'Pages de Service' },
194
+ { id: 'about_page', label: 'À Propos' },
195
+ { id: 'product_page', label: 'Produits' },
196
+ { id: 'blog_article', label: 'Articles' },
197
+ { id: 'portfolio_item', label: 'Portfolio' },
198
+ ]
199
+ ```
200
+
201
+ Override entirely:
202
+
203
+ ```js
204
+ adminCms: {
205
+ pageTypes: [
206
+ { id: 'landing_page', label: 'Landing Pages', desc: 'Conversion pages' },
207
+ { id: 'blog_article', label: 'Blog', desc: 'Articles & news' },
208
+ ],
209
+ }
210
+ ```
211
+
212
+ Each entry accepts: `id`, `label`, `icon` (emoji), `svgPath` (SVG path string for sidebar icon), `desc` (shown on dashboard card).
213
+
214
+ ---
215
+
216
+ ## Avoiding Route Conflicts
217
+
218
+ If your app already uses `/admin`, change the base path:
219
+
220
+ ```js
221
+ adminCms: {
222
+ basePath: '/cms',
223
+ }
224
+ ```
225
+
226
+ All routes, sidebar links, redirects, and the auth middleware automatically use `/cms` instead. Nothing in your app is touched.
227
+
228
+ ---
229
+
230
+ ## Project Setup (`/admin/setup`)
231
+
232
+ The setup page is the first thing to fill in when starting a new project.
233
+
234
+ ### AI Context
235
+
236
+ A free-text field where you describe the website: industry, target audience, tone of voice, key services. Saved to `brand_settings.ai_context`.
237
+
238
+ This context is **automatically prepended to every AI call** (content generation and SEO audit), so the AI always writes copy relevant to the actual project — not generic placeholder text.
239
+
240
+ ### Component Style Defaults
241
+
242
+ For each built-in section type you can set:
243
+
244
+ - **Layout variant** (e.g. `centered`, `split`, `bento`, `grid`)
245
+ - **Animation** (fade up, blur, reveal, typewriter, etc.)
246
+ - **Border radius** (sharp / rounded / pill)
247
+ - **Spacing** (compact / normal / spacious)
248
+ - **Background and text colors**
249
+
250
+ Saved to `brand_settings.section_presets`. When you add a section in the visual editor, these defaults are applied automatically so every new section matches the project's style from the start.
251
+
252
+ ---
253
+
254
+ ## Visual Editor (`/admin/editor/:id`)
255
+
256
+ Three-panel layout:
257
+
258
+ **Left — Structure**
259
+ - List of all content blocks
260
+ - Reorder / delete blocks
261
+ - "Add section" button — opens a modal with all available types
262
+ - Toggle between Structure view and SEO view
263
+
264
+ **Center — Live Preview**
265
+ - Renders the actual section components
266
+ - Click any block to select it
267
+ - Language switcher (fr / ar / en) to preview each locale
268
+
269
+ **Right — Properties**
270
+ - Edit the selected block's fields (layout, colors, text, images, lists)
271
+ - Fields are grouped by tabs (Design, Animation, etc.)
272
+ - AI button: type a prompt → generates all text fields in all three languages at once
273
+ - In SEO mode: AI SEO audit that scores the page and suggests optimised meta titles + descriptions
274
+
275
+ ### Content locking
276
+
277
+ If a page has `content_locked = true` in the database, the editor shows only the SEO panel. The page layout is treated as fixed (defined directly in your Vue page file) and cannot be edited via the CMS. Useful for hand-crafted pages like the homepage.
125
278
 
126
279
  ---
127
280
 
128
281
  ## Section Components
129
282
 
130
- The editor renders sections by resolving components from your app's `components/sections/` directory.
283
+ The module ships these built-in section types:
284
+
285
+ | Type | Description |
286
+ |---|---|
287
+ | `HeroSection` | Hero with title, subtitle, badge, CTA, image |
288
+ | `TextVisual` | Text + image (split / stacked / wide) |
289
+ | `ServiceGrid` | Services in grid, list, cards, or minimal layout |
290
+ | `Features` | Feature highlights with icons |
291
+ | `Process` | Step-by-step process (steps / horizontal / timeline / cards) |
292
+ | `Testimonials` | Customer testimonials (grid / featured / wall) |
293
+ | `Pricing` | Pricing plans (cards / minimal / table) |
294
+ | `FAQ` | Accordion FAQ |
295
+ | `ContactForm` | Contact form (split / centered / minimal) |
131
296
 
132
- The default section types are: `HeroSection`, `TextVisual`, `ServiceGrid`, `Features`, `FAQ`, `Process`, `Testimonials`, `Pricing`, `ContactForm`.
297
+ Each section supports: layout variants, spacing, border radius, animation, background color, text color, and multilingual text fields.
133
298
 
134
- Create matching Vue components:
299
+ ### Creating section components
300
+
301
+ The editor resolves section components from your app's `components/sections/` directory.
135
302
 
136
303
  ```
137
304
  components/
138
305
  sections/
139
- HeroSection.vue ← receives props defined in the editor
306
+ HeroSection.vue
307
+ TextVisual.vue
140
308
  ServiceGrid.vue
141
- FAQ.vue
142
309
  ...
143
310
  ```
144
311
 
145
- Each section component receives its props directly via `v-bind`. Example:
312
+ Each component receives its stored props directly via `v-bind`. Handle i18n fields (stored as `{ fr, ar, en }` objects):
146
313
 
147
314
  ```vue
148
315
  <!-- components/sections/HeroSection.vue -->
149
- <script setup lang="ts">
150
- defineProps<{
151
- title: string | { fr: string; en: string; ar: string }
152
- subtitle?: string | { fr: string; en: string; ar: string }
153
- bgColor?: string
154
- textColor?: string
155
- ctaText?: string | { fr: string; en: string; ar: string }
156
- ctaLink?: string
157
- }>()
316
+ <script setup>
317
+ const props = defineProps(['title', 'subtitle', 'bgColor', 'textColor', 'ctaText', 'ctaLink', 'layout', 'animation'])
318
+
319
+ const { locale } = useI18n()
320
+
321
+ // Helper: resolve i18n field to a string
322
+ const t = (field) => {
323
+ if (!field || typeof field === 'string') return field || ''
324
+ return field[locale.value] || field.fr || Object.values(field).find(Boolean) || ''
325
+ }
158
326
  </script>
159
- ```
160
327
 
161
- > **i18n props**: Some fields are stored as `{ fr: '...', en: '...', ar: '...' }` objects. Handle them accordingly in your section components.
328
+ <template>
329
+ <section :style="{ background: bgColor, color: textColor }">
330
+ <h1>{{ t(title) }}</h1>
331
+ <p>{{ t(subtitle) }}</p>
332
+ <NuxtLink v-if="ctaLink" :to="ctaLink">{{ t(ctaText) }}</NuxtLink>
333
+ </section>
334
+ </template>
335
+ ```
162
336
 
163
- ### Adding custom sections
337
+ ### Adding custom section types
164
338
 
165
- Add your own sections via the `extraSections` option:
339
+ Define the section config in `nuxt.config.js`:
166
340
 
167
- ```ts
341
+ ```js
168
342
  adminCms: {
169
343
  extraSections: {
170
- MyCustomSection: {
171
- label: 'Custom Block',
172
- icon: '',
173
- defaultProps: { title: 'Hello', bgColor: '#fff' },
344
+ StatsBar: {
345
+ label: 'Stats Bar',
346
+ icon: '📊',
347
+ defaultProps: {
348
+ items: [],
349
+ bgColor: '#0d0d0d',
350
+ textColor: '#ffffff',
351
+ },
174
352
  fields: {
175
- title: { type: 'text', label: 'Title' },
176
- bgColor: { type: 'color', label: 'Background' },
353
+ items: {
354
+ type: 'list',
355
+ label: 'Stats',
356
+ itemFields: {
357
+ value: { type: 'text', label: 'Number' },
358
+ label: { type: 'text', label: 'Label', i18n: true },
359
+ },
360
+ },
361
+ bgColor: { type: 'color', label: 'Background' },
362
+ textColor: { type: 'color', label: 'Text Color' },
177
363
  },
178
364
  },
179
365
  },
180
366
  }
181
367
  ```
182
368
 
183
- Then create `components/sections/MyCustomSection.vue` in your app.
369
+ Create `components/sections/StatsBar.vue` in your app. The section will appear in the editor's "Add section" modal automatically.
370
+
371
+ ### Field types
372
+
373
+ | Type | Description |
374
+ |---|---|
375
+ | `text` | Single-line input. Add `i18n: true` for fr/ar/en tabs |
376
+ | `textarea` | Multi-line input. Add `i18n: true` for fr/ar/en tabs |
377
+ | `color` | Color picker + hex input |
378
+ | `image` | Image URL input + Supabase storage upload button |
379
+ | `select` | Dropdown. Requires `options: [{ value, label }]` |
380
+ | `cards` | Visual card picker. Requires `options: [{ value, label, icon? }]` |
381
+ | `list` | Repeatable list. Requires `itemFields: { fieldName: FieldConfig }` |
382
+
383
+ ---
384
+
385
+ ## Multilingual Content
386
+
387
+ Text fields with `i18n: true` are stored as:
388
+
389
+ ```json
390
+ { "fr": "Titre français", "ar": "العنوان", "en": "English title" }
391
+ ```
392
+
393
+ Plain fields (colors, images, URLs, booleans) remain scalar values.
394
+
395
+ **Fallback rule:** current locale → `fr` → first non-empty value.
396
+
397
+ ---
398
+
399
+ ## AI Features
400
+
401
+ Requires `OPENROUTER_API_KEY` in your environment. Uses `google/gemini-2.0-flash-lite-001` via OpenRouter.
402
+
403
+ ### Content generation
404
+
405
+ 1. Select a block in the editor
406
+ 2. Click the AI button (wand icon)
407
+ 3. Type a prompt — e.g. "Emphasise 10 years of expertise and affordable prices"
408
+ 4. All text fields (title, subtitle, items, etc.) are filled in fr + ar + en simultaneously
409
+
410
+ The AI always receives the **AI Context** from `/admin/setup` as a system prompt, ensuring output is always relevant to the specific project.
411
+
412
+ ### SEO audit
413
+
414
+ In SEO mode (left panel toggle), click "Analyser avec l'IA" to:
415
+ - Get a score (0–100)
416
+ - Receive an optimised meta title and meta description in fr / ar / en
417
+ - See a list of issues (errors, warnings, info)
418
+ - Get actionable suggestions
419
+
420
+ Apply the AI suggestions with one click, then adjust as needed.
421
+
422
+ ---
423
+
424
+ ## Sidebar Extra Links
425
+
426
+ Add custom links to the sidebar's "Contenu" section:
427
+
428
+ ```js
429
+ adminCms: {
430
+ navSections: [
431
+ {
432
+ label: 'Messages',
433
+ to: '/admin/contact',
434
+ svgPath: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>',
435
+ },
436
+ ],
437
+ }
438
+ ```
439
+
440
+ `to` can point to any route in your app — module pages or your own custom pages.
441
+
442
+ ---
443
+
444
+ ## Subscription Plans (Account Page)
445
+
446
+ Show subscription plans on the `/admin/account` page:
447
+
448
+ ```js
449
+ adminCms: {
450
+ plans: [
451
+ {
452
+ value: 'starter',
453
+ title: 'Starter',
454
+ subtitle: 'For small projects',
455
+ price: '0 DZD',
456
+ features: ['5 pages', '1 user', 'Basic SEO'],
457
+ },
458
+ {
459
+ value: 'pro',
460
+ title: 'Pro',
461
+ subtitle: 'For growing agencies',
462
+ price: '4 900 DZD/mo',
463
+ features: ['Unlimited pages', 'AI generation', 'Priority support'],
464
+ popular: true,
465
+ },
466
+ ],
467
+ }
468
+ ```
469
+
470
+ Plan state (`plan`, `is_subscription_active`, `trial_expires_at`) is read from the `users` table.
471
+
472
+ ---
473
+
474
+ ## Auto-imported Composables
475
+
476
+ These are available everywhere in your app without importing:
477
+
478
+ ### `useAdminSettings()`
479
+
480
+ Load and save the `brand_settings` row (AI context, section presets, colors, fonts).
481
+
482
+ ```ts
483
+ const { settings, loading, saving, load, save, applyPresets } = useAdminSettings()
484
+
485
+ await load()
486
+ // settings.value → { ai_context, section_presets, primary_color, ... }
487
+
488
+ await save({ ai_context: 'Web agency in Algiers...' })
489
+
490
+ // Apply saved style presets when adding a new section
491
+ const props = applyPresets('HeroSection', defaultProps)
492
+ // → merges layout, spacing, radius, animation, bgColor, textColor from brand_settings
493
+ ```
494
+
495
+ ### `useAdminSections()`
496
+
497
+ Returns the merged section config (built-ins + `extraSections`).
498
+
499
+ ```ts
500
+ const sections = useAdminSections()
501
+ // { HeroSection: { label, icon, defaultProps, fields }, ... }
502
+ ```
503
+
504
+ ### `useAdminBasePath()`
505
+
506
+ Returns the configured admin base path string.
507
+
508
+ ```ts
509
+ const basePath = useAdminBasePath()
510
+ // '/admin' or whatever is configured
511
+ ```
512
+
513
+ ### `useAdminBranding()`
514
+
515
+ Load and save branding data from a `branding` table (separate from `brand_settings`).
516
+
517
+ ```ts
518
+ const { fetchBranding, saveBranding } = useAdminBranding()
519
+ const data = await fetchBranding()
520
+ await saveBranding({ brand_name: 'My Agency', color_primary: '#3d35ff' })
521
+ ```
522
+
523
+ ---
524
+
525
+ ## Styling
526
+
527
+ The module ships a pre-compiled Tailwind CSS file scoped to `[data-admin-cms]`.
528
+
529
+ - Admin styles never leak into your app
530
+ - No Tailwind config required in the consuming project
531
+ - If your app uses `@nuxtjs/tailwindcss`, the module automatically extends its content paths so admin classes are never purged
532
+ - The section **preview** area in the editor inherits your app's own body font, so sections look exactly as they do on the live site
184
533
 
185
534
  ---
186
535
 
187
536
  ## Auth
188
537
 
189
- The module registers a global middleware that:
190
- - Redirects unauthenticated users trying to access `/admin/*` to `loginRoute` (`/login` by default)
191
- - Redirects authenticated users away from `loginRoute` to `/admin`
538
+ A global Nuxt route middleware is registered automatically:
192
539
 
193
- Your login page + Supabase auth setup is your responsibility (the module only protects routes).
540
+ - Redirects unauthenticated users from `basePath/*` to `loginRoute`
541
+ - Redirects authenticated users away from `loginRoute` to `basePath`
542
+
543
+ Auth state is read from `useSupabaseUser()` provided by `@nuxtjs/supabase`. Your login page and Supabase auth setup are your responsibility.
194
544
 
195
545
  ---
196
546
 
197
- ## What's NOT included
547
+ ## What's NOT Included
548
+
549
+ - Login / register pages — you provide these
550
+ - Section component UI — you build the Vue components in `components/sections/`
551
+ - Email / webhook integrations
552
+ - Multi-tenancy / per-user page isolation — implement via Supabase RLS
553
+
554
+ ---
555
+
556
+ ## Full Example
557
+
558
+ ```js
559
+ // nuxt.config.js
560
+ export default defineNuxtConfig({
561
+ modules: [
562
+ '@nuxtjs/supabase',
563
+ '@pinia/nuxt',
564
+ '@nuxt/icon',
565
+ 'cinqcinqdev-seo',
566
+ ],
567
+
568
+ supabase: {
569
+ redirect: false,
570
+ },
571
+
572
+ adminCms: {
573
+ basePath: '/admin', // change if /admin is already taken
574
+ loginRoute: '/login',
198
575
 
199
- - Login / Register pages → you provide these
200
- - Section component UI → you provide the Vue components in `components/sections/`
201
- - Email notifications, webhooks → out of scope
202
- - Multi-tenancy / per-user page isolation → implement RLS in Supabase
576
+ branding: {
577
+ name: 'My Agency',
578
+ logoUrl: '/logo.svg',
579
+ },
580
+
581
+ storageBucket: 'site',
582
+
583
+ tables: {
584
+ pages: 'pages',
585
+ users: 'users',
586
+ },
587
+
588
+ pageTypes: [
589
+ { id: 'landing_page', label: 'Landing Pages', desc: 'Conversion pages' },
590
+ { id: 'service_page', label: 'Services', desc: 'Service pages' },
591
+ { id: 'blog_article', label: 'Blog', desc: 'Articles & news' },
592
+ ],
593
+
594
+ navSections: [
595
+ {
596
+ label: 'Contacts',
597
+ to: '/admin/contact',
598
+ svgPath: '<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M3 8l7.89 5.26a2 2 0 002.22 0L21 8M5 19h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v10a2 2 0 002 2z"/>',
599
+ },
600
+ ],
601
+
602
+ plans: [
603
+ {
604
+ value: 'starter',
605
+ title: 'Starter',
606
+ price: '0 DZD',
607
+ features: ['5 pages', '1 user'],
608
+ },
609
+ {
610
+ value: 'pro',
611
+ title: 'Pro',
612
+ price: '4 900 DZD/mo',
613
+ features: ['Unlimited pages', 'AI generation', 'Priority support'],
614
+ popular: true,
615
+ },
616
+ ],
617
+
618
+ extraSections: {
619
+ StatsBar: {
620
+ label: 'Stats Bar',
621
+ icon: '📊',
622
+ defaultProps: {
623
+ items: [],
624
+ bgColor: '#0d0d0d',
625
+ textColor: '#ffffff',
626
+ },
627
+ fields: {
628
+ items: {
629
+ type: 'list',
630
+ label: 'Stats',
631
+ itemFields: {
632
+ value: { type: 'text', label: 'Number' },
633
+ label: { type: 'text', label: 'Label', i18n: true },
634
+ },
635
+ },
636
+ bgColor: { type: 'color', label: 'Background' },
637
+ textColor: { type: 'color', label: 'Text Color' },
638
+ },
639
+ },
640
+ },
641
+ },
642
+ })
643
+ ```