cinqcinqdev-seo 0.1.42 → 0.1.44
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 +81 -0
- package/dist/module.json +1 -1
- package/dist/runtime/pages/admin/pages/[type].vue +18 -1
- package/package.json +1 -1
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,73 @@ 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.
|
|
571
|
+
|
|
572
|
+
### Required route file: `pages/[...slug].vue`
|
|
573
|
+
|
|
574
|
+
Slugs can contain slashes (e.g. `faq/my-question` when a subdirectory is set). A standard `[slug].vue` file **only matches a single path segment** — it will never match `/faq/my-question` and will return 404.
|
|
575
|
+
|
|
576
|
+
You must use a **catch-all** route:
|
|
577
|
+
|
|
578
|
+
| File | Matches |
|
|
579
|
+
|---|---|
|
|
580
|
+
| `pages/[...slug].vue` ✅ | `/about`, `/faq/pricing`, `/services/web-design` |
|
|
581
|
+
| `pages/[slug].vue` ❌ | `/about` only — `/faq/pricing` returns 404 |
|
|
582
|
+
|
|
583
|
+
> **If you already have `pages/[slug].vue`**, rename it to `pages/[...slug].vue` and update the slug reference as shown below. Both flat slugs and subdirectory slugs will continue to work.
|
|
584
|
+
|
|
585
|
+
### `pages/[...slug].vue`
|
|
586
|
+
|
|
587
|
+
```vue
|
|
588
|
+
<script setup>
|
|
589
|
+
const route = useRoute()
|
|
590
|
+
const supabase = useSupabaseClient()
|
|
591
|
+
|
|
592
|
+
// route.params.slug is an array of path segments — join them back into the DB slug
|
|
593
|
+
// e.g. /faq/pricing → ['faq', 'pricing'] → 'faq/pricing'
|
|
594
|
+
// e.g. /about → ['about'] → 'about'
|
|
595
|
+
const slug = (route.params.slug as string[]).join('/')
|
|
596
|
+
|
|
597
|
+
const { data: page } = await useAsyncData(`page-${slug}`, async () => {
|
|
598
|
+
const { data } = await supabase
|
|
599
|
+
.from('pages')
|
|
600
|
+
.select('*')
|
|
601
|
+
.eq('slug', slug)
|
|
602
|
+
.eq('status', 'published')
|
|
603
|
+
.single()
|
|
604
|
+
return data
|
|
605
|
+
})
|
|
606
|
+
|
|
607
|
+
if (!page.value) throw createError({ statusCode: 404, statusMessage: 'Page not found' })
|
|
608
|
+
|
|
609
|
+
useSeoMeta({
|
|
610
|
+
title: page.value.seo_config?.meta_title?.fr || page.value.title,
|
|
611
|
+
description: page.value.seo_config?.meta_description?.fr || '',
|
|
612
|
+
})
|
|
613
|
+
</script>
|
|
614
|
+
|
|
615
|
+
<template>
|
|
616
|
+
<div>
|
|
617
|
+
<component
|
|
618
|
+
v-for="section in page.content"
|
|
619
|
+
:key="section.id"
|
|
620
|
+
:is="`sections-${section.type}`"
|
|
621
|
+
v-bind="section.props"
|
|
622
|
+
/>
|
|
623
|
+
</div>
|
|
624
|
+
</template>
|
|
625
|
+
```
|
|
626
|
+
|
|
627
|
+
### How subdirectory slugs are stored
|
|
628
|
+
|
|
629
|
+
When a page is created with subdirectory `faq` and title `What is pricing`, the slug stored in the database is `faq/what-is-pricing`. The frontend URL is `/faq/what-is-pricing`. There is no nested table or parent record — the slash is just part of the slug string.
|
|
630
|
+
|
|
631
|
+
Pages without a subdirectory (e.g. title `About`, no subdirectory) get a plain slug like `about` and are served at `/about` — exactly as before.
|
|
632
|
+
|
|
633
|
+
---
|
|
634
|
+
|
|
554
635
|
## What's NOT Included
|
|
555
636
|
|
|
556
637
|
- Login / register pages — you provide these
|
package/dist/module.json
CHANGED
|
@@ -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">
|