cinqcinqdev-seo 0.1.42 → 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
@@ -4,7 +4,7 @@
4
4
  "compatibility": {
5
5
  "nuxt": ">=3.0.0"
6
6
  },
7
- "version": "0.1.42",
7
+ "version": "0.1.43",
8
8
  "builder": {
9
9
  "@nuxt/module-builder": "1.0.2",
10
10
  "unbuild": "unknown"
@@ -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 slug = newPageTitle.value.toLowerCase().normalize("NFD").replace(/[\u0300-\u036f]/g, "").replace(/[^a-z0-9]/g, "-").replace(/-+/g, "-").replace(/^-|-$/g, "");
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">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "cinqcinqdev-seo",
3
- "version": "0.1.42",
3
+ "version": "0.1.43",
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",