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.
@@ -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 { kebabCase } from 'scule'
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 { locale, isEnabled, defaultLocale } = useSiteI18n()
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({ statusCode: 404, statusMessage: import.meta.dev ? `Page ${route.path} not found in ${collectionName.value}` : 'Pages not found', fatal: true })
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
- defineOgImageComponent('Docs', {
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 === 0) {
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
- // Override strategy to prefix
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
 
@@ -1,14 +1,13 @@
1
- import { defineNuxtModule, extendPages, createResolver } from '@nuxt/kit'
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.2.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",
@@ -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>