simple-content-site 2.2.2 → 2.3.1
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 +10 -24
- package/modules/config.ts +17 -2
- package/modules/routing.ts +3 -31
- package/nuxt.config.ts +2 -1
- package/package.json +1 -2
- 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
|
@@ -7,6 +7,9 @@ const { options } = useNuxt()
|
|
|
7
7
|
const cwd = joinURL(options.rootDir, 'content')
|
|
8
8
|
// @ts-expect-error cannot be typed?
|
|
9
9
|
const locales = options.i18n?.locales
|
|
10
|
+
// todo: might be required for diff strategies for the collections
|
|
11
|
+
// const defaultLocale = options.i18n?.defaultLocale
|
|
12
|
+
// const i18nStrategy = options.i18n?.strategy || 'prefix_except_default'
|
|
10
13
|
|
|
11
14
|
const variantEnum = z.enum(['solid', 'outline', 'subtle', 'soft', 'ghost', 'link'])
|
|
12
15
|
const colorEnum = z.enum(['primary', 'secondary', 'neutral', 'error', 'warning', 'success', 'info'])
|
|
@@ -83,29 +86,19 @@ const createFooterSchema = () => z.object({
|
|
|
83
86
|
right: z.array(createLinkSchema()),
|
|
84
87
|
})
|
|
85
88
|
|
|
86
|
-
let collections: Record<string, DefinedCollection>
|
|
89
|
+
let collections: Record<string, DefinedCollection> = {}
|
|
87
90
|
|
|
88
91
|
const buildI18nCollections = () => {
|
|
89
|
-
collections = {}
|
|
90
92
|
for (const locale of locales) {
|
|
91
93
|
const code = (typeof locale === 'string' ? locale : locale.code).replace('-', '_')
|
|
92
94
|
|
|
93
|
-
collections[`landing_${code}`] = defineCollection({
|
|
94
|
-
type: 'page',
|
|
95
|
-
source: {
|
|
96
|
-
cwd,
|
|
97
|
-
include: `${code}/index.md`,
|
|
98
|
-
},
|
|
99
|
-
})
|
|
100
|
-
|
|
101
95
|
collections[`pages_${code}`] = defineCollection({
|
|
102
96
|
type: 'page',
|
|
103
97
|
source: {
|
|
104
98
|
cwd,
|
|
105
|
-
include: `${code}
|
|
99
|
+
include: `${code}/**/*.{md,yml}`,
|
|
106
100
|
prefix: `/${code}`,
|
|
107
101
|
exclude: [
|
|
108
|
-
`${code}/index.md`,
|
|
109
102
|
`${code}/header.yml`,
|
|
110
103
|
`${code}/footer.yml`,
|
|
111
104
|
],
|
|
@@ -118,7 +111,6 @@ const buildI18nCollections = () => {
|
|
|
118
111
|
source: {
|
|
119
112
|
cwd,
|
|
120
113
|
include: `${code}/header.yml`,
|
|
121
|
-
prefix: `/${code}`,
|
|
122
114
|
},
|
|
123
115
|
schema: createHeaderSchema(),
|
|
124
116
|
})
|
|
@@ -128,7 +120,6 @@ const buildI18nCollections = () => {
|
|
|
128
120
|
source: {
|
|
129
121
|
cwd,
|
|
130
122
|
include: `${code}/footer.yml`,
|
|
131
|
-
prefix: `/${code}`,
|
|
132
123
|
},
|
|
133
124
|
schema: createFooterSchema(),
|
|
134
125
|
})
|
|
@@ -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/nuxt.config.ts
CHANGED
|
@@ -22,7 +22,8 @@ export default defineNuxtConfig({
|
|
|
22
22
|
.map((id) => {
|
|
23
23
|
return id
|
|
24
24
|
.replace(/^@nuxt\/content > /, 'simple-content-site > @nuxt/content > ')
|
|
25
|
-
.replace(/^nuxt-studio > /, 'simple-content-site > nuxt-studio > ')
|
|
25
|
+
// .replace(/^nuxt-studio > /, 'simple-content-site > nuxt-studio > ')
|
|
26
|
+
// .replace(/^@nuxtjs\/i18n > /, 'simple-content-site > @nuxtjs/i18n > ')
|
|
26
27
|
},
|
|
27
28
|
)
|
|
28
29
|
})
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "simple-content-site",
|
|
3
3
|
"description": "Nuxt layer for simple website with basic functions.",
|
|
4
|
-
"version": "2.
|
|
4
|
+
"version": "2.3.1",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./nuxt.config.ts",
|
|
7
7
|
"repository": {
|
|
@@ -39,7 +39,6 @@
|
|
|
39
39
|
"minimark": "^0.2.0",
|
|
40
40
|
"motion-v": "^1.7.4",
|
|
41
41
|
"nuxt-og-image": "^5.1.13",
|
|
42
|
-
"nuxt-studio": "^1.1.0",
|
|
43
42
|
"pkg-types": "^2.3.0",
|
|
44
43
|
"scule": "^1.3.0",
|
|
45
44
|
"tailwindcss": "^4.1.17",
|
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>
|