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.
@@ -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
@@ -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 === 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/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.0",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
7
7
  "repository": {
@@ -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>