simple-content-site 2.3.1 → 3.1.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.
@@ -42,7 +42,7 @@ function getEmojiFlag(locale: string): string {
42
42
 
43
43
  <template>
44
44
  <UPopover
45
- mode="hover"
45
+ mode="click"
46
46
  :content="{ align: 'end' }"
47
47
  >
48
48
  <UButton
@@ -5,7 +5,6 @@ export const useSiteI18n = () => {
5
5
  const config = useRuntimeConfig().public
6
6
  const isEnabled = ref(!!config.i18n && config.i18n.locales?.length > 0)
7
7
  // todo: reading the strategy like this might cause issues in the future.
8
- // @ts-expect-error Due to the above comment
9
8
  const strategy = ref(config.i18n?.strategy || 'prefix_except_default')
10
9
 
11
10
  if (!isEnabled.value) {
@@ -25,7 +24,7 @@ export const useSiteI18n = () => {
25
24
  }
26
25
 
27
26
  const { locale, t } = useI18n()
28
- const filteredLocales = (config.Site as { filteredLocales: LocaleObject<string>[] })?.filteredLocales || []
27
+ const filteredLocales = (config.scs as { filteredLocales: LocaleObject<string>[] })?.filteredLocales || []
29
28
 
30
29
  return {
31
30
  isEnabled,
@@ -1,59 +1,36 @@
1
- import type { ContentNavigationItem, Collections, PagesCollectionItem } from '@nuxt/content'
2
- import { findPageHeadline } from '@nuxt/content/utils'
1
+ import type { Collections, PagesCollectionItem } from '@nuxt/content'
3
2
  import { kebabCase } from 'scule'
3
+ import { joinURL, withLeadingSlash } from 'ufo'
4
4
 
5
- type SitePageOptions = {
6
- immediate?: boolean
7
- }
8
- export const useSitePage = async (opts?: SitePageOptions) => {
9
- const route = useRoute()
5
+ export const useSitePage = () => {
10
6
  const { locale, isEnabled, defaultLocale, strategy } = useSiteI18n()
11
- const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
12
7
 
13
- const collectionName = computed(() => {
14
- if (!isEnabled.value || !defaultLocale.value) {
15
- return 'pages'
8
+ const collectionName = computed<keyof Collections>(() => {
9
+ if (!isEnabled.value || !defaultLocale.value || !locale.value) {
10
+ return 'pages' as keyof Collections
16
11
  }
17
- return `pages_${locale.value}`
12
+ return `pages_${locale.value}` as keyof Collections
18
13
  })
19
14
 
20
- // const page = ref<PagesCollectionItem | undefined>()
15
+ const getKeyForPath = (path: string) => {
16
+ const prefix = toValue(collectionName.value).replaceAll('_', '-')
17
+ const suffix = kebabCase(withLeadingSlash(path).replaceAll('/', '--'))
18
+ return `${prefix}:${suffix}`
19
+ }
21
20
 
22
21
  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}`
22
+ if (isEnabled.value && strategy.value === 'prefix_except_default' && locale.value === defaultLocale.value) {
23
+ const localePrefix = withLeadingSlash(locale.value)
24
+ if (path !== localePrefix && !path.startsWith(`${localePrefix}/`)) {
25
+ path = joinURL(localePrefix, path)
28
26
  }
29
27
  }
30
- return await queryCollection(collectionName.value as keyof Collections).path(path).first() as PagesCollectionItem
28
+ return await queryCollection(collectionName.value).path(withLeadingSlash(path)).first() as PagesCollectionItem
31
29
  }
32
30
 
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
31
  return {
50
- page,
51
- title,
52
- description,
53
- headline,
54
- exists,
55
32
  collectionName,
56
33
  findByPath,
57
- refresh,
34
+ getKeyForPath,
58
35
  }
59
36
  }
@@ -1,27 +1,36 @@
1
1
  <script setup lang="ts">
2
2
  import type { ContentNavigationItem } from '@nuxt/content'
3
3
  import { findPageHeadline } from '@nuxt/content/utils'
4
- import { addPrerenderPath } from '../../utils/prerender'
4
+ // import { addPrerenderPath } from '../../utils/prerender'
5
5
  import { useSitePage } from '#imports'
6
+ import { withLeadingSlash } from 'ufo'
6
7
 
7
8
  definePageMeta({
8
9
  layout: 'page',
9
10
  })
10
11
 
11
12
  const route = useRoute()
12
- const { page, collectionName } = await useSitePage()
13
- const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
13
+ const { findByPath, getKeyForPath } = useSitePage()
14
+
15
+ const { data: page } = await useAsyncData(() => getKeyForPath(route.path), async () => {
16
+ console.log('fetching page', route.path)
17
+ return await findByPath(withLeadingSlash(route.path) || '/')
18
+ }, {
19
+ immediate: true,
20
+ })
14
21
 
15
22
  if (!page.value) {
16
23
  throw createError({
17
- statusCode: 404,
18
- statusMessage: import.meta.dev ? `Page ${route.path} not found in ${collectionName.value}` : 'Pages not found',
24
+ status: 404,
25
+ message: 'Page not found',
19
26
  fatal: true,
20
27
  })
21
28
  }
22
29
 
30
+ const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
31
+
23
32
  // Add the page path to the prerender list
24
- addPrerenderPath(`/raw${route.path}.md`)
33
+ // addPrerenderPath(`/raw${route.path}.md`)
25
34
 
26
35
  const title = page.value.seo?.title || page.value.title
27
36
  const description = page.value.seo?.description || page.value.description
package/content.config.ts CHANGED
@@ -1,10 +1,15 @@
1
+ import { z } from 'zod/v4'
1
2
  import type { DefinedCollection } from '@nuxt/content'
2
- import { defineContentConfig, defineCollection, z } from '@nuxt/content'
3
+ import { defineContentConfig, defineCollection, property } from '@nuxt/content'
3
4
  import { useNuxt } from '@nuxt/kit'
4
5
  import { joinURL } from 'ufo'
5
6
 
6
7
  const { options } = useNuxt()
7
8
  const cwd = joinURL(options.rootDir, 'content')
9
+
10
+ // @ts-expect-error cannot be typed?
11
+ const contentExcluded = options?.scs?.excludeContent || []
12
+
8
13
  // @ts-expect-error cannot be typed?
9
14
  const locales = options.i18n?.locales
10
15
  // todo: might be required for diff strategies for the collections
@@ -29,10 +34,10 @@ const createLinkSchema = () => z.object({
29
34
  class: z.string().optional(),
30
35
  label: z.string().nonempty(),
31
36
  to: z.string().nonempty(),
32
- icon: z.string().optional().editor({ input: 'icon' }),
37
+ icon: property(z.string().optional()).editor({ input: 'icon' }),
33
38
  size: sizeEnum.optional(),
34
39
  trailing: z.boolean().optional(),
35
- trailingIcon: z.string().optional().editor({ input: 'icon' }),
40
+ trailingIcon: property(z.string().optional()).editor({ input: 'icon' }),
36
41
  target: z.string().optional(),
37
42
  color: colorEnum.optional(),
38
43
  variant: variantEnum.optional(),
@@ -42,10 +47,10 @@ const createNavigationSchema = () => z.object({
42
47
  label: z.string().nonempty(),
43
48
  class: z.string().optional(),
44
49
  to: z.string().nonempty(),
45
- icon: z.string().optional().editor({ input: 'icon' }),
50
+ icon: property(z.string().optional()).editor({ input: 'icon' }),
46
51
  size: sizeEnum.optional(),
47
52
  trailing: z.boolean().optional(),
48
- trailingIcon: z.string().optional().editor({ input: 'icon' }),
53
+ trailingIcon: property(z.string().optional()).editor({ input: 'icon' }),
49
54
  target: z.string().optional(),
50
55
  color: colorEnum.optional(),
51
56
  variant: variantEnum.optional(),
@@ -96,11 +101,12 @@ const buildI18nCollections = () => {
96
101
  type: 'page',
97
102
  source: {
98
103
  cwd,
99
- include: `${code}/**/*.{md,yml}`,
104
+ include: `${code}/**/**.{md,yml}`,
100
105
  prefix: `/${code}`,
101
106
  exclude: [
102
107
  `${code}/header.yml`,
103
108
  `${code}/footer.yml`,
109
+ ...contentExcluded,
104
110
  ],
105
111
  },
106
112
  schema: createPageSchema(),
@@ -132,10 +138,11 @@ const buildDefaultCollections = () => {
132
138
  type: 'page',
133
139
  source: {
134
140
  cwd,
135
- include: '**',
141
+ include: '**/**.{md,yml}',
136
142
  exclude: [
137
143
  'header.yml',
138
144
  'footer.yml',
145
+ ...contentExcluded,
139
146
  ],
140
147
  },
141
148
  schema: createPageSchema(),
package/modules/config.ts CHANGED
@@ -1,30 +1,30 @@
1
- import { createResolver, defineNuxtModule, addPlugin } from '@nuxt/kit'
1
+ import { createResolver, defineNuxtModule, addPlugin, logger } from '@nuxt/kit'
2
2
  import { defu } from 'defu'
3
3
  import { existsSync } from 'node:fs'
4
4
  import { join } from 'node:path'
5
5
  import { inferSiteURL, getPackageJsonMetadata } from '../utils/meta'
6
6
  import { getGitBranch, getGitEnv, getLocalGitInfo } from '../utils/git'
7
7
 
8
- export default defineNuxtModule({
8
+ interface SimpleContentSiteOptions {
9
+ excludeContent?: string[]
10
+ }
11
+
12
+ const log = logger.withTag('SimpleContentSite')
13
+
14
+ export default defineNuxtModule<SimpleContentSiteOptions>({
9
15
  meta: {
10
- name: 'config',
16
+ name: 'scs',
17
+ },
18
+ defaults: {
19
+ excludeContent: [],
11
20
  },
12
21
  async setup(_options, nuxt) {
22
+ const { resolve } = createResolver(import.meta.url)
13
23
  const dir = nuxt.options.rootDir
14
24
  const url = inferSiteURL()
15
25
  const meta = await getPackageJsonMetadata(dir)
16
26
  const gitInfo = await getLocalGitInfo(dir) || getGitEnv()
17
- const siteName = nuxt.options?.site?.name || meta.name || gitInfo?.name || ''
18
-
19
- // nuxt.options.llms = defu(nuxt.options.llms, {
20
- // domain: url,
21
- // title: siteName,
22
- // description: meta.description || '',
23
- // full: {
24
- // title: siteName,
25
- // description: meta.description || '',
26
- // },
27
- // })
27
+ const siteName = (typeof nuxt.options.site === 'object' && nuxt.options.site?.name) || meta.name || gitInfo?.name || ''
28
28
 
29
29
  nuxt.options.site = defu(nuxt.options.site, {
30
30
  url,
@@ -49,12 +49,31 @@ export default defineNuxtModule({
49
49
  branch: getGitBranch(),
50
50
  })
51
51
 
52
- /*
53
- ** I18N
54
- */
52
+ // ...
53
+ nuxt.options.i18n = defu(nuxt.options.i18n, {
54
+ strategy: 'prefix_except_default',
55
+ }) as typeof nuxt.options.i18n
56
+
57
+ // ensure we redirect from index if the strategy requires.
58
+
59
+ // todo: exposing the strategy like this might cause issues in the future.
60
+ // So it will be better to expose the i18n redirect plugin instead from a module.
61
+ nuxt.options.runtimeConfig.public.i18n = defu(nuxt.options.runtimeConfig.public.i18n, {
62
+ strategy: (nuxt.options.i18n ? nuxt.options.i18n.strategy : undefined) || 'prefix_except_default',
63
+ })
64
+
65
+ const i18nStrategy = nuxt.options.runtimeConfig.public.i18n.strategy as string
66
+
67
+ if (i18nStrategy && !['prefix_except_default', 'no_prefix'].includes(i18nStrategy)) {
68
+ console.log(`[I18n] Adding redirect plugin for root since strategy is: ${i18nStrategy}`)
69
+ addPlugin({
70
+ src: resolve('../runtime/plugins/i18n-redirect'),
71
+ mode: 'client',
72
+ })
73
+ }
74
+
55
75
  if (nuxt.options.i18n && nuxt.options.i18n.locales) {
56
76
  const { resolve } = createResolver(import.meta.url)
57
- const { resolve: resolveRoot } = createResolver(dir)
58
77
 
59
78
  // Filter locales to only include existing ones
60
79
  const filteredLocales = nuxt.options.i18n.locales.filter((locale) => {
@@ -69,63 +88,40 @@ export default defineNuxtModule({
69
88
  const hasContentFolder = existsSync(contentPath)
70
89
 
71
90
  if (!hasLocaleFile) {
72
- console.warn(`[Site] Locale file not found: ${localeCode}.json - skipping locale "${localeCode}"`)
91
+ log.warn(`Locale file not found: ${localeCode}.json - skipping locale "${localeCode}"`)
73
92
  }
74
93
 
75
94
  if (!hasContentFolder) {
76
- console.warn(`[Site] Content folder not found: content/${localeCode}/ - skipping locale "${localeCode}"`)
95
+ log.warn(`Content folder not found: content/${localeCode}/ - skipping locale "${localeCode}"`)
77
96
  }
78
97
 
79
98
  return hasLocaleFile && hasContentFolder
80
99
  })
81
100
 
82
- //
83
- nuxt.options.i18n = defu(nuxt.options.i18n, {
84
- strategy: 'prefix_except_default',
85
- }) as typeof nuxt.options.i18n
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
-
93
101
  // Expose filtered locales
94
- nuxt.options.runtimeConfig.public.Site = {
102
+ nuxt.options.runtimeConfig.public.scs = {
95
103
  filteredLocales,
96
104
  }
97
105
 
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
-
107
- nuxt.hook('i18n:registerModule', (register) => {
106
+ // @ts-expect-error This is messed up...
107
+ nuxt.hook('i18n:registerModule', (register: never) => {
108
108
  const langDir = resolve('../i18n/locales')
109
109
 
110
110
  const locales = filteredLocales?.map((locale) => {
111
- // Possibly load custom translations.
112
- const localeCode = typeof locale === 'string' ? locale : locale.code
113
- const customLocalePath = resolveRoot('i18n/locales', `${localeCode}.json`)
114
- const hasCustomLocale = existsSync(customLocalePath)
115
- const files = hasCustomLocale ? [customLocalePath, `${localeCode}.json`] : [`${localeCode}.json`]
116
111
  return typeof locale === 'string'
117
112
  ? {
118
113
  code: locale,
119
114
  name: locale,
120
- files,
115
+ file: `${locale}.json`,
121
116
  }
122
117
  : {
123
118
  code: locale.code,
124
119
  name: locale.name || locale.code,
125
- files,
120
+ file: `${locale.code}.json`,
126
121
  }
127
122
  })
128
123
 
124
+ // @ts-expect-error This is amessed up too
129
125
  register({
130
126
  langDir,
131
127
  locales,
@@ -0,0 +1,13 @@
1
+ import { defineNuxtModule } from '@nuxt/kit'
2
+
3
+ // const { resolve } = createResolver(import.meta.url)
4
+
5
+ export default defineNuxtModule({
6
+ meta: {
7
+ // todo: rename this module to fit it's purpose
8
+ name: 'routing',
9
+ },
10
+ async setup(_options) {
11
+ console.log('content module setup -> Should bootstrap content?! or can i make it work from a separate content.config.ts?')
12
+ },
13
+ })
package/modules/css.ts CHANGED
@@ -14,19 +14,24 @@ export default defineNuxtModule({
14
14
  const uiPath = resolveModulePath('@nuxt/ui', { from: import.meta.url, conditions: ['style'] })
15
15
  const tailwindPath = resolveModulePath('tailwindcss', { from: import.meta.url, conditions: ['style'] })
16
16
  const layerDir = resolver.resolve('../app')
17
+ // const assistantDir = resolver.resolve('../modules/assistant')
17
18
 
18
19
  const cssTemplate = addTemplate({
19
- filename: 'website.css',
20
+ filename: 'simple-content-site.css',
20
21
  getContents: () => {
21
22
  return `@import ${JSON.stringify(tailwindPath)};
22
23
  @import ${JSON.stringify(uiPath)};
23
24
 
24
25
  @source "${contentDir.replace(/\\/g, '/')}/**/*";
25
26
  @source "${layerDir.replace(/\\/g, '/')}/**/*";
26
- @source "../../app.config.ts";`
27
+ @source "../../app.config.ts";
28
+ `
29
+ // @source "${assistantDir.replace(/\\/g, '/')}/**/*";
27
30
  },
28
31
  })
29
32
 
30
- nuxt.options.css.unshift(cssTemplate.dst)
33
+ if (Array.isArray(nuxt.options.css)) {
34
+ nuxt.options.css.unshift(cssTemplate.dst)
35
+ }
31
36
  },
32
37
  })
@@ -1,13 +1,12 @@
1
1
  import { defineNuxtModule, createResolver } from '@nuxt/kit'
2
2
 
3
+ const { resolve } = createResolver(import.meta.url)
3
4
  export default defineNuxtModule({
4
5
  meta: {
5
6
  // todo: rename this module to fit it's purpose
6
7
  name: 'routing',
7
8
  },
8
9
  async setup(_options, nuxt) {
9
- const { resolve } = createResolver(import.meta.url)
10
-
11
10
  // Ensure useSiteI18n is available in the app
12
11
  nuxt.hook('imports:extend', (imports) => {
13
12
  const loadComposableIfNotFound = (composableName: string) => {
package/nuxt.config.ts CHANGED
@@ -48,14 +48,10 @@ export default defineNuxtConfig({
48
48
  },
49
49
  },
50
50
  },
51
+ // future: {
52
+ // compatibilityVersion: 5,
53
+ // },
51
54
  compatibilityDate: '2025-07-22',
52
- nitro: {
53
- prerender: {
54
- crawlLinks: true,
55
- failOnError: false,
56
- autoSubfolderIndex: false,
57
- },
58
- },
59
55
  hooks: {
60
56
  'nitro:config'(nitroConfig) {
61
57
  const nuxt = useNuxt()
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.3.1",
4
+ "version": "3.1.0",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
7
7
  "repository": {
@@ -22,27 +22,30 @@
22
22
  "README.md"
23
23
  ],
24
24
  "dependencies": {
25
- "@iconify-json/lucide": "^1.2.73",
26
- "@iconify-json/simple-icons": "^1.2.59",
27
- "@iconify-json/vscode-icons": "^1.2.35",
28
- "@nuxt/content": "^3.10.0",
29
- "@nuxt/image": "^1.11.0",
25
+ "@iconify-json/lucide": "^1.2.86",
26
+ "@iconify-json/simple-icons": "^1.2.67",
27
+ "@iconify-json/vscode-icons": "^1.2.40",
28
+ "@nuxt/content": "^3.11.0",
29
+ "@nuxt/image": "^2.0.0",
30
30
  "@nuxt/kit": "^4.2.2",
31
- "@nuxt/ui": "^4.3.0",
31
+ "@nuxt/ui": "^4.4.0",
32
32
  "@nuxtjs/i18n": "^10.2.1",
33
- "@nuxtjs/mdc": "^0.19.2",
33
+ "@nuxtjs/mdc": "^0.20.0",
34
34
  "@nuxtjs/robots": "^5.6.7",
35
- "@vueuse/core": "^13.9.0",
35
+ "@vueuse/core": "^14.1.0",
36
+ "@nuxt/fonts": "^0.14.0",
36
37
  "defu": "^6.1.4",
37
38
  "exsolve": "^1.0.8",
38
39
  "git-url-parse": "^16.1.0",
39
40
  "minimark": "^0.2.0",
40
- "motion-v": "^1.7.4",
41
+ "motion-v": "^1.9.0",
41
42
  "nuxt-og-image": "^5.1.13",
42
43
  "pkg-types": "^2.3.0",
43
44
  "scule": "^1.3.0",
44
- "tailwindcss": "^4.1.17",
45
- "ufo": "^1.6.1"
45
+ "tailwindcss": "^4.1.18",
46
+ "ufo": "^1.6.3",
47
+ "zod": "^4.3.5",
48
+ "zod-to-json-schema": "^3.25.1"
46
49
  },
47
50
  "peerDependencies": {
48
51
  "better-sqlite3": "12.x",
@@ -6,7 +6,7 @@ import type { Collections } from '@nuxt/content'
6
6
  export default eventHandler(async (event) => {
7
7
  const slug = getRouterParams(event)['slug.md']
8
8
  if (!slug?.endsWith('.md')) {
9
- throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
9
+ throw createError({ status: 404, message: 'Page not found', fatal: true })
10
10
  }
11
11
 
12
12
  const path = withLeadingSlash(slug.replace('.md', ''))
@@ -31,7 +31,7 @@ export default eventHandler(async (event) => {
31
31
 
32
32
  const page = await queryCollection(event, collectionName as keyof Omit<Collections, 'header' | 'footer'>).path(path).first()
33
33
  if (!page) {
34
- throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
34
+ throw createError({ status: 404, message: 'Page not found', fatal: true })
35
35
  }
36
36
 
37
37
  // Add title and description to the top of the page if missing