simple-content-site 2.2.2 → 2.3.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/composables/useSiteI18n.ts +5 -0
- package/app/composables/useSitePage.ts +59 -0
- package/app/pages/[[lang]]/[...slug].vue +10 -16
- package/content.config.ts +5 -19
- package/modules/config.ts +17 -2
- package/modules/routing.ts +3 -31
- package/package.json +1 -1
- package/app/plugins/i18n.ts +0 -16
- package/app/templates/landing.vue +0 -44
|
@@ -4,10 +4,14 @@ import en from '../../i18n/locales/en.json'
|
|
|
4
4
|
export const useSiteI18n = () => {
|
|
5
5
|
const config = useRuntimeConfig().public
|
|
6
6
|
const isEnabled = ref(!!config.i18n && config.i18n.locales?.length > 0)
|
|
7
|
+
// todo: reading the strategy like this might cause issues in the future.
|
|
8
|
+
// @ts-expect-error Due to the above comment
|
|
9
|
+
const strategy = ref(config.i18n?.strategy || 'prefix_except_default')
|
|
7
10
|
|
|
8
11
|
if (!isEnabled.value) {
|
|
9
12
|
return {
|
|
10
13
|
isEnabled,
|
|
14
|
+
strategy,
|
|
11
15
|
locale: ref('en'),
|
|
12
16
|
defaultLocale: ref('en'),
|
|
13
17
|
locales: [],
|
|
@@ -25,6 +29,7 @@ export const useSiteI18n = () => {
|
|
|
25
29
|
|
|
26
30
|
return {
|
|
27
31
|
isEnabled,
|
|
32
|
+
strategy,
|
|
28
33
|
locale,
|
|
29
34
|
defaultLocale: ref(config.i18n.defaultLocale || 'en'),
|
|
30
35
|
locales: filteredLocales,
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { ContentNavigationItem, Collections, PagesCollectionItem } from '@nuxt/content'
|
|
2
|
+
import { findPageHeadline } from '@nuxt/content/utils'
|
|
3
|
+
import { kebabCase } from 'scule'
|
|
4
|
+
|
|
5
|
+
type SitePageOptions = {
|
|
6
|
+
immediate?: boolean
|
|
7
|
+
}
|
|
8
|
+
export const useSitePage = async (opts?: SitePageOptions) => {
|
|
9
|
+
const route = useRoute()
|
|
10
|
+
const { locale, isEnabled, defaultLocale, strategy } = useSiteI18n()
|
|
11
|
+
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
|
12
|
+
|
|
13
|
+
const collectionName = computed(() => {
|
|
14
|
+
if (!isEnabled.value || !defaultLocale.value) {
|
|
15
|
+
return 'pages'
|
|
16
|
+
}
|
|
17
|
+
return `pages_${locale.value}`
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
// const page = ref<PagesCollectionItem | undefined>()
|
|
21
|
+
|
|
22
|
+
const findByPath = async (path: string) => {
|
|
23
|
+
if (strategy.value === 'prefix_except_default' && locale.value === defaultLocale.value) {
|
|
24
|
+
const prefix = `/${locale.value}`
|
|
25
|
+
if (path !== prefix && !path.startsWith(`${prefix}/`)) {
|
|
26
|
+
// we need to inject a virtual path to find the page in the collection
|
|
27
|
+
path = `${prefix}${path}`
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return await queryCollection(collectionName.value as keyof Collections).path(path).first() as PagesCollectionItem
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { data: page, refresh } = await useAsyncData(() => kebabCase(route.path), async () => {
|
|
34
|
+
const match = await findByPath(route.path)
|
|
35
|
+
return match ? match : undefined
|
|
36
|
+
}, {
|
|
37
|
+
immediate: opts?.immediate,
|
|
38
|
+
})
|
|
39
|
+
// watch(() => route.path, async (path) => {
|
|
40
|
+
// const match = await findByPath(path)
|
|
41
|
+
// page.value = match ? match : undefined
|
|
42
|
+
// })
|
|
43
|
+
|
|
44
|
+
const title = computed(() => page.value?.seo?.title || page.value?.title || undefined)
|
|
45
|
+
const description = computed(() => page.value?.seo?.description || page.value?.description || undefined)
|
|
46
|
+
const headline = computed(() => findPageHeadline(navigation?.value, page.value?.path))
|
|
47
|
+
const exists = computed(() => page.value !== undefined)
|
|
48
|
+
|
|
49
|
+
return {
|
|
50
|
+
page,
|
|
51
|
+
title,
|
|
52
|
+
description,
|
|
53
|
+
headline,
|
|
54
|
+
exists,
|
|
55
|
+
collectionName,
|
|
56
|
+
findByPath,
|
|
57
|
+
refresh,
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -1,30 +1,23 @@
|
|
|
1
1
|
<script setup lang="ts">
|
|
2
|
-
import {
|
|
3
|
-
import type { ContentNavigationItem, Collections, PagesCollectionItem } from '@nuxt/content'
|
|
2
|
+
import type { ContentNavigationItem } from '@nuxt/content'
|
|
4
3
|
import { findPageHeadline } from '@nuxt/content/utils'
|
|
5
4
|
import { addPrerenderPath } from '../../utils/prerender'
|
|
5
|
+
import { useSitePage } from '#imports'
|
|
6
6
|
|
|
7
7
|
definePageMeta({
|
|
8
8
|
layout: 'page',
|
|
9
9
|
})
|
|
10
10
|
|
|
11
11
|
const route = useRoute()
|
|
12
|
-
const {
|
|
12
|
+
const { page, collectionName } = await useSitePage()
|
|
13
13
|
const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
|
|
14
14
|
|
|
15
|
-
const collectionName = computed(() => {
|
|
16
|
-
if (!isEnabled.value || !defaultLocale.value) {
|
|
17
|
-
return 'pages'
|
|
18
|
-
}
|
|
19
|
-
return `pages_${locale.value}`
|
|
20
|
-
})
|
|
21
|
-
|
|
22
|
-
const [{ data: page }] = await Promise.all([
|
|
23
|
-
useAsyncData(kebabCase(route.path), () => queryCollection(collectionName.value as keyof Collections).path(route.path).first() as Promise<PagesCollectionItem>),
|
|
24
|
-
])
|
|
25
|
-
|
|
26
15
|
if (!page.value) {
|
|
27
|
-
throw createError({
|
|
16
|
+
throw createError({
|
|
17
|
+
statusCode: 404,
|
|
18
|
+
statusMessage: import.meta.dev ? `Page ${route.path} not found in ${collectionName.value}` : 'Pages not found',
|
|
19
|
+
fatal: true,
|
|
20
|
+
})
|
|
28
21
|
}
|
|
29
22
|
|
|
30
23
|
// Add the page path to the prerender list
|
|
@@ -45,7 +38,8 @@ watch(() => navigation?.value, () => {
|
|
|
45
38
|
headline.value = findPageHeadline(navigation?.value, page.value?.path) || headline.value
|
|
46
39
|
})
|
|
47
40
|
|
|
48
|
-
|
|
41
|
+
// todo: make the landing OG component customizable.
|
|
42
|
+
defineOgImageComponent('Landing', {
|
|
49
43
|
headline: headline.value,
|
|
50
44
|
})
|
|
51
45
|
</script>
|
package/content.config.ts
CHANGED
|
@@ -90,14 +90,6 @@ const buildI18nCollections = () => {
|
|
|
90
90
|
for (const locale of locales) {
|
|
91
91
|
const code = (typeof locale === 'string' ? locale : locale.code).replace('-', '_')
|
|
92
92
|
|
|
93
|
-
collections[`landing_${code}`] = defineCollection({
|
|
94
|
-
type: 'page',
|
|
95
|
-
source: {
|
|
96
|
-
cwd,
|
|
97
|
-
include: `${code}/index.md`,
|
|
98
|
-
},
|
|
99
|
-
})
|
|
100
|
-
|
|
101
93
|
collections[`pages_${code}`] = defineCollection({
|
|
102
94
|
type: 'page',
|
|
103
95
|
source: {
|
|
@@ -105,7 +97,6 @@ const buildI18nCollections = () => {
|
|
|
105
97
|
include: `${code}/**/*`,
|
|
106
98
|
prefix: `/${code}`,
|
|
107
99
|
exclude: [
|
|
108
|
-
`${code}/index.md`,
|
|
109
100
|
`${code}/header.yml`,
|
|
110
101
|
`${code}/footer.yml`,
|
|
111
102
|
],
|
|
@@ -137,20 +128,12 @@ const buildI18nCollections = () => {
|
|
|
137
128
|
|
|
138
129
|
const buildDefaultCollections = () => {
|
|
139
130
|
collections = {
|
|
140
|
-
landing: defineCollection({
|
|
141
|
-
type: 'page',
|
|
142
|
-
source: {
|
|
143
|
-
cwd,
|
|
144
|
-
include: 'index.md',
|
|
145
|
-
},
|
|
146
|
-
}),
|
|
147
131
|
pages: defineCollection({
|
|
148
132
|
type: 'page',
|
|
149
133
|
source: {
|
|
150
134
|
cwd,
|
|
151
135
|
include: '**',
|
|
152
136
|
exclude: [
|
|
153
|
-
'index.md',
|
|
154
137
|
'header.yml',
|
|
155
138
|
'footer.yml',
|
|
156
139
|
],
|
|
@@ -177,10 +160,13 @@ const buildDefaultCollections = () => {
|
|
|
177
160
|
}
|
|
178
161
|
|
|
179
162
|
if (locales && Array.isArray(locales)) {
|
|
180
|
-
if (locales.length
|
|
163
|
+
if (locales.length > 0) {
|
|
164
|
+
buildI18nCollections()
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
181
167
|
console.warn('Site: There are 0 locales, building with defaults instead.')
|
|
168
|
+
buildDefaultCollections()
|
|
182
169
|
}
|
|
183
|
-
buildI18nCollections()
|
|
184
170
|
}
|
|
185
171
|
else {
|
|
186
172
|
buildDefaultCollections()
|
package/modules/config.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createResolver, defineNuxtModule } from '@nuxt/kit'
|
|
1
|
+
import { createResolver, defineNuxtModule, addPlugin } from '@nuxt/kit'
|
|
2
2
|
import { defu } from 'defu'
|
|
3
3
|
import { existsSync } from 'node:fs'
|
|
4
4
|
import { join } from 'node:path'
|
|
@@ -79,16 +79,31 @@ export default defineNuxtModule({
|
|
|
79
79
|
return hasLocaleFile && hasContentFolder
|
|
80
80
|
})
|
|
81
81
|
|
|
82
|
-
//
|
|
82
|
+
//
|
|
83
83
|
nuxt.options.i18n = defu(nuxt.options.i18n, {
|
|
84
84
|
strategy: 'prefix_except_default',
|
|
85
85
|
}) as typeof nuxt.options.i18n
|
|
86
86
|
|
|
87
|
+
// todo: exposing the strategy like this might cause issues in the future.
|
|
88
|
+
// So it will be better to expose the i18n redirect plugin instead from a module.
|
|
89
|
+
nuxt.options.runtimeConfig.public.i18n = defu(nuxt.options.runtimeConfig.public.i18n, {
|
|
90
|
+
strategy: nuxt.options.i18n.strategy,
|
|
91
|
+
})
|
|
92
|
+
|
|
87
93
|
// Expose filtered locales
|
|
88
94
|
nuxt.options.runtimeConfig.public.Site = {
|
|
89
95
|
filteredLocales,
|
|
90
96
|
}
|
|
91
97
|
|
|
98
|
+
// ensure we redirect from index if the strategy requires.
|
|
99
|
+
if (nuxt.options.i18n.strategy && !['prefix_except_default', 'no_prefix'].includes(nuxt.options.i18n.strategy)) {
|
|
100
|
+
console.log(`[I18n] Adding redirect plugin for root since strategy is: ${nuxt.options.i18n.strategy}`)
|
|
101
|
+
addPlugin({
|
|
102
|
+
src: resolve('../runtime/plugins/i18n-redirect'),
|
|
103
|
+
mode: 'client',
|
|
104
|
+
})
|
|
105
|
+
}
|
|
106
|
+
|
|
92
107
|
nuxt.hook('i18n:registerModule', (register) => {
|
|
93
108
|
const langDir = resolve('../i18n/locales')
|
|
94
109
|
|
package/modules/routing.ts
CHANGED
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import { defineNuxtModule,
|
|
1
|
+
import { defineNuxtModule, createResolver } from '@nuxt/kit'
|
|
2
2
|
|
|
3
3
|
export default defineNuxtModule({
|
|
4
4
|
meta: {
|
|
5
|
+
// todo: rename this module to fit it's purpose
|
|
5
6
|
name: 'routing',
|
|
6
7
|
},
|
|
7
8
|
async setup(_options, nuxt) {
|
|
8
9
|
const { resolve } = createResolver(import.meta.url)
|
|
9
10
|
|
|
10
|
-
const isI18nEnabled = !!(nuxt.options.i18n && nuxt.options.i18n.locales && nuxt.options.i18n.locales.length > 0)
|
|
11
|
-
|
|
12
11
|
// Ensure useSiteI18n is available in the app
|
|
13
12
|
nuxt.hook('imports:extend', (imports) => {
|
|
14
13
|
const loadComposableIfNotFound = (composableName: string) => {
|
|
@@ -23,34 +22,7 @@ export default defineNuxtModule({
|
|
|
23
22
|
loadComposableIfNotFound('useSiteI18n')
|
|
24
23
|
loadComposableIfNotFound('useSiteHeader')
|
|
25
24
|
loadComposableIfNotFound('useSiteFooter')
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
// might want to know about this stuff.
|
|
29
|
-
if ((import.meta.dev || nuxt.options.dev) && !isI18nEnabled) {
|
|
30
|
-
console.warn('[Site] I18N is not enabled - using default landing page without language prefix')
|
|
31
|
-
}
|
|
32
|
-
|
|
33
|
-
extendPages((pages) => {
|
|
34
|
-
const landingTemplate = resolve('../app/templates/landing.vue')
|
|
35
|
-
|
|
36
|
-
if (pages.some(p => p.name === (isI18nEnabled ? 'lang-index' : 'index'))) {
|
|
37
|
-
if (import.meta.dev) console.warn('[Site] Duplicate landing page detected - skipping')
|
|
38
|
-
return
|
|
39
|
-
}
|
|
40
|
-
if (isI18nEnabled) {
|
|
41
|
-
pages.push({
|
|
42
|
-
name: 'lang-index',
|
|
43
|
-
path: '/:lang([a-zA-Z]{2})?',
|
|
44
|
-
file: landingTemplate,
|
|
45
|
-
})
|
|
46
|
-
}
|
|
47
|
-
else {
|
|
48
|
-
pages.push({
|
|
49
|
-
name: 'index',
|
|
50
|
-
path: '/',
|
|
51
|
-
file: landingTemplate,
|
|
52
|
-
})
|
|
53
|
-
}
|
|
25
|
+
loadComposableIfNotFound('useSitePage')
|
|
54
26
|
})
|
|
55
27
|
},
|
|
56
28
|
})
|
package/package.json
CHANGED
package/app/plugins/i18n.ts
DELETED
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
export default defineNuxtPlugin(() => {
|
|
2
|
-
const nuxtApp = useNuxtApp()
|
|
3
|
-
|
|
4
|
-
const i18nConfig = nuxtApp.$config.public.i18n
|
|
5
|
-
if (!i18nConfig || i18nConfig.locales.length === 0) {
|
|
6
|
-
return
|
|
7
|
-
}
|
|
8
|
-
|
|
9
|
-
addRouteMiddleware((to) => {
|
|
10
|
-
if (to.path === '/') {
|
|
11
|
-
const cookieLocale = useCookie('i18n_redirected').value || i18nConfig.defaultLocale || 'en'
|
|
12
|
-
|
|
13
|
-
return navigateTo(`/${cookieLocale}`)
|
|
14
|
-
}
|
|
15
|
-
})
|
|
16
|
-
})
|
|
@@ -1,44 +0,0 @@
|
|
|
1
|
-
<script setup lang="ts">
|
|
2
|
-
import type { Collections } from '@nuxt/content'
|
|
3
|
-
|
|
4
|
-
const route = useRoute()
|
|
5
|
-
const { locale, isEnabled } = useSiteI18n()
|
|
6
|
-
|
|
7
|
-
// Dynamic collection name based on i18n status
|
|
8
|
-
const collectionName = computed(() => isEnabled.value ? `landing_${locale.value}` : 'landing')
|
|
9
|
-
|
|
10
|
-
const { data: page } = await useAsyncData(collectionName.value, () => queryCollection(collectionName.value as keyof Omit<Collections, 'header' | 'footer'>).path(route.path).first())
|
|
11
|
-
if (!page.value) {
|
|
12
|
-
throw createError({ statusCode: 404, statusMessage: import.meta.dev ? `Page ${route.path} not found in ${collectionName.value}` : 'Page not found', fatal: true })
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const title = page.value.seo?.title || page.value.title
|
|
16
|
-
const description = page.value.seo?.description || page.value.description
|
|
17
|
-
|
|
18
|
-
useSeoMeta({
|
|
19
|
-
title,
|
|
20
|
-
description,
|
|
21
|
-
ogTitle: title,
|
|
22
|
-
ogDescription: description,
|
|
23
|
-
})
|
|
24
|
-
|
|
25
|
-
if (page.value?.seo?.ogImage) {
|
|
26
|
-
useSeoMeta({
|
|
27
|
-
ogImage: page.value.seo.ogImage,
|
|
28
|
-
twitterImage: page.value.seo.ogImage,
|
|
29
|
-
})
|
|
30
|
-
}
|
|
31
|
-
else {
|
|
32
|
-
defineOgImageComponent('Landing', {
|
|
33
|
-
title,
|
|
34
|
-
description,
|
|
35
|
-
})
|
|
36
|
-
}
|
|
37
|
-
</script>
|
|
38
|
-
|
|
39
|
-
<template>
|
|
40
|
-
<ContentRenderer
|
|
41
|
-
v-if="page"
|
|
42
|
-
:value="page"
|
|
43
|
-
/>
|
|
44
|
-
</template>
|