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.
Files changed (36) 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.vue +14 -10
  9. package/dist/runtime/components/sections/FAQ.vue +10 -6
  10. package/dist/runtime/components/sections/Features.vue +10 -5
  11. package/dist/runtime/components/sections/HeroSection.vue +12 -8
  12. package/dist/runtime/components/sections/Pricing.vue +8 -5
  13. package/dist/runtime/components/sections/Process.vue +9 -4
  14. package/dist/runtime/components/sections/ServiceGrid.vue +10 -5
  15. package/dist/runtime/components/sections/Testimonials.vue +12 -7
  16. package/dist/runtime/components/sections/TextVisual.vue +8 -3
  17. package/dist/runtime/composables/useAdminBasePath.d.ts +1 -0
  18. package/dist/runtime/composables/useAdminBasePath.js +5 -0
  19. package/dist/runtime/composables/useAdminSections.d.ts +0 -7
  20. package/dist/runtime/composables/useAdminSections.js +68 -17
  21. package/dist/runtime/composables/useAdminSettings.d.ts +31 -0
  22. package/dist/runtime/composables/useAdminSettings.js +31 -0
  23. package/dist/runtime/middleware/admin-auth.js +3 -2
  24. package/dist/runtime/pages/admin/editor/[id].vue +10 -5
  25. package/dist/runtime/pages/admin/index.vue +4 -2
  26. package/dist/runtime/pages/admin/pages/[type].vue +5 -3
  27. package/dist/runtime/pages/admin/personas/[id].vue +3 -2
  28. package/dist/runtime/pages/admin/personas/index.vue +3 -2
  29. package/dist/runtime/pages/admin/setup.d.vue.ts +3 -0
  30. package/dist/runtime/pages/admin/setup.vue +220 -0
  31. package/dist/runtime/pages/admin/setup.vue.d.ts +3 -0
  32. package/dist/runtime/pages/admin/strategies/[id].vue +3 -2
  33. package/dist/runtime/pages/admin/strategies/index.vue +3 -2
  34. package/dist/runtime/server/api/ai/generate.post.js +12 -6
  35. package/dist/runtime/server/api/ai/seo-audit.post.js +12 -6
  36. package/package.json +4 -1
@@ -1,6 +1,7 @@
1
1
  <script setup>
2
- import { ref, computed, watch, definePageMeta, useRoute, useRuntimeConfig, useAsyncData, useHead, useSupabaseClient, useNuxtApp } from "#imports";
2
+ import { ref, computed, watch, onMounted, definePageMeta, useRoute, useRuntimeConfig, useAsyncData, useHead, useSupabaseClient, useNuxtApp } from "#imports";
3
3
  import { useAdminSections } from "../../../composables/useAdminSections";
4
+ import { useAdminSettings, useAdminBasePath } from "#imports";
4
5
  definePageMeta({ layout: false });
5
6
  const route = useRoute();
6
7
  const supabase = useSupabaseClient();
@@ -10,6 +11,9 @@ const bucket = config?.storageBucket || "site";
10
11
  const pageId = route.params.id;
11
12
  const CMS_COMPONENTS = useAdminSections();
12
13
  const { vueApp } = useNuxtApp();
14
+ const adminSettings = useAdminSettings();
15
+ const basePath = useAdminBasePath();
16
+ onMounted(() => adminSettings.load());
13
17
  const resolveSection = (type) => vueApp.component(`Sections${type}`) || vueApp.component(type) || null;
14
18
  const { data: page } = await useAsyncData(`admin-edit-${pageId}`, async () => {
15
19
  const { data } = await supabase.from(table).select("*").eq("id", pageId).maybeSingle();
@@ -180,7 +184,8 @@ const generateWithAI = async () => {
180
184
  fields,
181
185
  currentProps,
182
186
  prompt: aiPromptText.value,
183
- itemFieldsConfig
187
+ itemFieldsConfig,
188
+ aiContext: adminSettings.settings.value?.ai_context || ""
184
189
  }
185
190
  });
186
191
  const props = page.value.content[selectedBlockIndex.value].props;
@@ -229,7 +234,7 @@ const runSeoAudit = async () => {
229
234
  try {
230
235
  const result = await $fetch("/api/admin-cms/ai/seo-audit", {
231
236
  method: "POST",
232
- body: { pageTitle: page.value.title, pageType: page.value.type, content: page.value.content, seoConfig: page.value.seo_config }
237
+ body: { pageTitle: page.value.title, pageType: page.value.type, content: page.value.content, seoConfig: page.value.seo_config, aiContext: adminSettings.settings.value?.ai_context || "" }
233
238
  });
234
239
  seoAudit.value = result;
235
240
  } catch (err) {
@@ -397,7 +402,7 @@ const savePage = async () => {
397
402
  <!-- ── Top Bar ── -->
398
403
  <header class="h-12 bg-white/80 backdrop-blur-md border-b px-6 flex items-center justify-between z-[60] shrink-0">
399
404
  <div class="flex items-center gap-4">
400
- <NuxtLink to="/admin" class="text-gray-400 hover:text-black transition-colors">←</NuxtLink>
405
+ <NuxtLink :to="basePath" class="text-gray-400 hover:text-black transition-colors">←</NuxtLink>
401
406
  <div class="flex gap-1 bg-gray-100 p-1 rounded-md">
402
407
  <button @click="isLeftSidebarOpen = !isLeftSidebarOpen" :class="['p-1.5 rounded transition-all', isLeftSidebarOpen ? 'bg-white shadow-sm text-[#3d35ff]' : 'text-gray-400']" title="Structure">
403
408
  <svg class="w-3 h-3" fill="none" stroke="currentColor" viewBox="0 0 24 24"><path stroke-linecap="round" stroke-linejoin="round" stroke-width="2.5" d="M4 6h16M4 12h10M4 18h16"/></svg>
@@ -877,7 +882,7 @@ const savePage = async () => {
877
882
  <h2 class="text-[12px] font-black uppercase text-center mb-12 tracking-[0.4em] italic text-[#3d35ff]">Nouvelle Section</h2>
878
883
  <div class="grid grid-cols-2 md:grid-cols-4 gap-8">
879
884
  <button v-for="(conf, type) in CMS_COMPONENTS" :key="type"
880
- @click="page.content.push({ type, props: JSON.parse(JSON.stringify(conf.defaultProps)) });
885
+ @click="page.content.push({ type, props: JSON.parse(JSON.stringify(adminSettings.applyPresets(type, conf.defaultProps))) });
881
886
  showAddModal = false;
882
887
  selectedBlockIndex = page.content.length - 1"
883
888
  class="flex flex-col items-center gap-6 p-8 border-2 border-gray-50 rounded-[40px] hover:border-[#3d35ff] transition-all group active:scale-95">
@@ -1,9 +1,11 @@
1
1
  <script setup>
2
- import { computed, useHead, useRuntimeConfig, useSupabaseClient } from "#imports";
2
+ import { computed, useHead, useRuntimeConfig, useSupabaseClient, useAsyncData } from "#imports";
3
+ import { useAdminBasePath } from "#imports";
3
4
  useHead({ title: "Tableau de bord \u2014 Admin" });
4
5
  const config = useRuntimeConfig().public.adminCms;
5
6
  const supabase = useSupabaseClient();
6
7
  const tables = computed(() => config?.tables || { pages: "pages" });
8
+ const basePath = useAdminBasePath();
7
9
  const pageTypes = computed(() => {
8
10
  const types = config?.pageTypes || [];
9
11
  return types.map((pt) => ({
@@ -74,7 +76,7 @@ const { data: stats } = await useAsyncData("admin-dashboard-stats", async () =>
74
76
  <NuxtLink
75
77
  v-for="type in pageTypes"
76
78
  :key="type.id"
77
- :to="`/admin/pages/${type.id}`"
79
+ :to="`${basePath}/pages/${type.id}`"
78
80
  class="group bg-white rounded-2xl p-6 border border-gray-100 shadow-sm hover:border-[#3d35ff]/30 hover:shadow-md transition-all"
79
81
  >
80
82
  <div class="flex items-start justify-between mb-4">
@@ -1,8 +1,10 @@
1
1
  <script setup>
2
2
  import { ref, computed, watch, useRoute, useRuntimeConfig, useAsyncData, useHead, navigateTo, useSupabaseClient } from "#imports";
3
+ import { useAdminBasePath } from "#imports";
3
4
  const route = useRoute();
4
5
  const supabase = useSupabaseClient();
5
6
  const config = useRuntimeConfig().public.adminCms;
7
+ const basePath = useAdminBasePath();
6
8
  const pageType = route.params.type;
7
9
  const table = config?.tables?.pages || "pages";
8
10
  const typeLabel = computed(
@@ -46,7 +48,7 @@ const handleCreate = async () => {
46
48
  }
47
49
  const { data, error } = await supabase.from(table).insert([{ title: newPageTitle.value, slug, type: pageType, status: "draft", content }]).select();
48
50
  if (!error && data) {
49
- navigateTo(`/admin/editor/${data[0].id}`);
51
+ navigateTo(`${basePath}/editor/${data[0].id}`);
50
52
  } else {
51
53
  isCreating.value = false;
52
54
  showToast("Ce slug est d\xE9j\xE0 utilis\xE9. Changez le titre.", "error");
@@ -106,7 +108,7 @@ useHead({ title: computed(() => `${typeLabel.value} \u2014 Admin`) });
106
108
  <div class="mb-8 flex items-center justify-between">
107
109
  <div>
108
110
  <div class="flex items-center gap-2 text-[11px] text-gray-400 mb-2">
109
- <NuxtLink to="/admin" class="hover:text-gray-600 transition-colors">Tableau de bord</NuxtLink>
111
+ <NuxtLink :to="basePath" class="hover:text-gray-600 transition-colors">Tableau de bord</NuxtLink>
110
112
  <span>/</span>
111
113
  <span class="text-gray-600 font-medium">{{ typeLabel }}</span>
112
114
  </div>
@@ -164,7 +166,7 @@ useHead({ title: computed(() => `${typeLabel.value} \u2014 Admin`) });
164
166
  </td>
165
167
  <td class="px-6 py-4">
166
168
  <div class="flex items-center justify-end gap-1">
167
- <NuxtLink :to="`/admin/editor/${page.id}`"
169
+ <NuxtLink :to="`${basePath}/editor/${page.id}`"
168
170
  class="px-3 py-1.5 rounded-lg text-[11px] font-bold bg-gray-100 text-gray-600 hover:bg-[#3d35ff] hover:text-white transition-all">
169
171
  Éditer
170
172
  </NuxtLink>
@@ -1,8 +1,9 @@
1
1
  <script setup>
2
2
  import { ref, onMounted, useHead, useRoute, navigateTo } from "#imports";
3
- import { useAdminPersonas, useAdminContent } from "#imports";
3
+ import { useAdminPersonas, useAdminContent, useAdminBasePath } from "#imports";
4
4
  const route = useRoute();
5
5
  const id = route.params.id;
6
+ const basePath = useAdminBasePath();
6
7
  useHead({ title: "Persona - Admin" });
7
8
  const { fetchPersonaById, updatePersona } = useAdminPersonas();
8
9
  const { getContentByPersona } = useAdminContent();
@@ -47,7 +48,7 @@ const statusClass = {
47
48
  <template>
48
49
  <AdminDashboardLayout>
49
50
  <div class="max-w-5xl mx-auto px-6 md:px-10 py-10">
50
- <button @click="navigateTo('/admin/personas')" class="flex items-center gap-2 text-slate-500 hover:text-blue-600 font-semibold transition-all mb-8 group">
51
+ <button @click="navigateTo(`${basePath}/personas`)" class="flex items-center gap-2 text-slate-500 hover:text-blue-600 font-semibold transition-all mb-8 group">
51
52
  <Icon name="mdi:arrow-left" class="text-xl group-hover:-translate-x-1 transition-transform" />
52
53
  Retour aux Personas
53
54
  </button>
@@ -1,7 +1,8 @@
1
1
  <script setup>
2
2
  import { ref, computed, onMounted, useHead, navigateTo } from "#imports";
3
- import { useAdminPersonas } from "#imports";
3
+ import { useAdminPersonas, useAdminBasePath } from "#imports";
4
4
  useHead({ title: "Personas - Admin" });
5
+ const basePath = useAdminBasePath();
5
6
  const { fetchPersonas, createPersona, updatePersona, deletePersona } = useAdminPersonas();
6
7
  const personas = ref([]);
7
8
  const loading = ref(true);
@@ -88,7 +89,7 @@ const handleDelete = async (id) => {
88
89
  }
89
90
  }
90
91
  };
91
- const viewPersona = (persona) => navigateTo(`/admin/personas/${persona.id}`);
92
+ const viewPersona = (persona) => navigateTo(`${basePath}/personas/${persona.id}`);
92
93
  </script>
93
94
 
94
95
  <template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: any;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -0,0 +1,220 @@
1
+ <script setup>
2
+ import { ref, onMounted, computed, useHead } from "#imports";
3
+ import { useAdminSettings } from "#imports";
4
+ import { useAdminSections } from "#imports";
5
+ useHead({ title: "Configuration \u2014 Admin" });
6
+ const { settings, loading, saving, load, save } = useAdminSettings();
7
+ const sections = useAdminSections();
8
+ const aiContext = ref("");
9
+ const presets = ref({});
10
+ const saved = ref(false);
11
+ onMounted(async () => {
12
+ await load();
13
+ aiContext.value = settings.value?.ai_context || "";
14
+ presets.value = JSON.parse(JSON.stringify(settings.value?.section_presets || {}));
15
+ });
16
+ const setPreset = (sectionType, field, value) => {
17
+ if (!presets.value[sectionType]) presets.value[sectionType] = {};
18
+ presets.value[sectionType][field] = value;
19
+ };
20
+ const getPreset = (sectionType, field, fallback) => {
21
+ return presets.value[sectionType]?.[field] ?? fallback ?? "";
22
+ };
23
+ const saveAll = async () => {
24
+ await save({ ai_context: aiContext.value, section_presets: presets.value });
25
+ saved.value = true;
26
+ setTimeout(() => {
27
+ saved.value = false;
28
+ }, 2e3);
29
+ };
30
+ const styleFields = ["layout", "spacing", "radius", "animation"];
31
+ const hasStyleOptions = (conf) => styleFields.some((f) => conf.fields?.[f]?.options?.length);
32
+ </script>
33
+
34
+ <template>
35
+ <AdminDashboardLayout>
36
+ <div data-admin-cms class="max-w-3xl mx-auto px-6 py-10">
37
+
38
+ <!-- Header -->
39
+ <div class="flex items-start justify-between mb-10">
40
+ <div>
41
+ <h1 class="text-2xl font-bold text-slate-900">Configuration du Projet</h1>
42
+ <p class="text-sm text-slate-500 mt-1">Contexte IA et préférences des composants</p>
43
+ </div>
44
+ <button
45
+ @click="saveAll"
46
+ :disabled="saving"
47
+ class="px-5 py-2.5 bg-slate-900 text-white text-sm font-semibold rounded-xl hover:bg-slate-700 disabled:opacity-50 transition-all flex items-center gap-2"
48
+ >
49
+ <svg v-if="!saving && !saved" class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
50
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"/>
51
+ </svg>
52
+ <svg v-if="saving" class="w-4 h-4 animate-spin" fill="none" viewBox="0 0 24 24">
53
+ <circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"/>
54
+ <path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8v8H4z"/>
55
+ </svg>
56
+ {{ saved ? "Enregistr\xE9 \u2713" : saving ? "Enregistrement..." : "Enregistrer" }}
57
+ </button>
58
+ </div>
59
+
60
+ <div v-if="loading" class="text-center py-20 text-slate-400 text-sm">Chargement...</div>
61
+
62
+ <div v-else class="space-y-6">
63
+
64
+ <!-- ── AI Context ───────────────────────────────────────────────── -->
65
+ <div class="bg-white border border-slate-200 rounded-2xl p-8">
66
+ <div class="flex items-center gap-3 mb-2">
67
+ <span class="text-xl">✨</span>
68
+ <h2 class="text-base font-bold text-slate-900">Contexte IA</h2>
69
+ </div>
70
+ <p class="text-xs text-slate-500 mb-5 leading-relaxed">
71
+ Décrivez votre activité, cible et ton. L'IA s'en souviendra pour chaque génération de contenu et audit SEO.
72
+ </p>
73
+ <textarea
74
+ v-model="aiContext"
75
+ rows="7"
76
+ placeholder="Ex: Agence web basée à Alger spécialisée dans la création de sites vitrines et e-commerce pour les PME algériennes. Ton professionnel mais accessible, orienté résultats. Nos clients sont des entrepreneurs et dirigeants algériens entre 30 et 50 ans..."
77
+ class="w-full px-4 py-3 text-sm border border-slate-200 rounded-xl focus:outline-none focus:border-slate-400 resize-none leading-relaxed text-slate-700 placeholder:text-slate-300"
78
+ />
79
+ <p class="text-xs text-slate-400 mt-2 text-right">{{ aiContext.length }} caractères</p>
80
+ </div>
81
+
82
+ <!-- ── Component Defaults ──────────────────────────────────────── -->
83
+ <div class="bg-white border border-slate-200 rounded-2xl p-8">
84
+ <div class="flex items-center gap-3 mb-2">
85
+ <span class="text-xl">🎨</span>
86
+ <h2 class="text-base font-bold text-slate-900">Préférences des Composants</h2>
87
+ </div>
88
+ <p class="text-xs text-slate-500 mb-8 leading-relaxed">
89
+ Définissez le style par défaut de chaque section. Ces préférences s'appliquent automatiquement quand vous ajoutez une section dans l'éditeur.
90
+ </p>
91
+
92
+ <div class="space-y-8">
93
+ <div
94
+ v-for="(conf, type) in sections"
95
+ :key="type"
96
+ v-show="hasStyleOptions(conf)"
97
+ class="border border-slate-100 rounded-xl p-5"
98
+ >
99
+ <!-- Component label -->
100
+ <div class="flex items-center gap-2 mb-5">
101
+ <span class="text-lg leading-none">{{ conf.icon }}</span>
102
+ <span class="text-sm font-bold text-slate-800">{{ conf.label }}</span>
103
+ </div>
104
+
105
+ <div class="space-y-4">
106
+
107
+ <!-- Layout -->
108
+ <div v-if="conf.fields?.layout?.options">
109
+ <p class="text-[10px] font-semibold uppercase tracking-widest text-slate-400 mb-2">Layout</p>
110
+ <div class="flex flex-wrap gap-1.5">
111
+ <button
112
+ v-for="opt in conf.fields.layout.options"
113
+ :key="opt.value"
114
+ @click="setPreset(String(type), 'layout', opt.value)"
115
+ :class="[
116
+ 'px-3 py-1.5 rounded-lg text-xs font-semibold border transition-all',
117
+ getPreset(String(type), 'layout', conf.defaultProps?.layout) === opt.value ? 'bg-slate-900 text-white border-slate-900' : 'bg-white text-slate-600 border-slate-200 hover:border-slate-400'
118
+ ]"
119
+ >
120
+ <span v-if="opt.icon" class="mr-1">{{ opt.icon }}</span>{{ opt.label }}
121
+ </button>
122
+ </div>
123
+ </div>
124
+
125
+ <!-- Animation -->
126
+ <div v-if="conf.fields?.animation?.options">
127
+ <p class="text-[10px] font-semibold uppercase tracking-widest text-slate-400 mb-2">Animation</p>
128
+ <div class="flex flex-wrap gap-1.5">
129
+ <button
130
+ v-for="opt in conf.fields.animation.options"
131
+ :key="opt.value"
132
+ @click="setPreset(String(type), 'animation', opt.value)"
133
+ :class="[
134
+ 'px-3 py-1.5 rounded-lg text-xs font-semibold border transition-all',
135
+ getPreset(String(type), 'animation', conf.defaultProps?.animation) === opt.value ? 'bg-slate-900 text-white border-slate-900' : 'bg-white text-slate-600 border-slate-200 hover:border-slate-400'
136
+ ]"
137
+ >
138
+ <span v-if="opt.icon" class="mr-1">{{ opt.icon }}</span>{{ opt.label }}
139
+ </button>
140
+ </div>
141
+ </div>
142
+
143
+ <!-- Radius + Spacing inline -->
144
+ <div class="flex gap-8">
145
+ <div v-if="conf.fields?.radius?.options">
146
+ <p class="text-[10px] font-semibold uppercase tracking-widest text-slate-400 mb-2">Arrondis</p>
147
+ <div class="flex gap-1.5">
148
+ <button
149
+ v-for="opt in conf.fields.radius.options"
150
+ :key="opt.value"
151
+ @click="setPreset(String(type), 'radius', opt.value)"
152
+ :class="[
153
+ 'px-3 py-1.5 rounded-lg text-xs font-semibold border transition-all',
154
+ getPreset(String(type), 'radius', conf.defaultProps?.radius) === opt.value ? 'bg-slate-900 text-white border-slate-900' : 'bg-white text-slate-600 border-slate-200 hover:border-slate-400'
155
+ ]"
156
+ >
157
+ <span v-if="opt.icon" class="mr-1">{{ opt.icon }}</span>{{ opt.label }}
158
+ </button>
159
+ </div>
160
+ </div>
161
+
162
+ <div v-if="conf.fields?.spacing?.options">
163
+ <p class="text-[10px] font-semibold uppercase tracking-widest text-slate-400 mb-2">Espacement</p>
164
+ <div class="flex gap-1.5">
165
+ <button
166
+ v-for="opt in conf.fields.spacing.options"
167
+ :key="opt.value"
168
+ @click="setPreset(String(type), 'spacing', opt.value)"
169
+ :class="[
170
+ 'px-3 py-1.5 rounded-lg text-xs font-semibold border transition-all',
171
+ getPreset(String(type), 'spacing', conf.defaultProps?.spacing) === opt.value ? 'bg-slate-900 text-white border-slate-900' : 'bg-white text-slate-600 border-slate-200 hover:border-slate-400'
172
+ ]"
173
+ >
174
+ <span v-if="opt.icon" class="mr-1">{{ opt.icon }}</span>{{ opt.label }}
175
+ </button>
176
+ </div>
177
+ </div>
178
+ </div>
179
+
180
+ <!-- Colors -->
181
+ <div v-if="conf.fields?.bgColor || conf.fields?.textColor" class="flex gap-6">
182
+ <div v-if="conf.fields?.bgColor">
183
+ <p class="text-[10px] font-semibold uppercase tracking-widest text-slate-400 mb-2">Fond</p>
184
+ <div class="flex items-center gap-2">
185
+ <input
186
+ type="color"
187
+ :value="getPreset(String(type), 'bgColor', conf.defaultProps?.bgColor || '#ffffff')"
188
+ @input="setPreset(String(type), 'bgColor', $event.target.value)"
189
+ class="w-8 h-8 rounded-lg border border-slate-200 cursor-pointer p-0.5"
190
+ />
191
+ <span class="text-xs font-mono text-slate-500">
192
+ {{ getPreset(String(type), "bgColor", conf.defaultProps?.bgColor || "#ffffff") }}
193
+ </span>
194
+ </div>
195
+ </div>
196
+ <div v-if="conf.fields?.textColor">
197
+ <p class="text-[10px] font-semibold uppercase tracking-widest text-slate-400 mb-2">Texte</p>
198
+ <div class="flex items-center gap-2">
199
+ <input
200
+ type="color"
201
+ :value="getPreset(String(type), 'textColor', conf.defaultProps?.textColor || '#000000')"
202
+ @input="setPreset(String(type), 'textColor', $event.target.value)"
203
+ class="w-8 h-8 rounded-lg border border-slate-200 cursor-pointer p-0.5"
204
+ />
205
+ <span class="text-xs font-mono text-slate-500">
206
+ {{ getPreset(String(type), "textColor", conf.defaultProps?.textColor || "#000000") }}
207
+ </span>
208
+ </div>
209
+ </div>
210
+ </div>
211
+
212
+ </div>
213
+ </div>
214
+ </div>
215
+ </div>
216
+
217
+ </div>
218
+ </div>
219
+ </AdminDashboardLayout>
220
+ </template>
@@ -0,0 +1,3 @@
1
+ declare const __VLS_export: any;
2
+ declare const _default: typeof __VLS_export;
3
+ export default _default;
@@ -1,8 +1,9 @@
1
1
  <script setup>
2
2
  import { ref, onMounted, useHead, useRoute, navigateTo } from "#imports";
3
- import { useAdminStrategies, useAdminContent, useAdminPersonas } from "#imports";
3
+ import { useAdminStrategies, useAdminContent, useAdminPersonas, useAdminBasePath } from "#imports";
4
4
  const route = useRoute();
5
5
  const id = route.params.id;
6
+ const basePath = useAdminBasePath();
6
7
  useHead({ title: "Strategie - Admin" });
7
8
  const { fetchStrategyById, updateStrategy, fetchStrategyContents, fetchStrategyPersonas } = useAdminStrategies();
8
9
  const { fetchContent, createContent } = useAdminContent();
@@ -74,7 +75,7 @@ const contentStatusClass = {
74
75
  <template>
75
76
  <AdminDashboardLayout>
76
77
  <div class="max-w-6xl mx-auto px-6 md:px-10 py-10">
77
- <NuxtLink to="/admin/strategies" class="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 mb-8 font-medium">
78
+ <NuxtLink :to="`${basePath}/strategies`" class="inline-flex items-center gap-2 text-blue-600 hover:text-blue-700 mb-8 font-medium">
78
79
  <Icon name="mdi:arrow-left" />
79
80
  Retour aux strategies
80
81
  </NuxtLink>
@@ -1,7 +1,8 @@
1
1
  <script setup>
2
2
  import { ref, computed, onMounted, useHead, navigateTo } from "#imports";
3
- import { useAdminStrategies } from "#imports";
3
+ import { useAdminStrategies, useAdminBasePath } from "#imports";
4
4
  useHead({ title: "Strategies - Admin" });
5
+ const basePath = useAdminBasePath();
5
6
  const { fetchStrategies, createStrategy, updateStrategy, deleteStrategy, fetchStrategyContents, fetchStrategyPersonas } = useAdminStrategies();
6
7
  const strategies = ref([]);
7
8
  const strategyContents = ref({});
@@ -125,7 +126,7 @@ const handleDelete = async () => {
125
126
  <p class="text-sm text-slate-600">{{ strategy.description }}</p>
126
127
  </div>
127
128
  <div class="flex gap-2 ml-4">
128
- <button @click="navigateTo(`/admin/strategies/${strategy.id}`)" class="px-5 py-2.5 bg-blue-600 text-white rounded-xl text-sm font-semibold hover:bg-blue-700 transition-all">Gerer</button>
129
+ <button @click="navigateTo(`${basePath}/strategies/${strategy.id}`)" class="px-5 py-2.5 bg-blue-600 text-white rounded-xl text-sm font-semibold hover:bg-blue-700 transition-all">Gerer</button>
129
130
  <button @click="openEdit(strategy)" class="p-2.5 bg-slate-100 text-slate-600 rounded-xl hover:bg-slate-200 transition-all"><Icon name="mdi:pencil" /></button>
130
131
  </div>
131
132
  </div>
@@ -1,9 +1,10 @@
1
+ import { defineEventHandler, readBody, createError } from "h3";
1
2
  const isI18nField = (val) => val !== null && typeof val === "object" && !Array.isArray(val) && ("fr" in val || "ar" in val || "en" in val);
2
3
  export default defineEventHandler(async (event) => {
3
4
  const body = await readBody(event);
4
- const { sectionType, sectionLabel, fields, currentProps, prompt, itemFieldsConfig } = body;
5
- const config = useRuntimeConfig();
6
- if (!config.openrouterApiKey) {
5
+ const { sectionType, sectionLabel, fields, currentProps, prompt, itemFieldsConfig, aiContext } = body;
6
+ const openrouterApiKey = process.env.OPENROUTER_API_KEY || "";
7
+ if (!openrouterApiKey) {
7
8
  throw createError({ statusCode: 500, message: "OPENROUTER_API_KEY not configured" });
8
9
  }
9
10
  const skeleton = {};
@@ -52,8 +53,13 @@ export default defineEventHandler(async (event) => {
52
53
  return `items[${i}]: "${typeof val === "object" ? val?.fr : val || ""}"`;
53
54
  }) : []
54
55
  ].join("\n");
55
- const systemPrompt = `You are a professional multilingual copywriter for a web agency CMS.
56
- Always respond with a valid JSON object only \u2014 no markdown, no code blocks, no explanation.`;
56
+ const systemPrompt = [
57
+ "You are a professional multilingual copywriter for a web agency CMS.",
58
+ aiContext ? `
59
+ Website context (always keep this in mind):
60
+ ${aiContext}` : "",
61
+ "\nAlways respond with a valid JSON object only \u2014 no markdown, no code blocks, no explanation."
62
+ ].filter(Boolean).join("");
57
63
  const userPrompt = `Section: ${sectionLabel} (${sectionType})
58
64
  User instruction: ${prompt}
59
65
 
@@ -77,7 +83,7 @@ Rules:
77
83
  const response = await $fetch("https://openrouter.ai/api/v1/chat/completions", {
78
84
  method: "POST",
79
85
  headers: {
80
- "Authorization": `Bearer ${config.openrouterApiKey}`,
86
+ "Authorization": `Bearer ${openrouterApiKey}`,
81
87
  "Content-Type": "application/json",
82
88
  "HTTP-Referer": "https://admin-cms.dev",
83
89
  "X-Title": "Admin CMS"
@@ -1,8 +1,9 @@
1
+ import { defineEventHandler, readBody, createError } from "h3";
1
2
  export default defineEventHandler(async (event) => {
2
3
  const body = await readBody(event);
3
- const { pageTitle, pageType, content, seoConfig } = body;
4
- const config = useRuntimeConfig();
5
- if (!config.openrouterApiKey) {
4
+ const { pageTitle, pageType, content, seoConfig, aiContext } = body;
5
+ const openrouterApiKey = process.env.OPENROUTER_API_KEY || "";
6
+ if (!openrouterApiKey) {
6
7
  throw createError({ statusCode: 500, message: "OPENROUTER_API_KEY not configured" });
7
8
  }
8
9
  const contentSummary = content.map((block) => {
@@ -25,8 +26,13 @@ export default defineEventHandler(async (event) => {
25
26
  title: typeof seoConfig?.meta_title === "object" ? seoConfig.meta_title?.fr : seoConfig?.meta_title || "",
26
27
  description: typeof seoConfig?.meta_description === "object" ? seoConfig.meta_description?.fr : seoConfig?.meta_description || ""
27
28
  };
28
- const systemPrompt = `You are an expert SEO consultant.
29
- Respond with a valid JSON object only \u2014 no markdown, no code blocks, no extra text.`;
29
+ const systemPrompt = [
30
+ "You are an expert SEO consultant.",
31
+ aiContext ? `
32
+ Website context:
33
+ ${aiContext}` : "",
34
+ "\nRespond with a valid JSON object only \u2014 no markdown, no code blocks, no extra text."
35
+ ].filter(Boolean).join("");
30
36
  const userPrompt = `Audit this web page for SEO and provide actionable recommendations.
31
37
 
32
38
  Page title: "${pageTitle}"
@@ -61,7 +67,7 @@ Rules:
61
67
  const response = await $fetch("https://openrouter.ai/api/v1/chat/completions", {
62
68
  method: "POST",
63
69
  headers: {
64
- "Authorization": `Bearer ${config.openrouterApiKey}`,
70
+ "Authorization": `Bearer ${openrouterApiKey}`,
65
71
  "Content-Type": "application/json",
66
72
  "HTTP-Referer": "https://admin-cms.dev",
67
73
  "X-Title": "Admin CMS"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cinqcinqdev-seo",
3
- "version": "0.1.15",
3
+ "version": "0.1.17",
4
4
  "description": "A reusable Nuxt 3 admin CMS module with visual page editor powered by Supabase",
5
5
  "license": "MIT",
6
6
  "module": "./dist/module.mjs",
@@ -27,6 +27,9 @@
27
27
  "@pinia/nuxt": ">=0.5.0 || >=0.11.0",
28
28
  "nuxt": ">=3.0.0"
29
29
  },
30
+ "dependencies": {
31
+ "h3": ">=1.0.0"
32
+ },
30
33
  "devDependencies": {
31
34
  "@nuxt/icon": "^1.15.0",
32
35
  "@nuxt/kit": "^3.13.0",