@shopbite-de/storefront 1.10.1 → 1.11.0
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/app/components/Category/Breadcrumb.vue +74 -0
- package/app/components/Category/Header.vue +20 -38
- package/app/components/Category/Listing.vue +7 -6
- package/app/components/Hero.vue +2 -1
- package/app/components/Navigation/DesktopLeft2.vue +16 -16
- package/app/components/Navigation/MobileTop2.vue +31 -11
- package/app/components/User/RegistrationForm.vue +1 -3
- package/app/composables/useCategorySeo.ts +0 -55
- package/app/layouts/listing2.vue +1 -1
- package/app/pages/speisekarte/[...all].vue +52 -0
- package/content/index.yml +3 -3
- package/node.dockerfile +1 -1
- package/package.json +1 -1
- package/public/category-placeholder.webp +0 -0
- package/test/e2e/simple-checkout-as-recurring-customer.test.ts +1 -1
- package/test/unit/useCategorySeo.spec.ts +1 -15
- package/app/pages/speisekarte.vue +0 -57
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
|
+
import type { BreadcrumbItem } from "#ui/components/Breadcrumb.vue";
|
|
4
|
+
|
|
5
|
+
const props = defineProps<{
|
|
6
|
+
categoryId: string | undefined;
|
|
7
|
+
}>();
|
|
8
|
+
|
|
9
|
+
const { categoryId } = toRefs(props);
|
|
10
|
+
|
|
11
|
+
const { apiClient } = useShopwareContext();
|
|
12
|
+
|
|
13
|
+
const breadcrumbJsonLd = ref<object | null>(null);
|
|
14
|
+
|
|
15
|
+
useHead(() => {
|
|
16
|
+
if (!breadcrumbJsonLd.value) return {};
|
|
17
|
+
return {
|
|
18
|
+
script: [
|
|
19
|
+
{
|
|
20
|
+
key: "jsonld-breadcrumb",
|
|
21
|
+
type: "application/ld+json",
|
|
22
|
+
children: JSON.stringify(breadcrumbJsonLd.value),
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
};
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
const cacheKey = computed(() => `breadcrumb-${categoryId.value}`);
|
|
29
|
+
|
|
30
|
+
const { data } = await useAsyncData(cacheKey, async () => {
|
|
31
|
+
if (!categoryId.value) return [];
|
|
32
|
+
const response = await apiClient.invoke(
|
|
33
|
+
"readBreadcrumb get /breadcrumb/{id}",
|
|
34
|
+
{
|
|
35
|
+
pathParams: { id: categoryId.value },
|
|
36
|
+
query: { type: "category" },
|
|
37
|
+
},
|
|
38
|
+
);
|
|
39
|
+
return response.data;
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const items = computed<BreadcrumbItem[]>(() => {
|
|
43
|
+
if (!data.value) return [];
|
|
44
|
+
return data.value?.map((item: Schemas["Breadcrumb"]) => {
|
|
45
|
+
return {
|
|
46
|
+
label: item.name,
|
|
47
|
+
to: "/" + item.path,
|
|
48
|
+
};
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
watchEffect(() => {
|
|
53
|
+
const list = items.value ?? [];
|
|
54
|
+
if (!list.length) {
|
|
55
|
+
breadcrumbJsonLd.value = null;
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
breadcrumbJsonLd.value = {
|
|
60
|
+
"@context": "https://schema.org",
|
|
61
|
+
"@type": "BreadcrumbList",
|
|
62
|
+
itemListElement: list.map((it, index) => ({
|
|
63
|
+
"@type": "ListItem",
|
|
64
|
+
position: index + 1,
|
|
65
|
+
name: it.label,
|
|
66
|
+
item: new URL(String(it.to ?? "/"), useRequestURL().origin).toString(),
|
|
67
|
+
})),
|
|
68
|
+
};
|
|
69
|
+
});
|
|
70
|
+
</script>
|
|
71
|
+
|
|
72
|
+
<template>
|
|
73
|
+
<UBreadcrumb :items="items" />
|
|
74
|
+
</template>
|
|
@@ -1,60 +1,42 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { Schemas } from "#shopware";
|
|
3
3
|
|
|
4
|
-
defineProps<{
|
|
4
|
+
const props = defineProps<{
|
|
5
5
|
category: Schemas["Category"];
|
|
6
6
|
}>();
|
|
7
|
+
|
|
8
|
+
const { category: categoryRef } = toRefs(props);
|
|
9
|
+
|
|
10
|
+
const categoryCover = computed(
|
|
11
|
+
() => categoryRef.value.media?.url ?? "/category-placeholder.webp",
|
|
12
|
+
);
|
|
7
13
|
</script>
|
|
8
14
|
|
|
9
15
|
<template>
|
|
10
16
|
<div
|
|
11
|
-
|
|
12
|
-
class="relative mb-4 mt-8 h-40 w-full overflow-hidden rounded-lg"
|
|
17
|
+
class="relative mb-4 mt-8 min-h-36 w-full overflow-hidden rounded-[0.5rem]"
|
|
13
18
|
>
|
|
14
19
|
<NuxtImg
|
|
15
|
-
|
|
16
|
-
:src="category.media.url"
|
|
20
|
+
:src="categoryCover"
|
|
17
21
|
class="absolute inset-0 h-full w-full object-cover"
|
|
18
22
|
sizes="sm:100vw md:700px"
|
|
19
|
-
alt="
|
|
23
|
+
:alt="category.name + ' Cover Image'"
|
|
20
24
|
placeholder
|
|
21
25
|
/>
|
|
22
|
-
<div
|
|
23
|
-
v-if="category.media?.url"
|
|
24
|
-
class="absolute inset-0 bg-gradient-to-t from-black/50 to-black/10"
|
|
25
|
-
/>
|
|
26
|
-
|
|
27
|
-
<div v-else class="absolute inset-0 bg-primary" />
|
|
26
|
+
<div class="absolute inset-0 bg-linear-to-t from-black/50 to-black/10" />
|
|
28
27
|
|
|
29
28
|
<div class="relative p-4">
|
|
30
|
-
<h1
|
|
31
|
-
|
|
29
|
+
<h1
|
|
30
|
+
class="text-white text-4xl md:text-5xl lg:text-6xl font-extrabold leading-none tracking-tighter mb-3"
|
|
31
|
+
>
|
|
32
|
+
{{ categoryRef.name }}
|
|
32
33
|
</h1>
|
|
33
|
-
<p
|
|
34
|
-
|
|
34
|
+
<p
|
|
35
|
+
v-if="categoryRef.description"
|
|
36
|
+
class="text-white/90 text-[16px] text-pretty mt-1"
|
|
37
|
+
>
|
|
38
|
+
{{ categoryRef.description }}
|
|
35
39
|
</p>
|
|
36
40
|
</div>
|
|
37
41
|
</div>
|
|
38
|
-
<UPageCard
|
|
39
|
-
v-else
|
|
40
|
-
:title="category.translated.name ?? category.name"
|
|
41
|
-
:description="category.translated.description ?? category.description"
|
|
42
|
-
variant="soft"
|
|
43
|
-
class="my-4"
|
|
44
|
-
:ui="{
|
|
45
|
-
title: 'text-3xl md:text-4xl',
|
|
46
|
-
}"
|
|
47
|
-
/>
|
|
48
42
|
</template>
|
|
49
|
-
|
|
50
|
-
<style scoped>
|
|
51
|
-
@import "tailwindcss";
|
|
52
|
-
|
|
53
|
-
h1 {
|
|
54
|
-
@apply text-white text-4xl md:text-5xl lg:text-6xl font-extrabold leading-none tracking-tighter mb-3;
|
|
55
|
-
}
|
|
56
|
-
|
|
57
|
-
p {
|
|
58
|
-
@apply text-white/90 text-[16px] text-pretty mt-1;
|
|
59
|
-
}
|
|
60
|
-
</style>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
2
|
import type { operations, Schemas } from "#shopware";
|
|
3
|
+
import Breadcrumb from "~/components/Category/Breadcrumb.vue";
|
|
3
4
|
|
|
4
5
|
const props = defineProps<{
|
|
5
6
|
id: string;
|
|
@@ -59,12 +60,11 @@ const {
|
|
|
59
60
|
|
|
60
61
|
const { search: categorySearch } = useCategorySearch();
|
|
61
62
|
|
|
62
|
-
const
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
);
|
|
63
|
+
const categoryCacheKey = computed(() => `category-${categoryId.value}`);
|
|
64
|
+
|
|
65
|
+
const { data: category } = await useAsyncData(categoryCacheKey, async () => {
|
|
66
|
+
return await categorySearch(categoryId.value);
|
|
67
|
+
});
|
|
68
68
|
|
|
69
69
|
useCategorySeo(category);
|
|
70
70
|
|
|
@@ -130,6 +130,7 @@ const moreThanOneFilterAndOption = computed<boolean>(
|
|
|
130
130
|
|
|
131
131
|
<UPageBody>
|
|
132
132
|
<div>
|
|
133
|
+
<Breadcrumb :category-id="category?.id" />
|
|
133
134
|
<CategoryHeader v-if="category" :category="category" />
|
|
134
135
|
<div class="flex flex-row justify-between gap-4 mb-4">
|
|
135
136
|
<UBadge
|
package/app/components/Hero.vue
CHANGED
|
@@ -43,9 +43,10 @@ onMounted(() => {
|
|
|
43
43
|
loop
|
|
44
44
|
muted
|
|
45
45
|
playsinline
|
|
46
|
+
fetchpriority="high"
|
|
46
47
|
class="absolute inset-0 w-full h-full object-cover -z-10"
|
|
47
48
|
>
|
|
48
|
-
<source
|
|
49
|
+
<source :src="backgroundVideo" type="video/mp4" >
|
|
49
50
|
</video>
|
|
50
51
|
<div class="bg-black/50 backdrop-blur-sm">
|
|
51
52
|
<UPageHero
|
|
@@ -7,26 +7,26 @@ const { loadNavigationElements } = useNavigation();
|
|
|
7
7
|
const { data: navigationElements } = await useAsyncData(
|
|
8
8
|
`menu-navigation`,
|
|
9
9
|
async () => {
|
|
10
|
-
return await loadNavigationElements({ depth:
|
|
10
|
+
return await loadNavigationElements({ depth: 3 });
|
|
11
11
|
},
|
|
12
12
|
);
|
|
13
13
|
|
|
14
|
+
const mapCategoryToNavItem = (
|
|
15
|
+
category: Schemas["Category"],
|
|
16
|
+
): NavigationMenuItem => {
|
|
17
|
+
const label = category.translated?.name ?? "";
|
|
18
|
+
|
|
19
|
+
return {
|
|
20
|
+
label,
|
|
21
|
+
description: `${label} Kategorie`,
|
|
22
|
+
to: category.seoUrl,
|
|
23
|
+
defaultOpen: true,
|
|
24
|
+
children: (category.children ?? []).map(mapCategoryToNavItem),
|
|
25
|
+
};
|
|
26
|
+
};
|
|
27
|
+
|
|
14
28
|
const navItems = computed<NavigationMenuItem[]>(() => {
|
|
15
|
-
return navigationElements.value
|
|
16
|
-
return {
|
|
17
|
-
label: item.translated?.name,
|
|
18
|
-
description: `${item.translated?.name} Kategorie`,
|
|
19
|
-
to: item.seoUrl,
|
|
20
|
-
defaultOpen: true,
|
|
21
|
-
children: item.children?.map((child: Schemas["Category"]) => {
|
|
22
|
-
return {
|
|
23
|
-
label: child.translated?.name,
|
|
24
|
-
description: `${child.translated?.name} Kategorie`,
|
|
25
|
-
to: child.seoUrl,
|
|
26
|
-
};
|
|
27
|
-
}),
|
|
28
|
-
};
|
|
29
|
-
});
|
|
29
|
+
return (navigationElements.value ?? []).map(mapCategoryToNavItem);
|
|
30
30
|
});
|
|
31
31
|
</script>
|
|
32
32
|
|
|
@@ -2,23 +2,43 @@
|
|
|
2
2
|
import type { NavigationMenuItem } from "@nuxt/ui";
|
|
3
3
|
import type { Schemas } from "#shopware";
|
|
4
4
|
|
|
5
|
+
type Category = Schemas["Category"];
|
|
6
|
+
|
|
7
|
+
const props = withDefaults(
|
|
8
|
+
defineProps<{
|
|
9
|
+
shouldSkipFirstLevel?: boolean;
|
|
10
|
+
}>(),
|
|
11
|
+
{
|
|
12
|
+
shouldSkipFirstLevel: false,
|
|
13
|
+
},
|
|
14
|
+
);
|
|
15
|
+
|
|
5
16
|
const { loadNavigationElements, navigationElements } = useNavigation();
|
|
6
17
|
|
|
7
|
-
loadNavigationElements({ depth:
|
|
18
|
+
loadNavigationElements({ depth: 3 });
|
|
8
19
|
|
|
9
20
|
const navItems = computed<NavigationMenuItem[]>(() => {
|
|
10
|
-
|
|
21
|
+
const elements = navigationElements.value ?? [];
|
|
22
|
+
|
|
23
|
+
const mapCategoryRecursively = (category: Category): NavigationMenuItem => {
|
|
24
|
+
const hasChildren = (category.children?.length ?? 0) > 0;
|
|
25
|
+
|
|
11
26
|
return {
|
|
12
|
-
label:
|
|
13
|
-
to:
|
|
14
|
-
children:
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
to: child.seoUrl,
|
|
18
|
-
};
|
|
19
|
-
}),
|
|
27
|
+
label: category.translated?.name ?? "",
|
|
28
|
+
to: hasChildren ? undefined : category.seoUrl,
|
|
29
|
+
children: hasChildren
|
|
30
|
+
? category.children!.map(mapCategoryRecursively)
|
|
31
|
+
: undefined,
|
|
20
32
|
};
|
|
21
|
-
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
if (props.shouldSkipFirstLevel) {
|
|
36
|
+
return elements
|
|
37
|
+
.flatMap((item: Category) => item.children ?? [])
|
|
38
|
+
.map(mapCategoryRecursively);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
return elements.map(mapCategoryRecursively);
|
|
22
42
|
});
|
|
23
43
|
</script>
|
|
24
44
|
|
|
@@ -250,9 +250,7 @@ const emit = defineEmits<{
|
|
|
250
250
|
<template #label>
|
|
251
251
|
<span>
|
|
252
252
|
Ich habe die
|
|
253
|
-
<ULink to="/
|
|
254
|
-
Datenschutzbestimmungen
|
|
255
|
-
</ULink>
|
|
253
|
+
<ULink to="/datenschutz"> Datenschutzbestimmungen </ULink>
|
|
256
254
|
gelesen und akzeptiere diese.
|
|
257
255
|
</span>
|
|
258
256
|
</template>
|
|
@@ -28,53 +28,6 @@ export function useCategorySeo(category: Ref<Schemas["Category"] | undefined>) {
|
|
|
28
28
|
return base + path;
|
|
29
29
|
});
|
|
30
30
|
|
|
31
|
-
const breadcrumb = computed<string[]>(
|
|
32
|
-
() =>
|
|
33
|
-
category.value?.translated?.breadcrumb ??
|
|
34
|
-
category.value?.breadcrumb ??
|
|
35
|
-
[],
|
|
36
|
-
);
|
|
37
|
-
|
|
38
|
-
// Build BreadcrumbList items from category breadcrumb
|
|
39
|
-
type BreadcrumbListItem = {
|
|
40
|
-
"@type": "ListItem";
|
|
41
|
-
position: number;
|
|
42
|
-
name: string;
|
|
43
|
-
item?: string;
|
|
44
|
-
};
|
|
45
|
-
const breadcrumbItems = computed(() => {
|
|
46
|
-
const names = (breadcrumb.value || []) as string[];
|
|
47
|
-
const items: BreadcrumbListItem[] = [
|
|
48
|
-
{
|
|
49
|
-
"@type": "ListItem",
|
|
50
|
-
position: 1,
|
|
51
|
-
name: "Home",
|
|
52
|
-
item: config.public.storeUrl || "/",
|
|
53
|
-
},
|
|
54
|
-
];
|
|
55
|
-
|
|
56
|
-
if (names.length > 0) {
|
|
57
|
-
names.forEach((name: string, idx: number) => {
|
|
58
|
-
const isLast = idx === names.length - 1;
|
|
59
|
-
items.push({
|
|
60
|
-
"@type": "ListItem",
|
|
61
|
-
position: idx + 2,
|
|
62
|
-
name,
|
|
63
|
-
...(isLast && canonicalUrl.value ? { item: canonicalUrl.value } : {}),
|
|
64
|
-
});
|
|
65
|
-
});
|
|
66
|
-
} else if (pageTitle.value) {
|
|
67
|
-
items.push({
|
|
68
|
-
"@type": "ListItem",
|
|
69
|
-
position: 2,
|
|
70
|
-
name: pageTitle.value,
|
|
71
|
-
...(canonicalUrl.value ? { item: canonicalUrl.value } : {}),
|
|
72
|
-
});
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
return items;
|
|
76
|
-
});
|
|
77
|
-
|
|
78
31
|
const ogImage = computed(() => category.value?.media?.url);
|
|
79
32
|
|
|
80
33
|
const siteName = computed(() => config.public.site?.name || "");
|
|
@@ -145,14 +98,6 @@ export function useCategorySeo(category: Ref<Schemas["Category"] | undefined>) {
|
|
|
145
98
|
...(ogImage.value ? { image: [ogImage.value] } : {}),
|
|
146
99
|
}),
|
|
147
100
|
},
|
|
148
|
-
{
|
|
149
|
-
type: "application/ld+json",
|
|
150
|
-
innerHTML: JSON.stringify({
|
|
151
|
-
"@context": "https://schema.org",
|
|
152
|
-
"@type": "BreadcrumbList",
|
|
153
|
-
itemListElement: breadcrumbItems.value,
|
|
154
|
-
}),
|
|
155
|
-
},
|
|
156
101
|
],
|
|
157
102
|
});
|
|
158
103
|
|
package/app/layouts/listing2.vue
CHANGED
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
import type { Schemas } from "#shopware";
|
|
3
|
+
|
|
4
|
+
definePageMeta({
|
|
5
|
+
layout: "listing2",
|
|
6
|
+
});
|
|
7
|
+
const { clearBreadcrumbs } = useBreadcrumbs();
|
|
8
|
+
const { resolvePath } = useNavigationSearch();
|
|
9
|
+
const route = useRoute();
|
|
10
|
+
const routePath = route.path;
|
|
11
|
+
|
|
12
|
+
const { data: seoResult, error } = await useAsyncData(
|
|
13
|
+
`cmsResponse${routePath}`,
|
|
14
|
+
async () => {
|
|
15
|
+
// For client links if the history state contains seo url information we can omit the api call
|
|
16
|
+
if (import.meta.client) {
|
|
17
|
+
if (history.state?.routeName) {
|
|
18
|
+
return {
|
|
19
|
+
routeName: history.state?.routeName,
|
|
20
|
+
foreignKey: history.state?.foreignKey,
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
const seoUrl = await resolvePath(routePath);
|
|
25
|
+
|
|
26
|
+
if (!seoUrl?.foreignKey) {
|
|
27
|
+
throw createError({
|
|
28
|
+
statusCode: 404,
|
|
29
|
+
statusMessage: `No data fetched from API for ${routePath}`,
|
|
30
|
+
});
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return seoUrl;
|
|
34
|
+
},
|
|
35
|
+
);
|
|
36
|
+
|
|
37
|
+
if (error.value) {
|
|
38
|
+
throw error.value;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const { foreignKey } = useNavigationContext(
|
|
42
|
+
seoResult as Ref<Schemas["SeoUrl"]>,
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
onBeforeRouteLeave(() => {
|
|
46
|
+
clearBreadcrumbs();
|
|
47
|
+
});
|
|
48
|
+
</script>
|
|
49
|
+
|
|
50
|
+
<template>
|
|
51
|
+
<CategoryListing :id="foreignKey" :key="foreignKey" />
|
|
52
|
+
</template>
|
package/content/index.yml
CHANGED
|
@@ -23,7 +23,7 @@ hero:
|
|
|
23
23
|
icon: i-lucide-arrow-right
|
|
24
24
|
trailing: true
|
|
25
25
|
color: primary
|
|
26
|
-
to: /
|
|
26
|
+
to: /speisekarte/
|
|
27
27
|
size: xl
|
|
28
28
|
- label: Tisch reservieren
|
|
29
29
|
icon: i-lucide-phone
|
|
@@ -84,10 +84,10 @@ gallery:
|
|
|
84
84
|
cta:
|
|
85
85
|
title: Jetzt bestellen!
|
|
86
86
|
description: Genieße die italienische Küche, frisch zubereitet und direkt zu dir geliefert oder vor Ort genießen.
|
|
87
|
-
backgroundImage: https://shopware.shopbite.de/media/
|
|
87
|
+
backgroundImage: https://shopware.shopbite.de/media/f2/22/5f/1770899823/category-pizza-header2.webp
|
|
88
88
|
links:
|
|
89
89
|
- label: Zur Speisekarte
|
|
90
|
-
to: /
|
|
90
|
+
to: /speisekarte/
|
|
91
91
|
color: primary
|
|
92
92
|
- label: Tisch reservieren
|
|
93
93
|
to: tel:+49610471427
|
package/node.dockerfile
CHANGED
package/package.json
CHANGED
|
Binary file
|
|
@@ -50,7 +50,7 @@ async function clearCart(page: Page) {
|
|
|
50
50
|
}
|
|
51
51
|
|
|
52
52
|
async function navigateToCategoryAndVerifyProducts(page: Page) {
|
|
53
|
-
await page.goto("/
|
|
53
|
+
await page.goto("/speisekarte/pizza/", { waitUntil: "load" });
|
|
54
54
|
await expect(page.locator("h1")).toHaveText("Pizza");
|
|
55
55
|
|
|
56
56
|
const productCards = page.locator('[id^="product-card-"]');
|
|
@@ -107,26 +107,12 @@ describe("useCategorySeo", () => {
|
|
|
107
107
|
|
|
108
108
|
// JSON-LD scripts
|
|
109
109
|
const scripts = headArg.script || [];
|
|
110
|
-
expect(scripts.length).toBeGreaterThanOrEqual(
|
|
110
|
+
expect(scripts.length).toBeGreaterThanOrEqual(1);
|
|
111
111
|
|
|
112
112
|
const collection = JSON.parse(scripts[0].innerHTML);
|
|
113
113
|
expect(collection["@type"]).toBe("CollectionPage");
|
|
114
114
|
expect(collection.url).toBe("https://example.com/c/pasta");
|
|
115
115
|
expect(collection.image?.[0]).toBe("https://example.com/img/pasta.jpg");
|
|
116
|
-
|
|
117
|
-
const breadcrumb = JSON.parse(scripts[1].innerHTML);
|
|
118
|
-
expect(breadcrumb["@type"]).toBe("BreadcrumbList");
|
|
119
|
-
const items = breadcrumb.itemListElement;
|
|
120
|
-
// Home item
|
|
121
|
-
expect(items[0]).toMatchObject({
|
|
122
|
-
"@type": "ListItem",
|
|
123
|
-
position: 1,
|
|
124
|
-
name: "Home",
|
|
125
|
-
item: "https://example.com",
|
|
126
|
-
});
|
|
127
|
-
// Last item should include canonical URL
|
|
128
|
-
const last = items[items.length - 1];
|
|
129
|
-
expect(last.item).toBe("https://example.com/c/pasta");
|
|
130
116
|
});
|
|
131
117
|
|
|
132
118
|
it("sets robots to noindex when category is inactive", () => {
|
|
@@ -1,57 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { Schemas } from "#shopware";
|
|
3
|
-
|
|
4
|
-
const {
|
|
5
|
-
public: { site },
|
|
6
|
-
} = useRuntimeConfig();
|
|
7
|
-
|
|
8
|
-
const pageTitle = computed(() => `Speisekarte | ${site?.name}`);
|
|
9
|
-
|
|
10
|
-
useSeoMeta({ title: pageTitle });
|
|
11
|
-
|
|
12
|
-
definePageMeta({
|
|
13
|
-
layout: "listing",
|
|
14
|
-
});
|
|
15
|
-
|
|
16
|
-
const { loadNavigationElements, navigationElements } = useNavigation();
|
|
17
|
-
await loadNavigationElements({ depth: 1 });
|
|
18
|
-
|
|
19
|
-
const searchBarRef = ref<{
|
|
20
|
-
showSuggest: boolean;
|
|
21
|
-
loading: boolean;
|
|
22
|
-
products: Schemas["Product"];
|
|
23
|
-
} | null>(null);
|
|
24
|
-
|
|
25
|
-
const searchInProgress = ref(false);
|
|
26
|
-
</script>
|
|
27
|
-
|
|
28
|
-
<template>
|
|
29
|
-
<div v-if="navigationElements">
|
|
30
|
-
<div class="sticky top-16 left-0 z-20 w-full backdrop-blur-md rounded-md">
|
|
31
|
-
<NavigationMobileTop v-if="!searchInProgress" class="" />
|
|
32
|
-
<ProductSearchBar
|
|
33
|
-
ref="searchBarRef"
|
|
34
|
-
v-model:search-in-progress="searchInProgress"
|
|
35
|
-
/>
|
|
36
|
-
</div>
|
|
37
|
-
<div v-if="searchBarRef?.showSuggest" class="flex flex-col gap-4 mt-4">
|
|
38
|
-
<div v-if="!searchBarRef?.loading" class="flex flex-col gap-4">
|
|
39
|
-
<ProductCard
|
|
40
|
-
v-for="product in searchBarRef?.products"
|
|
41
|
-
:key="product.id"
|
|
42
|
-
:product="product"
|
|
43
|
-
:with-favorite-button="true"
|
|
44
|
-
:with-add-to-cart-button="true"
|
|
45
|
-
/>
|
|
46
|
-
</div>
|
|
47
|
-
</div>
|
|
48
|
-
<div v-else>
|
|
49
|
-
<ProductCategory
|
|
50
|
-
v-for="category in navigationElements"
|
|
51
|
-
:key="category.id"
|
|
52
|
-
:category="category"
|
|
53
|
-
/>
|
|
54
|
-
</div>
|
|
55
|
-
</div>
|
|
56
|
-
<div v-else>Loading...</div>
|
|
57
|
-
</template>
|