cinqcinqdev-seo 0.1.41 → 0.1.43
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
CHANGED
|
@@ -126,6 +126,20 @@ CREATE TABLE IF NOT EXISTS public.brand_settings (
|
|
|
126
126
|
);
|
|
127
127
|
|
|
128
128
|
INSERT INTO public.brand_settings (id) VALUES (1) ON CONFLICT (id) DO NOTHING;
|
|
129
|
+
|
|
130
|
+
-- Page templates (saved from the page list via "Turn into template")
|
|
131
|
+
CREATE TABLE IF NOT EXISTS public.pages_templates (
|
|
132
|
+
id uuid DEFAULT gen_random_uuid() PRIMARY KEY,
|
|
133
|
+
created_at timestamptz DEFAULT now(),
|
|
134
|
+
name text NOT NULL,
|
|
135
|
+
content jsonb DEFAULT '[]'::jsonb,
|
|
136
|
+
source_page_id uuid REFERENCES public.pages(id) ON DELETE SET NULL
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
ALTER TABLE public.pages_templates ENABLE ROW LEVEL SECURITY;
|
|
140
|
+
|
|
141
|
+
CREATE POLICY "auth_full_access" ON public.pages_templates
|
|
142
|
+
FOR ALL TO authenticated USING (true) WITH CHECK (true);
|
|
129
143
|
```
|
|
130
144
|
|
|
131
145
|
### Storage bucket
|
|
@@ -551,6 +565,66 @@ Auth state is read from `useSupabaseUser()` provided by `@nuxtjs/supabase`. Your
|
|
|
551
565
|
|
|
552
566
|
---
|
|
553
567
|
|
|
568
|
+
## Rendering Pages on the Frontend
|
|
569
|
+
|
|
570
|
+
Pages created in the CMS must be rendered by your Nuxt app. Since slugs can now contain slashes (e.g. `faq/my-question`), your route file **must use a catch-all segment**.
|
|
571
|
+
|
|
572
|
+
### Route file
|
|
573
|
+
|
|
574
|
+
Create `pages/[...slug].vue` (not `pages/[slug].vue`):
|
|
575
|
+
|
|
576
|
+
```
|
|
577
|
+
pages/
|
|
578
|
+
[...slug].vue ✅ matches /about, /faq/my-question, /services/web-design
|
|
579
|
+
[slug].vue ❌ only matches /about — breaks subdirectory slugs
|
|
580
|
+
```
|
|
581
|
+
|
|
582
|
+
### Example `pages/[...slug].vue`
|
|
583
|
+
|
|
584
|
+
```vue
|
|
585
|
+
<script setup>
|
|
586
|
+
const route = useRoute()
|
|
587
|
+
const supabase = useSupabaseClient()
|
|
588
|
+
|
|
589
|
+
// Join segments back into the full slug stored in the DB
|
|
590
|
+
const slug = computed(() => (route.params.slug as string[]).join('/'))
|
|
591
|
+
|
|
592
|
+
const { data: page } = await useAsyncData(`page-${slug.value}`, async () => {
|
|
593
|
+
const { data } = await supabase
|
|
594
|
+
.from('pages')
|
|
595
|
+
.select('*')
|
|
596
|
+
.eq('slug', slug.value)
|
|
597
|
+
.eq('status', 'published')
|
|
598
|
+
.single()
|
|
599
|
+
return data
|
|
600
|
+
})
|
|
601
|
+
|
|
602
|
+
if (!page.value) throw createError({ statusCode: 404, statusMessage: 'Page not found' })
|
|
603
|
+
|
|
604
|
+
useSeoMeta({
|
|
605
|
+
title: page.value.seo_config?.meta_title?.fr || page.value.title,
|
|
606
|
+
description: page.value.seo_config?.meta_description?.fr || '',
|
|
607
|
+
})
|
|
608
|
+
</script>
|
|
609
|
+
|
|
610
|
+
<template>
|
|
611
|
+
<div>
|
|
612
|
+
<component
|
|
613
|
+
v-for="section in page.content"
|
|
614
|
+
:key="section.id"
|
|
615
|
+
:is="`sections-${section.type}`"
|
|
616
|
+
v-bind="section.props"
|
|
617
|
+
/>
|
|
618
|
+
</div>
|
|
619
|
+
</template>
|
|
620
|
+
```
|
|
621
|
+
|
|
622
|
+
### How subdirectory slugs are stored
|
|
623
|
+
|
|
624
|
+
When a page is created with subdirectory `faq` and title `What is pricing`, the slug saved to the database is `faq/what-is-pricing`. The full URL on the frontend is `/faq/what-is-pricing`. There is no separate parent page or nested table — the slash is just part of the slug string.
|
|
625
|
+
|
|
626
|
+
---
|
|
627
|
+
|
|
554
628
|
## What's NOT Included
|
|
555
629
|
|
|
556
630
|
- Login / register pages — you provide these
|
package/dist/module.json
CHANGED
|
@@ -741,7 +741,7 @@ const savePage = async () => {
|
|
|
741
741
|
v-html="getRteProp(page.content[selectedBlockIndex].props, name)"
|
|
742
742
|
@blur="setRteProp(page.content[selectedBlockIndex].props, name, $event.target.innerHTML);
|
|
743
743
|
trigger()"
|
|
744
|
-
class="w-full min-h-[140px] bg-gray-50 rounded-xl p-3 text-[11px] leading-relaxed outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff] overflow-auto rte-field"
|
|
744
|
+
class="w-full min-h-[140px] bg-gray-50 rounded-xl p-3 text-[11px] text-gray-800 leading-relaxed outline-none ring-1 ring-gray-100 focus:ring-[#3d35ff] overflow-auto rte-field"
|
|
745
745
|
></div>
|
|
746
746
|
</div>
|
|
747
747
|
|
|
@@ -25,9 +25,17 @@ const showToast = (msg, type = "success") => {
|
|
|
25
25
|
};
|
|
26
26
|
const isCreating = ref(false);
|
|
27
27
|
const newPageTitle = ref("");
|
|
28
|
+
const newPageSubdir = ref("");
|
|
28
29
|
const createMode = ref("blank");
|
|
29
30
|
const templates = ref([]);
|
|
30
31
|
const selectedTemplateId = ref("");
|
|
32
|
+
const toSlugPart = (val) => val.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
|
|
33
|
+
const slugPreview = computed(() => {
|
|
34
|
+
const subdir = toSlugPart(newPageSubdir.value);
|
|
35
|
+
const slug = toSlugPart(newPageTitle.value);
|
|
36
|
+
if (!slug) return subdir ? `/${subdir}/\u2026` : "/\u2026";
|
|
37
|
+
return subdir ? `/${subdir}/${slug}` : `/${slug}`;
|
|
38
|
+
});
|
|
31
39
|
watch(isCreating, async (val) => {
|
|
32
40
|
if (val) {
|
|
33
41
|
const { data } = await supabase.from("pages_templates").select("id, name").order("created_at", { ascending: false });
|
|
@@ -36,11 +44,14 @@ watch(isCreating, async (val) => {
|
|
|
36
44
|
createMode.value = "blank";
|
|
37
45
|
selectedTemplateId.value = "";
|
|
38
46
|
newPageTitle.value = "";
|
|
47
|
+
newPageSubdir.value = "";
|
|
39
48
|
}
|
|
40
49
|
});
|
|
41
50
|
const handleCreate = async () => {
|
|
42
51
|
if (!newPageTitle.value) return;
|
|
43
|
-
const
|
|
52
|
+
const subdir = toSlugPart(newPageSubdir.value);
|
|
53
|
+
const base = toSlugPart(newPageTitle.value);
|
|
54
|
+
const slug = subdir ? `${subdir}/${base}` : base;
|
|
44
55
|
let content = [];
|
|
45
56
|
if (createMode.value === "template" && selectedTemplateId.value) {
|
|
46
57
|
const { data: tpl } = await supabase.from("pages_templates").select("content").eq("id", selectedTemplateId.value).single();
|
|
@@ -205,6 +216,12 @@ useHead({ title: computed(() => `${typeLabel.value} \u2014 Admin`) });
|
|
|
205
216
|
<input v-model="newPageTitle" type="text" placeholder="Ex: Création site web"
|
|
206
217
|
class="w-full border border-gray-200 p-3 rounded-xl text-sm text-gray-900 focus:ring-2 focus:ring-[#3d35ff] outline-none transition" autofocus />
|
|
207
218
|
</div>
|
|
219
|
+
<div>
|
|
220
|
+
<label class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-2 block">Sous-dossier <span class="normal-case font-normal">(optionnel)</span></label>
|
|
221
|
+
<input v-model="newPageSubdir" type="text" placeholder="Ex: faq"
|
|
222
|
+
class="w-full border border-gray-200 p-3 rounded-xl text-sm text-gray-900 focus:ring-2 focus:ring-[#3d35ff] outline-none transition" />
|
|
223
|
+
<p class="text-[11px] font-mono text-[#3d35ff] mt-1.5 opacity-70">{{ slugPreview }}</p>
|
|
224
|
+
</div>
|
|
208
225
|
<div>
|
|
209
226
|
<label class="text-[10px] font-black uppercase tracking-widest text-gray-400 mb-2 block">Départ</label>
|
|
210
227
|
<div class="grid grid-cols-2 gap-2">
|