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.
- 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.d.vue.ts +10 -0
- package/dist/runtime/components/sections/ContactForm.vue +138 -0
- package/dist/runtime/components/sections/ContactForm.vue.d.ts +10 -0
- package/dist/runtime/components/sections/FAQ.d.vue.ts +3 -0
- package/dist/runtime/components/sections/FAQ.vue +46 -0
- package/dist/runtime/components/sections/FAQ.vue.d.ts +3 -0
- package/dist/runtime/components/sections/Features.d.vue.ts +3 -0
- package/dist/runtime/components/sections/Features.vue +41 -0
- package/dist/runtime/components/sections/Features.vue.d.ts +3 -0
- package/dist/runtime/components/sections/HeroSection.d.vue.ts +3 -0
- package/dist/runtime/components/sections/HeroSection.vue +133 -0
- package/dist/runtime/components/sections/HeroSection.vue.d.ts +3 -0
- package/dist/runtime/components/sections/Pricing.d.vue.ts +3 -0
- package/dist/runtime/components/sections/Pricing.vue +108 -0
- package/dist/runtime/components/sections/Pricing.vue.d.ts +3 -0
- package/dist/runtime/components/sections/Process.d.vue.ts +3 -0
- package/dist/runtime/components/sections/Process.vue +95 -0
- package/dist/runtime/components/sections/Process.vue.d.ts +3 -0
- package/dist/runtime/components/sections/ServiceGrid.d.vue.ts +3 -0
- package/dist/runtime/components/sections/ServiceGrid.vue +97 -0
- package/dist/runtime/components/sections/ServiceGrid.vue.d.ts +3 -0
- package/dist/runtime/components/sections/Testimonials.d.vue.ts +3 -0
- package/dist/runtime/components/sections/Testimonials.vue +95 -0
- package/dist/runtime/components/sections/Testimonials.vue.d.ts +3 -0
- package/dist/runtime/components/sections/TextVisual.d.vue.ts +3 -0
- package/dist/runtime/components/sections/TextVisual.vue +94 -0
- package/dist/runtime/components/sections/TextVisual.vue.d.ts +3 -0
- 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 +3 -1
- 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 +8 -3
- package/dist/runtime/server/api/ai/seo-audit.post.js +8 -3
- package/package.json +1 -1
|
@@ -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(
|
|
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(
|
|
92
|
+
const viewPersona = (persona) => navigateTo(`${basePath}/personas/${persona.id}`);
|
|
92
93
|
</script>
|
|
93
94
|
|
|
94
95
|
<template>
|
|
@@ -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>
|
|
@@ -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="/
|
|
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(
|
|
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,7 +1,7 @@
|
|
|
1
1
|
const isI18nField = (val) => val !== null && typeof val === "object" && !Array.isArray(val) && ("fr" in val || "ar" in val || "en" in val);
|
|
2
2
|
export default defineEventHandler(async (event) => {
|
|
3
3
|
const body = await readBody(event);
|
|
4
|
-
const { sectionType, sectionLabel, fields, currentProps, prompt, itemFieldsConfig } = body;
|
|
4
|
+
const { sectionType, sectionLabel, fields, currentProps, prompt, itemFieldsConfig, aiContext } = body;
|
|
5
5
|
const config = useRuntimeConfig();
|
|
6
6
|
if (!config.openrouterApiKey) {
|
|
7
7
|
throw createError({ statusCode: 500, message: "OPENROUTER_API_KEY not configured" });
|
|
@@ -52,8 +52,13 @@ export default defineEventHandler(async (event) => {
|
|
|
52
52
|
return `items[${i}]: "${typeof val === "object" ? val?.fr : val || ""}"`;
|
|
53
53
|
}) : []
|
|
54
54
|
].join("\n");
|
|
55
|
-
const systemPrompt =
|
|
56
|
-
|
|
55
|
+
const systemPrompt = [
|
|
56
|
+
"You are a professional multilingual copywriter for a web agency CMS.",
|
|
57
|
+
aiContext ? `
|
|
58
|
+
Website context (always keep this in mind):
|
|
59
|
+
${aiContext}` : "",
|
|
60
|
+
"\nAlways respond with a valid JSON object only \u2014 no markdown, no code blocks, no explanation."
|
|
61
|
+
].filter(Boolean).join("");
|
|
57
62
|
const userPrompt = `Section: ${sectionLabel} (${sectionType})
|
|
58
63
|
User instruction: ${prompt}
|
|
59
64
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export default defineEventHandler(async (event) => {
|
|
2
2
|
const body = await readBody(event);
|
|
3
|
-
const { pageTitle, pageType, content, seoConfig } = body;
|
|
3
|
+
const { pageTitle, pageType, content, seoConfig, aiContext } = body;
|
|
4
4
|
const config = useRuntimeConfig();
|
|
5
5
|
if (!config.openrouterApiKey) {
|
|
6
6
|
throw createError({ statusCode: 500, message: "OPENROUTER_API_KEY not configured" });
|
|
@@ -25,8 +25,13 @@ export default defineEventHandler(async (event) => {
|
|
|
25
25
|
title: typeof seoConfig?.meta_title === "object" ? seoConfig.meta_title?.fr : seoConfig?.meta_title || "",
|
|
26
26
|
description: typeof seoConfig?.meta_description === "object" ? seoConfig.meta_description?.fr : seoConfig?.meta_description || ""
|
|
27
27
|
};
|
|
28
|
-
const systemPrompt =
|
|
29
|
-
|
|
28
|
+
const systemPrompt = [
|
|
29
|
+
"You are an expert SEO consultant.",
|
|
30
|
+
aiContext ? `
|
|
31
|
+
Website context:
|
|
32
|
+
${aiContext}` : "",
|
|
33
|
+
"\nRespond with a valid JSON object only \u2014 no markdown, no code blocks, no extra text."
|
|
34
|
+
].filter(Boolean).join("");
|
|
30
35
|
const userPrompt = `Audit this web page for SEO and provide actionable recommendations.
|
|
31
36
|
|
|
32
37
|
Page title: "${pageTitle}"
|