cinqcinqdev-seo 0.1.15 → 0.1.17
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/README.md +554 -113
- package/dist/module.d.mts +6 -0
- package/dist/module.json +1 -1
- package/dist/module.mjs +14 -6
- package/dist/runtime/assets/admin-tw.css +1 -1
- package/dist/runtime/components/admin/Navbar.vue +6 -5
- package/dist/runtime/components/admin/Sidebar.vue +21 -8
- package/dist/runtime/components/sections/ContactForm.vue +14 -10
- package/dist/runtime/components/sections/FAQ.vue +10 -6
- package/dist/runtime/components/sections/Features.vue +10 -5
- package/dist/runtime/components/sections/HeroSection.vue +12 -8
- package/dist/runtime/components/sections/Pricing.vue +8 -5
- package/dist/runtime/components/sections/Process.vue +9 -4
- package/dist/runtime/components/sections/ServiceGrid.vue +10 -5
- package/dist/runtime/components/sections/Testimonials.vue +12 -7
- package/dist/runtime/components/sections/TextVisual.vue +8 -3
- package/dist/runtime/composables/useAdminBasePath.d.ts +1 -0
- package/dist/runtime/composables/useAdminBasePath.js +5 -0
- package/dist/runtime/composables/useAdminSections.d.ts +0 -7
- package/dist/runtime/composables/useAdminSections.js +68 -17
- package/dist/runtime/composables/useAdminSettings.d.ts +31 -0
- package/dist/runtime/composables/useAdminSettings.js +31 -0
- package/dist/runtime/middleware/admin-auth.js +3 -2
- package/dist/runtime/pages/admin/editor/[id].vue +10 -5
- package/dist/runtime/pages/admin/index.vue +4 -2
- package/dist/runtime/pages/admin/pages/[type].vue +5 -3
- package/dist/runtime/pages/admin/personas/[id].vue +3 -2
- package/dist/runtime/pages/admin/personas/index.vue +3 -2
- package/dist/runtime/pages/admin/setup.d.vue.ts +3 -0
- package/dist/runtime/pages/admin/setup.vue +220 -0
- package/dist/runtime/pages/admin/setup.vue.d.ts +3 -0
- package/dist/runtime/pages/admin/strategies/[id].vue +3 -2
- package/dist/runtime/pages/admin/strategies/index.vue +3 -2
- package/dist/runtime/server/api/ai/generate.post.js +12 -6
- package/dist/runtime/server/api/ai/seo-audit.post.js +12 -6
- package/package.json +4 -1
package/README.md
CHANGED
|
@@ -1,33 +1,31 @@
|
|
|
1
|
-
#
|
|
1
|
+
# cinqcinqdev-seo
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
-
##
|
|
15
|
-
|
|
16
|
-
The module ships its own pre-compiled CSS — **no Tailwind setup required in your project**.
|
|
7
|
+
## Features
|
|
17
8
|
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
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
|
-
##
|
|
20
|
+
## Requirements
|
|
25
21
|
|
|
26
|
-
|
|
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
|
-
|
|
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
|
-
|
|
36
|
+
Add to `nuxt.config.js`:
|
|
39
37
|
|
|
40
|
-
```
|
|
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
|
-
##
|
|
62
|
+
## Database Setup
|
|
77
63
|
|
|
78
|
-
|
|
64
|
+
Run this SQL in your Supabase project (SQL Editor):
|
|
79
65
|
|
|
80
66
|
```sql
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
-
|
|
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
|
-
|
|
93
|
+
-- Public can read published pages
|
|
94
|
+
CREATE POLICY "public_read_published" ON public.pages
|
|
95
|
+
FOR SELECT USING (status = 'published');
|
|
111
96
|
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
297
|
+
Each section supports: layout variants, spacing, border radius, animation, background color, text color, and multilingual text fields.
|
|
133
298
|
|
|
134
|
-
|
|
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
|
|
306
|
+
HeroSection.vue
|
|
307
|
+
TextVisual.vue
|
|
140
308
|
ServiceGrid.vue
|
|
141
|
-
FAQ.vue
|
|
142
309
|
...
|
|
143
310
|
```
|
|
144
311
|
|
|
145
|
-
Each
|
|
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
|
|
150
|
-
defineProps
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
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
|
-
>
|
|
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
|
|
337
|
+
### Adding custom section types
|
|
164
338
|
|
|
165
|
-
|
|
339
|
+
Define the section config in `nuxt.config.js`:
|
|
166
340
|
|
|
167
|
-
```
|
|
341
|
+
```js
|
|
168
342
|
adminCms: {
|
|
169
343
|
extraSections: {
|
|
170
|
-
|
|
171
|
-
label: '
|
|
172
|
-
icon: '
|
|
173
|
-
defaultProps: {
|
|
344
|
+
StatsBar: {
|
|
345
|
+
label: 'Stats Bar',
|
|
346
|
+
icon: '📊',
|
|
347
|
+
defaultProps: {
|
|
348
|
+
items: [],
|
|
349
|
+
bgColor: '#0d0d0d',
|
|
350
|
+
textColor: '#ffffff',
|
|
351
|
+
},
|
|
174
352
|
fields: {
|
|
175
|
-
|
|
176
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
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
|
+
```
|