feat: Virtual page redirect, breadcrumb & menu items
All checks were successful
Deploy WordPress and Nuxt / deploy (push) Successful in 1m5s

This commit is contained in:
2025-09-25 21:27:41 -04:00
parent 3f0d4dbb4e
commit e9c92840fc
12 changed files with 155 additions and 134 deletions

View File

@@ -71,7 +71,7 @@ Use GitHub-style checkboxes to mark task completion:
- [x] Configure GraphQL schema for ACF fields <!-- Completed: wpgraphql-acf plugin configured -->
- [x] Test GraphQL queries for all content types <!-- Completed: Basic .gql files created -->
- [x] Set up GraphQL authentication and permissions <!-- Completed: JWT auth configured with includes/graphql/auth.php -->
- [ ] Customize MediaItem GraphQL type to include center (x/y) fields <!-- Priority: High - Image cropping and point-of-interest support -->
- [x] Customize MediaItem GraphQL type to include center (x/y) fields <!-- Priority: High - Image cropping and point-of-interest support -->
---
@@ -157,7 +157,7 @@ Use GitHub-style checkboxes to mark task completion:
### Content Features
- [x] Implement image uploads with point-of-interest cropping
- [ ] Implement virtual pages (redirect to first child)
- [x] Implement virtual pages (redirect to first child)
- [ ] Create automated URL redirect system
- [ ] Build content search and filtering
- [ ] Create content categorization system

View File

@@ -62,5 +62,5 @@
"graphql_types": "",
"acfe_meta": "",
"acfe_note": "",
"modified": 1757959232
"modified": 1758829149
}

View File

@@ -1,5 +1,13 @@
fragment ThePage on Page {
title
template {
__typename
}
children {
nodes {
uri
}
}
groupPostPage {
sections {
...TheSection

View File

@@ -2,6 +2,11 @@
import type { ThePageFragment } from "#graphql-operations";
const props = defineProps<ThePageFragment>();
if (props.template?.__typename === "Template_VirtualPage") {
await navigateTo(props.children?.nodes[0]?.uri || "/");
}
useSeoMeta({ title: props.title });
</script>

View File

@@ -1,7 +1,13 @@
export async function useSiteOptions() {
const { data } = await useAsyncGraphqlQuery("siteOptions", {}, { graphqlCaching: { client: true } });
if (data.value?.errors?.length || !data.value?.data.siteOptions?.groupCcat) {
throw createError({ statusCode: 500, message: "Erreur lors de la récupération des options du site" });
const { data, error } = await useAsyncGraphqlQuery("siteOptions", {}, { graphqlCaching: { client: true } });
if (error.value) {
throw createError({ statusCode: 500, statusMessage: "Erreur interne", message: error.value.message });
}
if (data.value?.errors.length) {
throw createError({ statusCode: 500, statusMessage: "Erreur interne", message: data.value.errors.map((error) => error.message).join("\n") });
}
if (!data.value?.data.siteOptions?.groupCcat) {
throw createError({ statusCode: 500, statusMessage: "Erreur interne", message: "Options du site invalides." });
}
return { ...data.value?.data.siteOptions?.groupCcat };
}

View File

@@ -5,6 +5,7 @@
// Core
require_once __DIR__ . '/includes/core/sections.php';
require_once __DIR__ . '/includes/core/theme-setup.php';
require_once __DIR__ . '/includes/core/virtual-page.php';
// Custom Post Types
require_once __DIR__ . '/includes/cpt/contributor.php';

View File

@@ -0,0 +1,18 @@
<?php
// Helper: Determine if page is a virtual page
function ccat_is_virtual_page( $page_id ) {
return get_post_meta( $page_id, '_wp_page_template', true ) === 'virtual.php';
}
// Replace menu item path with '#' for virtual pages
add_filter( 'graphql_resolve_field', 'ccat_virtual_page_menu_item_graphql_value', 10, 5 );
function ccat_virtual_page_menu_item_graphql_value( $result, $source, $args, $context, $info ) {
if ( $source instanceof \WPGraphQL\Model\MenuItem && $info->fieldName === 'path' ) {
$menu_item = wp_setup_nav_menu_item( get_post( $source->databaseId ) );
if ( $menu_item->object === 'page' && ccat_is_virtual_page( $menu_item->object_id ) ) {
return '#';
}
}
return $result;
}

View File

@@ -49,102 +49,77 @@ function ccat_register_breadcrumb_graphql_fields() {
// Helper: Get breadcrumbs for a given node
function ccat_get_breadcrumbs( $node ) {
$breadcrumbs = array();
if ( ! $node ) {
return $breadcrumbs;
}
if ( $node instanceof \WPGraphQL\Model\Post ) {
$post = get_post( $node->databaseId );
if ( $post ) {
$breadcrumbs = ccat_get_post_breadcrumbs( $post );
$breadcrumbs[] = array(
'label' => get_the_title( $post->ID ),
'to' => null,
);
return ccat_get_post_breadcrumbs( $node->databaseId );
}
} elseif ( $node instanceof \WPGraphQL\Model\Term ) {
$term = get_term( $node->databaseId );
if ( $term && ! is_wp_error( $term ) ) {
$breadcrumbs = ccat_get_term_breadcrumbs( $term );
$breadcrumbs[] = array(
'label' => $term->name,
'to' => null,
);
if ( $node instanceof \WPGraphQL\Model\Term ) {
return ccat_get_term_breadcrumbs( $node->databaseId );
}
}
return $breadcrumbs;
return array();
}
// Helper: Get breadcrumbs for a given post
function ccat_get_post_breadcrumbs( WP_Post $post ) {
// Helper: Get breadcrumbs for a given post ID
function ccat_get_post_breadcrumbs( $post_id ) {
$breadcrumbs = array();
$is_front_page = get_option( 'show_on_front' ) === 'page' && (int) get_option( 'page_on_front' ) === $post->ID;
$is_front_page = get_option( 'show_on_front' ) === 'page' && (int) get_option( 'page_on_front' ) === $post_id;
if ( $is_front_page ) {
return $breadcrumbs; // No breadcrumbs for the front page
} else {
return $breadcrumbs;
}
$breadcrumbs[] = array(
'label' => 'Accueil',
'to' => '/',
);
}
$post_type = get_post_type( $post->ID );
switch ( $post_type ) {
case 'page':
$ancestors = get_post_ancestors( $post->ID );
if ( ! empty( $ancestors ) ) {
$ancestors = array_reverse( $ancestors );
foreach ( $ancestors as $ancestor_id ) {
$breadcrumbs[] = array(
'label' => get_the_title( $ancestor_id ),
'to' => str_replace( home_url(), '', get_permalink( $ancestor_id ) ),
);
}
}
break;
default:
if ( ! empty( $post_id = get_option( "page_for_$post_type" ) ) ) {
// Get ancestors of the archive page
$ancestors = get_post_ancestors( $post_id );
if ( ! empty( $ancestors ) ) {
$ancestors = array_reverse( $ancestors );
foreach ( $ancestors as $ancestor_id ) {
$post_type = get_post_type( $post_id );
$ancestor_ids = get_post_ancestors( $post_type === 'page' ? $post_id : get_option( "page_for_$post_type" ) );
$ancestor_ids = array_reverse( $ancestor_ids );
foreach ( $ancestor_ids as $ancestor_id ) {
$breadcrumbs[] = array(
'label' => get_the_title( $ancestor_id ),
'to' => str_replace( home_url(), '', get_permalink( $ancestor_id ) ),
'to' => ccat_is_virtual_page( $ancestor_id ) ? null : str_replace( home_url(), '', get_permalink( $ancestor_id ) ),
);
}
}
// Add the archive page itself
$breadcrumbs[] = array(
'label' => get_the_title( $post_id ),
'to' => str_replace( home_url(), '', get_permalink( $post_id ) ),
'to' => null,
);
}
break;
}
return $breadcrumbs;
}
// Helper: Get breadcrumbs for a given term
function ccat_get_term_breadcrumbs( WP_Term $term ) {
$breadcrumbs = array();
$taxonomy = $term->taxonomy;
if ( $term->parent ) {
$ancestors = get_ancestors( $term->term_id, $taxonomy );
if ( ! empty( $ancestors ) ) {
$ancestors = array_reverse( $ancestors );
foreach ( $ancestors as $ancestor_id ) {
$ancestor_term = get_term( $ancestor_id, $taxonomy );
if ( $ancestor_term && ! is_wp_error( $ancestor_term ) ) {
// Helper: Get breadcrumbs for a given term ID
function ccat_get_term_breadcrumbs( $term_id ) {
$breadcrumbs = array(
array(
'label' => 'Accueil',
'to' => '/',
),
);
$term = get_term( $term_id );
if ( is_wp_error( $term ) || ! $term ) {
return $breadcrumbs;
}
$ancestor_ids = get_ancestors( $term->term_id, $term->taxonomy );
$ancestor_ids = array_reverse( $ancestor_ids );
foreach ( $ancestor_ids as $ancestor_id ) {
$ancestor_term = get_term( $ancestor_id, $term->taxonomy );
if ( is_wp_error( $ancestor_term ) || ! $ancestor_term ) {
continue;
}
$breadcrumbs[] = array(
'label' => $ancestor_term->name,
'to' => str_replace( home_url(), '', get_term_link( $ancestor_term ) ),
);
}
}
}
}
$breadcrumbs[] = array(
'label' => $term->name,
'to' => null,
);
return $breadcrumbs;
}

View File

@@ -39,7 +39,7 @@
"eslint": "^9.36.0",
"typescript": "^5.9.2",
"vue-tsc": "^3.0.8",
"wrangler": "^4.40.0"
"wrangler": "^4.40.1"
},
"packageManager": "pnpm@10.15.0",
"pnpm": {

View File

@@ -73,8 +73,8 @@ importers:
specifier: ^3.0.8
version: 3.0.8(typescript@5.9.2)
wrangler:
specifier: ^4.40.0
version: 4.40.0(@cloudflare/workers-types@4.20250924.0)
specifier: ^4.40.1
version: 4.40.1(@cloudflare/workers-types@4.20250924.0)
packages:
@@ -106,8 +106,8 @@ packages:
resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==}
engines: {node: '>=18'}
'@ai-sdk/vue@2.0.52':
resolution: {integrity: sha512-r2p+Z0DQNckgIyzUz7CzCJfGoOnwV8Z7HYYb0AuR3+maCqlpKQlovDDp3e171Z2WY+JBWof1UYSnOlcrWIrQig==}
'@ai-sdk/vue@2.0.53':
resolution: {integrity: sha512-IZ0a5oF82tjpfQbjWieTmuzNlovrH5xjunSaHdOcvNCVRSnJiiguHuf/NJXO2999OsfTRFRJO8KAbjogPltp7w==}
engines: {node: '>=18'}
peerDependencies:
vue: ^3.3.4
@@ -1009,8 +1009,8 @@ packages:
'@iconify-json/lucide@1.2.68':
resolution: {integrity: sha512-lR5xNJdn2CT0iR7lM25G4SewBO4G2hbr3fTWOc3AE9BspflEcneh02E3l9TBaCU/JOHozTJevWLrxBGypD7Tng==}
'@iconify/collections@1.0.597':
resolution: {integrity: sha512-itoqOLcEmgoj+4I92R+PUAnSyhDWN7Ez1QjQvp08ww5EMVeAM5x3Zt3Snf1MS7R5SMM9rl3h7kPRY6+m6g4BwQ==}
'@iconify/collections@1.0.598':
resolution: {integrity: sha512-Mm3gHh0gwgi2T6sYygmCF9A+QirG8E+yCFqLd2zjXNmacbQ4ng0bHTP9QRZqNh96ao4YxxyKAcZyedjW35Wevw==}
'@iconify/types@2.0.0':
resolution: {integrity: sha512-+wluvCrRhXrhyOmRDJ3q8mux9JkKy5SJ/v8ol2tu4FVjyYvtEzkc/3pK15ET6RKg4b4w4BmTk1+gsCUhf21Ykg==}
@@ -2544,8 +2544,8 @@ packages:
resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==}
engines: {node: '>=8'}
ai@5.0.52:
resolution: {integrity: sha512-GLlRHjMlvN9+w7UYGxCpUQ8GgCRv5Z+JCprRH3Q8YbXJ/JyIc6EP9+YRUmQsyExX/qQsuehe7y/LLygarbSTOw==}
ai@5.0.53:
resolution: {integrity: sha512-TIRelwDRczBS6vGLbJlskC1jLRKUT7DAYz+1DiHjJrgx1MnE2a5xQWv06koLHT+r0GImpkmqP77/n+Vbvea3aw==}
engines: {node: '>=18'}
peerDependencies:
zod: ^3.25.76 || ^4
@@ -3267,8 +3267,8 @@ packages:
ee-first@1.1.1:
resolution: {integrity: sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==}
electron-to-chromium@1.5.223:
resolution: {integrity: sha512-qKm55ic6nbEmagFlTFczML33rF90aU+WtrJ9MdTCThrcvDNdUHN4p6QfVN78U06ZmguqXIyMPyYhw2TrbDUwPQ==}
electron-to-chromium@1.5.224:
resolution: {integrity: sha512-kWAoUu/bwzvnhpdZSIc6KUyvkI1rbRXMT0Eq8pKReyOyaPZcctMli+EgvcN1PAvwVc7Tdo4Fxi2PsLNDU05mdg==}
embla-carousel-auto-height@8.6.0:
resolution: {integrity: sha512-/HrJQOEM6aol/oF33gd2QlINcXy3e19fJWvHDuHWp2bpyTa+2dm9tVVJak30m2Qy6QyQ6Fc8DkImtv7pxWOJUQ==}
@@ -6157,8 +6157,8 @@ packages:
engines: {node: '>=16'}
hasBin: true
wrangler@4.40.0:
resolution: {integrity: sha512-HCPNUz599h9emi6qjppn92kxS6E12QvD0A3K087CZFEysw6lO1saZUdJ8azk0LsYGYDnrkBs5TzUOEfpuccwWA==}
wrangler@4.40.1:
resolution: {integrity: sha512-XQEHOW6g1zW2xnBq1dmhDbEiVBbPGh7mX1tQAUMqKETa61XXvxHCJSzVI3is5xuo9HzZ8ITzg4VnhB/91cg9DQ==}
engines: {node: '>=18.0.0'}
hasBin: true
peerDependencies:
@@ -6304,10 +6304,10 @@ snapshots:
dependencies:
json-schema: 0.4.0
'@ai-sdk/vue@2.0.52(vue@3.5.22(typescript@5.9.2))(zod@4.1.11)':
'@ai-sdk/vue@2.0.53(vue@3.5.22(typescript@5.9.2))(zod@4.1.11)':
dependencies:
'@ai-sdk/provider-utils': 3.0.9(zod@4.1.11)
ai: 5.0.52(zod@4.1.11)
ai: 5.0.53(zod@4.1.11)
swrv: 1.1.0(vue@3.5.22(typescript@5.9.2))
optionalDependencies:
vue: 3.5.22(typescript@5.9.2)
@@ -7354,7 +7354,7 @@ snapshots:
dependencies:
'@iconify/types': 2.0.0
'@iconify/collections@1.0.597':
'@iconify/collections@1.0.598':
dependencies:
'@iconify/types': 2.0.0
@@ -7815,7 +7815,7 @@ snapshots:
'@nuxt/icon@2.0.0(magicast@0.3.5)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue@3.5.22(typescript@5.9.2))':
dependencies:
'@iconify/collections': 1.0.597
'@iconify/collections': 1.0.598
'@iconify/types': 2.0.0
'@iconify/utils': 3.0.2
'@iconify/vue': 5.0.0(vue@3.5.22(typescript@5.9.2))
@@ -7957,7 +7957,7 @@ snapshots:
'@nuxt/ui@4.0.0(@babel/parser@7.28.4)(@netlify/blobs@9.1.2)(change-case@5.4.4)(db0@0.3.2)(embla-carousel@8.6.0)(ioredis@5.8.0)(jwt-decode@4.0.0)(magicast@0.3.5)(typescript@5.9.2)(vite@7.1.7(@types/node@24.5.2)(jiti@2.6.0)(lightningcss@1.30.1)(terser@5.44.0)(yaml@2.8.1))(vue-router@4.5.1(vue@3.5.22(typescript@5.9.2)))(vue@3.5.22(typescript@5.9.2))(zod@4.1.11)':
dependencies:
'@ai-sdk/vue': 2.0.52(vue@3.5.22(typescript@5.9.2))(zod@4.1.11)
'@ai-sdk/vue': 2.0.53(vue@3.5.22(typescript@5.9.2))(zod@4.1.11)
'@iconify/vue': 5.0.0(vue@3.5.22(typescript@5.9.2))
'@internationalized/date': 3.9.0
'@internationalized/number': 3.6.5
@@ -9223,7 +9223,7 @@ snapshots:
clean-stack: 2.2.0
indent-string: 4.0.0
ai@5.0.52(zod@4.1.11):
ai@5.0.53(zod@4.1.11):
dependencies:
'@ai-sdk/gateway': 1.0.29(zod@4.1.11)
'@ai-sdk/provider': 2.0.0
@@ -9407,7 +9407,7 @@ snapshots:
dependencies:
baseline-browser-mapping: 2.8.7
caniuse-lite: 1.0.30001745
electron-to-chromium: 1.5.223
electron-to-chromium: 1.5.224
node-releases: 2.0.21
update-browserslist-db: 1.1.3(browserslist@4.26.2)
@@ -9481,7 +9481,7 @@ snapshots:
camel-case@4.1.2:
dependencies:
pascal-case: 3.1.2
tslib: 2.6.3
tslib: 2.8.1
caniuse-api@3.0.0:
dependencies:
@@ -9495,7 +9495,7 @@ snapshots:
capital-case@1.0.4:
dependencies:
no-case: 3.0.4
tslib: 2.6.3
tslib: 2.8.1
upper-case-first: 2.0.2
case-anything@3.1.2: {}
@@ -9533,7 +9533,7 @@ snapshots:
path-case: 3.0.4
sentence-case: 3.0.4
snake-case: 3.0.4
tslib: 2.6.3
tslib: 2.8.1
change-case@5.4.4: {}
@@ -9658,7 +9658,7 @@ snapshots:
constant-case@3.0.4:
dependencies:
no-case: 3.0.4
tslib: 2.6.3
tslib: 2.8.1
upper-case: 2.0.2
convert-source-map@2.0.0: {}
@@ -9903,7 +9903,7 @@ snapshots:
dot-case@3.0.4:
dependencies:
no-case: 3.0.4
tslib: 2.6.3
tslib: 2.8.1
dot-prop@9.0.0:
dependencies:
@@ -9928,7 +9928,7 @@ snapshots:
ee-first@1.1.1: {}
electron-to-chromium@1.5.223: {}
electron-to-chromium@1.5.224: {}
embla-carousel-auto-height@8.6.0(embla-carousel@8.6.0):
dependencies:
@@ -10648,7 +10648,7 @@ snapshots:
header-case@2.0.4:
dependencies:
capital-case: 1.0.4
tslib: 2.6.3
tslib: 2.8.1
hey-listen@1.0.8: {}
@@ -10852,7 +10852,7 @@ snapshots:
is-lower-case@2.0.2:
dependencies:
tslib: 2.6.3
tslib: 2.8.1
is-module@1.0.0: {}
@@ -10884,7 +10884,7 @@ snapshots:
is-upper-case@2.0.2:
dependencies:
tslib: 2.6.3
tslib: 2.8.1
is-what@4.1.16: {}
@@ -11130,11 +11130,11 @@ snapshots:
lower-case-first@2.0.2:
dependencies:
tslib: 2.6.3
tslib: 2.8.1
lower-case@2.0.2:
dependencies:
tslib: 2.6.3
tslib: 2.8.1
lru-cache@10.4.3: {}
@@ -11430,7 +11430,7 @@ snapshots:
no-case@3.0.4:
dependencies:
lower-case: 2.0.2
tslib: 2.6.3
tslib: 2.8.1
node-abi@3.77.0:
dependencies:
@@ -11876,7 +11876,7 @@ snapshots:
param-case@3.0.4:
dependencies:
dot-case: 3.0.4
tslib: 2.6.3
tslib: 2.8.1
parent-module@1.0.1:
dependencies:
@@ -11918,14 +11918,14 @@ snapshots:
pascal-case@3.1.2:
dependencies:
no-case: 3.0.4
tslib: 2.6.3
tslib: 2.8.1
path-browserify@1.0.1: {}
path-case@3.0.4:
dependencies:
dot-case: 3.0.4
tslib: 2.6.3
tslib: 2.8.1
path-exists@4.0.0: {}
@@ -12427,7 +12427,7 @@ snapshots:
sentence-case@3.0.4:
dependencies:
no-case: 3.0.4
tslib: 2.6.3
tslib: 2.8.1
upper-case-first: 2.0.2
serialize-javascript@6.0.2:
@@ -12596,7 +12596,7 @@ snapshots:
snake-case@3.0.4:
dependencies:
dot-case: 3.0.4
tslib: 2.6.3
tslib: 2.8.1
source-map-js@1.2.1: {}
@@ -12622,7 +12622,7 @@ snapshots:
sponge-case@1.0.1:
dependencies:
tslib: 2.6.3
tslib: 2.8.1
stable-hash-x@0.2.0: {}
@@ -12741,7 +12741,7 @@ snapshots:
swap-case@2.0.2:
dependencies:
tslib: 2.6.3
tslib: 2.8.1
swrv@1.1.0(vue@3.5.22(typescript@5.9.2)):
dependencies:
@@ -12842,7 +12842,7 @@ snapshots:
title-case@3.0.3:
dependencies:
tslib: 2.6.3
tslib: 2.8.1
to-regex-range@5.0.1:
dependencies:
@@ -13097,11 +13097,11 @@ snapshots:
upper-case-first@2.0.2:
dependencies:
tslib: 2.6.3
tslib: 2.8.1
upper-case@2.0.2:
dependencies:
tslib: 2.6.3
tslib: 2.8.1
uqr@0.1.2: {}
@@ -13300,7 +13300,7 @@ snapshots:
'@cloudflare/workerd-linux-arm64': 1.20250924.0
'@cloudflare/workerd-windows-64': 1.20250924.0
wrangler@4.40.0(@cloudflare/workers-types@4.20250924.0):
wrangler@4.40.1(@cloudflare/workers-types@4.20250924.0):
dependencies:
'@cloudflare/kv-asset-handler': 0.4.0
'@cloudflare/unenv-preset': 2.7.4(unenv@2.0.0-rc.21)(workerd@1.20250924.0)

View File

@@ -24891,6 +24891,12 @@ type TemplateToTemplateConnectionPageInfo implements PageInfo & TemplateConnecti
startCursor: String
}
"""The template assigned to the node"""
type Template_VirtualPage implements ContentTemplate {
"""The name of the template"""
templateName: String
}
"""
Base interface for taxonomy terms such as categories and tags. Terms are used to organize and classify content.
"""

View File

@@ -0,0 +1,2 @@
<?php
/** Template Name: Virtual Page */