Skip to content

Commit 2c3846f

Browse files
committed
chore(template): product variant + seo optimization
Signed-off-by: Frederik Bußmann <frederik@bussmann.io>
1 parent 860dbd9 commit 2c3846f

25 files changed

Lines changed: 131 additions & 23 deletions

template/app/app.config.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
export default defineAppConfig({
22
shopify: {
3+
shopName: 'Nuxt Shopify Demo Store',
4+
35
collection: {
46
perPage: 12,
57
},

template/app/app.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
<script setup lang="ts">
22
import * as locales from '@nuxt/ui/locale'
33
4+
const { shopify: { shopName } } = useAppConfig()
45
const { language } = useLocalization()
56
const { id, init, get } = useCart()
67
@@ -12,7 +13,20 @@ useHead({
1213
lang,
1314
dir,
1415
},
15-
title: 'Nuxt Shopify Demo Store',
16+
17+
title: shopName,
18+
19+
meta: [
20+
{ property: 'og:image', content: 'https://shopify.nuxtjs.org/logo-readme.jpg' },
21+
{ property: 'og:image:type', content: 'image/jpeg' },
22+
{ name: 'twitter:card', content: 'summary_large_image' },
23+
{ name: 'twitter:image', content: 'https://shopify.nuxtjs.org/logo-readme.jpg' },
24+
{ name: 'twitter:image:src', content: 'https://shopify.nuxtjs.org/logo-readme.jpg' },
25+
{ property: 'og:image:width', content: '1200' },
26+
{ name: 'twitter:image:width', content: '1200' },
27+
{ property: 'og:image:height', content: '600' },
28+
{ name: 'twitter:image:height', content: '600' },
29+
],
1630
})
1731
1832
watch(id, value => !value ? init().then(get) : get(), { immediate: true })

template/app/components/Header.vue

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ const { data: items } = await useStorefrontData('main-menu', `#graphql
2525
: item.url ?? undefined,
2626
})) ?? [],
2727
cache: 'long',
28+
watch: [language, country],
2829
})
2930
</script>
3031

template/app/components/cart/Choose.vue

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,20 @@ const open = ref(false)
4646

4747
<template #body>
4848
<div
49-
v-if="product && selectedVariant"
49+
v-if="props.product && selectedVariant"
5050
class="lg:grid lg:grid-cols-12"
5151
>
5252
<ProductGallery
5353
:selected-variant="selectedVariant"
54-
:product="product"
54+
:product="props.product"
5555
class="lg:col-span-6"
5656
/>
5757

5858
<ProductConfigurator
5959
v-model="selectedVariant"
60+
:product="props.product"
6061
class="lg:col-start-8 lg:col-span-5"
62+
@submit="open = false"
6163
/>
6264
</div>
6365
</template>

template/app/components/collection/Products.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ const props = defineProps<{
33
handle: string
44
}>()
55
6+
const { shopify: { shopName } } = useAppConfig()
67
const { params } = useCollection()
78
const { locale } = useI18n()
89
const router = useRouter()
@@ -57,7 +58,7 @@ const { data: collection, status } = await useStorefrontData(key, `#graphql
5758
})
5859
5960
useSeoMeta({
60-
title: `${collection.value?.title} | Nuxt Shopify Demo Store`,
61+
title: `${collection.value?.title} | ${shopName}`,
6162
description: collection.value?.description,
6263
})
6364

template/app/components/product/Configurator.vue

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,17 @@
11
<script setup lang="ts">
2-
import type { ProductVariantFieldsFragment } from '#shopify/storefront'
2+
import type { ProductFieldsFragment, ProductVariantFieldsFragment } from '#shopify/storefront'
33
import type { FormSubmitEvent } from '#ui/types'
44
5+
const props = defineProps<{
6+
product?: ProductFieldsFragment
7+
}>()
8+
59
const selectedVariant = defineModel<ProductVariantFieldsFragment>()
610
11+
const emit = defineEmits<{
12+
submit: [FormSubmitEvent<typeof state>]
13+
}>()
14+
715
const { language, country } = useLocalization()
816
const { locale } = useI18n()
917
const { add } = useCart()
@@ -15,7 +23,7 @@ const state = reactive({
1523
selectedOptions: selectedVariant.value?.selectedOptions,
1624
})
1725
18-
const { data: product } = await useStorefrontData(`product-options-${locale.value}-${handle.value}`, `#graphql
26+
const { data } = await useStorefrontData(`product-options-${locale.value}-${handle.value}`, `#graphql
1927
query FetchProductOptions($handle: String, $language: LanguageCode, $country: CountryCode, $selectedOptions: [SelectedOptionInput!])
2028
@inContext(language: $language, country: $country) {
2129
product(handle: $handle) {
@@ -47,14 +55,20 @@ const { data: product } = await useStorefrontData(`product-options-${locale.valu
4755
4856
const loading = ref(false)
4957
50-
watch(() => product.value?.selectedOrFirstAvailableVariant, variant => selectedVariant.value = variant ?? undefined)
58+
const product = computed(() => data.value ?? props.product)
59+
60+
watch(() => data.value?.selectedOrFirstAvailableVariant, variant => selectedVariant.value = variant ?? undefined)
5161
5262
const onSubmit = async (event: FormSubmitEvent<typeof state>) => {
5363
if (!selectedVariant.value) return
5464
5565
loading.value = true
5666
57-
await add(selectedVariant.value.id, event.data.quantity).then(() => loading.value = false)
67+
await add(selectedVariant.value.id, event.data.quantity).finally(() => {
68+
loading.value = false
69+
70+
emit('submit', event)
71+
})
5872
}
5973
</script>
6074

template/app/components/product/Gallery.vue

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,18 @@ const carousel = useTemplateRef('carousel')
1111
1212
const images = computed(() => flattenConnection(props.product.images))
1313
14-
const sliderImages = computed(() => (props.selectedVariant?.image ? [props.selectedVariant.image] : [])
15-
.concat(flattenConnection(props.product?.images)))
14+
const sliderImages = computed(() => {
15+
if (props.selectedVariant?.image) {
16+
const variantImage = props.selectedVariant.image
17+
18+
return [
19+
variantImage,
20+
...images.value.filter(image => image.url !== variantImage.url),
21+
]
22+
}
23+
24+
return images.value
25+
})
1626
1727
watch(() => props.selectedVariant, () => carousel.value?.emblaApi?.scrollTo(0))
1828
</script>

template/app/error.vue

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ const props = defineProps<{
77
error: NuxtError
88
}>()
99
10+
const { shopify: { shopName } } = useAppConfig()
1011
const { language } = useLocalization()
1112
const { id, init, get } = useCart()
1213
const localePath = useLocalePath()
@@ -19,7 +20,7 @@ useHead({
1920
lang,
2021
dir,
2122
},
22-
title: 'Nuxt Shopify Demo Store',
23+
title: shopName,
2324
})
2425
2526
watch(id, value => !value ? init().then(get) : get(), { immediate: true })

template/app/pages/blog/[handle]/[article].vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@ definePageMeta({
55
&& typeof route.params.article === 'string',
66
})
77
8+
const { shopify: { shopName } } = useAppConfig()
89
const localePath = useLocalePath()
910
const { locale } = useI18n()
1011
const route = useRoute()
1112
1213
const handle = computed(() => route.params.handle as string)
1314
const article = computed(() => route.params.article as string)
1415
15-
const { data: blog } = await useStorefrontData(`article-${locale.value}-${handle.value}`, `#graphql
16+
const { data: blog, error } = await useStorefrontData(`article-${locale.value}-${handle.value}`, `#graphql
1617
query FetchBlogArticle($handle: String!, $article: String!) {
1718
blog(handle: $handle) {
1819
title
@@ -32,6 +33,19 @@ const { data: blog } = await useStorefrontData(`article-${locale.value}-${handle
3233
})
3334
3435
const articleData = computed(() => blog.value?.articleByHandle)
36+
37+
if (!articleData.value || error.value) {
38+
throw createError({
39+
status: 404,
40+
statusText: `${$t('error.notFound')}: ${route.fullPath}`,
41+
message: error.value?.message || $t('error.article'),
42+
})
43+
}
44+
45+
useSeoMeta({
46+
title: `${articleData.value?.seo?.title ?? articleData.value?.title} | ${shopName}`,
47+
description: articleData.value?.seo?.description ?? $t('seo.description'),
48+
})
3549
</script>
3650

3751
<template>

template/app/pages/blog/[handle]/index.vue

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,13 +3,14 @@ definePageMeta({
33
validate: route => typeof route.params.handle === 'string',
44
})
55
6+
const { shopify: { shopName } } = useAppConfig()
67
const localePath = useLocalePath()
78
const { locale } = useI18n()
89
const route = useRoute()
910
1011
const handle = computed(() => route.params.handle as string)
1112
12-
const { data: blog } = await useStorefrontData(`blog-${locale.value}-${handle.value}`, `#graphql
13+
const { data: blog, error } = await useStorefrontData(`blog-${locale.value}-${handle.value}`, `#graphql
1314
query FetchBlog($handle: String) {
1415
blog(handle: $handle) {
1516
...BlogFields
@@ -23,6 +24,19 @@ const { data: blog } = await useStorefrontData(`blog-${locale.value}-${handle.va
2324
transform: data => data?.blog,
2425
cache: 'long',
2526
})
27+
28+
if (!blog.value || error.value) {
29+
throw createError({
30+
status: 404,
31+
statusText: `${$t('error.notFound')}: ${route.fullPath}`,
32+
message: error.value?.message || $t('error.blog'),
33+
})
34+
}
35+
36+
useSeoMeta({
37+
title: `${blog.value?.seo?.title ?? blog.value?.title} | ${shopName}`,
38+
description: blog.value?.seo?.description ?? $t('seo.description'),
39+
})
2640
</script>
2741

2842
<template>

0 commit comments

Comments
 (0)