simple-content-site 1.0.3 → 1.0.5

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/app.vue CHANGED
@@ -4,7 +4,7 @@ import * as nuxtUiLocales from '@nuxt/ui/locale'
4
4
 
5
5
  const { seo } = useAppConfig()
6
6
  const site = useSiteConfig()
7
- const { locale, locales, isEnabled, switchLocalePath } = useDocusI18n()
7
+ const { locale, locales, isEnabled, switchLocalePath } = useSiteI18n()
8
8
 
9
9
  const lang = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales]?.code || 'en')
10
10
  const dir = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales]?.dir || 'ltr')
@@ -1,5 +1,5 @@
1
1
  <script setup lang="ts">
2
- const { locale, locales, switchLocalePath } = useDocusI18n()
2
+ const { locale, locales, switchLocalePath } = useSiteI18n()
3
3
 
4
4
  function getEmojiFlag(locale: string): string {
5
5
  const languageToCountry: Record<string, string> = {
@@ -1,11 +1,26 @@
1
+ <script setup lang="ts">
2
+ import { useSiteFooter } from '../../composables/useSiteFooter'
3
+
4
+ const { data: footer } = await useSiteFooter()
5
+ </script>
6
+
1
7
  <template>
2
8
  <UFooter>
3
9
  <template #left>
4
10
  <AppFooterLeft />
5
11
  </template>
6
-
12
+ <template
13
+ v-if="footer && footer.sections && footer.sections.length"
14
+ #top
15
+ >
16
+ <UContainer>
17
+ <UFooterColumns
18
+ :columns="footer.sections"
19
+ />
20
+ </UContainer>
21
+ </template>
7
22
  <template #right>
8
- <AppFooterRight />
23
+ <AppFooterRight :links="footer && footer.socials" />
9
24
  </template>
10
25
  </UFooter>
11
26
  </template>
@@ -1,7 +1,13 @@
1
1
  <script setup lang="ts">
2
+ import type { ButtonProps } from '@nuxt/ui'
3
+
4
+ const props = defineProps<{
5
+ links?: ButtonProps[]
6
+ }>()
7
+
2
8
  const appConfig = useAppConfig()
3
9
 
4
- const links = computed(() => [
10
+ const footerLinks = computed(() => props.links || [
5
11
  ...Object.entries(appConfig.socials || {}).map(([key, url]) => ({
6
12
  'icon': `i-simple-icons-${key}`,
7
13
  'to': url,
@@ -18,9 +24,9 @@ const links = computed(() => [
18
24
  </script>
19
25
 
20
26
  <template>
21
- <template v-if="links.length">
27
+ <template v-if="footerLinks.length">
22
28
  <UButton
23
- v-for="(link, index) of links"
29
+ v-for="(link, index) of footerLinks"
24
30
  :key="index"
25
31
  size="sm"
26
32
  v-bind="{ color: 'neutral', variant: 'ghost', ...link }"
@@ -1,35 +1,26 @@
1
1
  <script setup lang="ts">
2
- import { useDocusI18n } from '../../composables/useDocusI18n'
2
+ import { useSiteI18n } from '../../composables/useSiteI18n'
3
+ import { useSiteHeader } from '../../composables/useSiteHeader'
4
+
5
+ const { data: header } = await useSiteHeader()
3
6
 
4
7
  const appConfig = useAppConfig()
5
8
  const site = useSiteConfig()
6
9
 
7
- const { localePath, isEnabled, locales } = useDocusI18n()
8
-
9
- const links = computed(() => appConfig.github && appConfig.github.url
10
- ? [
11
- {
12
- 'icon': 'i-simple-icons-github',
13
- 'to': appConfig.github.url,
14
- 'target': '_blank',
15
- 'aria-label': 'GitHub',
16
- },
17
- ]
18
- : [])
10
+ const { localePath, isEnabled, locales } = useSiteI18n()
19
11
  </script>
20
12
 
21
13
  <template>
22
14
  <UHeader
23
15
  :ui="{ center: 'flex-1' }"
24
16
  :to="localePath('/')"
25
- :title="appConfig.header?.title || site.name"
17
+ :title="header?.title || appConfig.header?.title || site.name"
26
18
  >
27
- <AppHeaderCenter />
28
-
29
19
  <template #title>
30
20
  <AppHeaderLogo class="h-6 w-auto shrink-0" />
31
21
  </template>
32
22
 
23
+ <AppHeaderCenter />
33
24
  <template #right>
34
25
  <AppHeaderCTA />
35
26
 
@@ -57,14 +48,6 @@ const links = computed(() => appConfig.github && appConfig.github.url
57
48
  <div class="h-8 w-8 animate-pulse bg-neutral-200 dark:bg-neutral-800 rounded-md" />
58
49
  </template>
59
50
  </ClientOnly>
60
-
61
- <template v-if="links?.length">
62
- <UButton
63
- v-for="(link, index) of links"
64
- :key="index"
65
- v-bind="{ color: 'neutral', variant: 'ghost', ...link }"
66
- />
67
- </template>
68
51
  </template>
69
52
 
70
53
  <template #toggle="{ open, toggle }">
@@ -1,11 +1,21 @@
1
1
  <script setup lang="ts">
2
2
  import type { ContentNavigationItem } from '@nuxt/content'
3
+ import { useSiteHeader } from '../../composables/useSiteHeader'
4
+
5
+ const { data: header } = await useSiteHeader()
3
6
 
4
7
  const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
5
8
  </script>
6
9
 
7
10
  <template>
11
+ <UNavigationMenu
12
+ v-if="header?.navigation"
13
+ :items="header?.navigation"
14
+ orientation="vertical"
15
+ class="-mx-2.5"
16
+ />
8
17
  <UContentNavigation
18
+ v-else
9
19
  highlight
10
20
  variant="link"
11
21
  :navigation="navigation"
@@ -1,5 +1,17 @@
1
+ <script setup lang="ts">
2
+ import { useSiteHeader } from '#imports'
3
+
4
+ const { data: header } = await useSiteHeader()
5
+ </script>
6
+
1
7
  <template>
8
+ <UNavigationMenu
9
+ v-if="header?.navigation"
10
+ :items="header?.navigation"
11
+ variant="link"
12
+ />
2
13
  <UContentSearchButton
14
+ v-else
3
15
  :collapsed="false"
4
16
  class="w-full"
5
17
  variant="soft"
@@ -1,16 +1,19 @@
1
1
  <script setup lang="ts">
2
+ import { useSiteHeader } from '#imports'
3
+
2
4
  const appConfig = useAppConfig()
5
+ const { data: header } = await useSiteHeader()
3
6
  </script>
4
7
 
5
8
  <template>
6
9
  <UColorModeImage
7
- v-if="appConfig.header?.logo?.dark || appConfig.header?.logo?.light"
8
- :light="appConfig.header?.logo?.light || appConfig.header?.logo?.dark"
9
- :dark="appConfig.header?.logo?.dark || appConfig.header?.logo?.light"
10
- :alt="appConfig.header?.logo?.alt || appConfig.header?.title"
10
+ v-if="header?.logo || appConfig.header?.logo?.dark || appConfig.header?.logo?.light"
11
+ :light="header?.logo?.light || appConfig.header?.logo?.light || appConfig.header?.logo?.dark"
12
+ :dark="header?.logo?.dark || appConfig.header?.logo?.dark || appConfig.header?.logo?.light"
13
+ :alt="header?.logo?.alt || appConfig.header?.logo?.alt || appConfig.header?.title"
11
14
  class="h-6 w-auto shrink-0"
12
15
  />
13
16
  <span v-else>
14
- {{ appConfig.header?.title || '{appConfig.header.title}' }}
17
+ {{ header?.title || appConfig.header?.title || '{appConfig.header.title}' }}
15
18
  </span>
16
19
  </template>
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  const appConfig = useAppConfig()
3
- const { t } = useDocusI18n()
3
+ const { t } = useSiteI18n()
4
4
  </script>
5
5
 
6
6
  <template>
@@ -4,7 +4,7 @@ import { useClipboard } from '@vueuse/core'
4
4
  const route = useRoute()
5
5
 
6
6
  const { copy, copied } = useClipboard()
7
- const { t } = useDocusI18n()
7
+ const { t } = useSiteI18n()
8
8
 
9
9
  const markdownLink = computed(() => `${window?.location?.origin}/raw${route.path}.md`)
10
10
  const items = [
@@ -0,0 +1,25 @@
1
+ import type { Collections, FooterCollectionItem } from '@nuxt/content'
2
+
3
+ export const useSiteFooter = () => {
4
+ const { locale, isEnabled } = useSiteI18n()
5
+ const defaultLocale = useRuntimeConfig().public.i18n.defaultLocale!
6
+ const collectionName = computed(() => isEnabled.value ? `footer_${locale.value}` : 'footer')
7
+
8
+ return useAsyncData(collectionName, async () => {
9
+ const customFooter = await queryCollection(collectionName.value as keyof Collections).first()
10
+ if (!customFooter) {
11
+ // attempt to find for the default language
12
+ if (isEnabled.value && defaultLocale) {
13
+ const fallbackFooter = await queryCollection(`footer_${defaultLocale}` as keyof Collections).first()
14
+ if (fallbackFooter) {
15
+ return fallbackFooter as FooterCollectionItem
16
+ }
17
+ }
18
+ return <FooterCollectionItem> {
19
+ socials: [] as FooterCollectionItem['socials'],
20
+ }
21
+ }
22
+
23
+ return customFooter as FooterCollectionItem
24
+ })
25
+ }
@@ -0,0 +1,26 @@
1
+ import type { Collections, HeaderCollectionItem } from '@nuxt/content'
2
+
3
+ export const useSiteHeader = () => {
4
+ const config = useAppConfig()
5
+ const { locale, isEnabled } = useSiteI18n()
6
+ const defaultLocale = useRuntimeConfig().public.i18n.defaultLocale!
7
+ const collectionName = computed(() => isEnabled.value ? `header_${locale.value}` : 'header')
8
+
9
+ return useAsyncData(collectionName, async () => {
10
+ const customHeader = await queryCollection(collectionName.value as keyof Collections).first()
11
+ if (!customHeader) {
12
+ if (isEnabled.value && defaultLocale) {
13
+ const fallbackHeader = await queryCollection(`header_${defaultLocale}` as keyof Collections).first()
14
+ if (fallbackHeader) {
15
+ return fallbackHeader as HeaderCollectionItem
16
+ }
17
+ }
18
+ return <HeaderCollectionItem> {
19
+ title: config.header.title,
20
+ logo: config.header.logo,
21
+ }
22
+ }
23
+
24
+ return customHeader as HeaderCollectionItem
25
+ })
26
+ }
@@ -1,7 +1,7 @@
1
1
  import type { LocaleObject } from '@nuxtjs/i18n'
2
2
  import en from '../../i18n/locales/en.json'
3
3
 
4
- export const useDocusI18n = () => {
4
+ export const useSiteI18n = () => {
5
5
  const config = useRuntimeConfig().public
6
6
  const isEnabled = ref(!!config.i18n)
7
7
 
package/app/error.vue CHANGED
@@ -7,7 +7,7 @@ const props = defineProps<{
7
7
  error: NuxtError
8
8
  }>()
9
9
 
10
- const { locale, locales, isEnabled, t, switchLocalePath } = useDocusI18n()
10
+ const { locale, locales, isEnabled, t, switchLocalePath } = useSiteI18n()
11
11
 
12
12
  const lang = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales]?.code || 'en')
13
13
  const dir = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales]?.dir || 'ltr')
@@ -9,7 +9,7 @@ definePageMeta({
9
9
  })
10
10
 
11
11
  const route = useRoute()
12
- const { locale, isEnabled } = useDocusI18n()
12
+ const { locale, isEnabled } = useSiteI18n()
13
13
  const navigation = inject<Ref<ContentNavigationItem[]>>('navigation')
14
14
 
15
15
  const collectionName = computed(() => isEnabled.value ? `pages_${locale.value}` : 'pages')
@@ -2,12 +2,12 @@
2
2
  import type { Collections } from '@nuxt/content'
3
3
 
4
4
  const route = useRoute()
5
- const { locale, isEnabled } = useDocusI18n()
5
+ const { locale, isEnabled } = useSiteI18n()
6
6
 
7
7
  // Dynamic collection name based on i18n status
8
8
  const collectionName = computed(() => isEnabled.value ? `landing_${locale.value}` : 'landing')
9
9
 
10
- const { data: page } = await useAsyncData(collectionName.value, () => queryCollection(collectionName.value as keyof Collections).path(route.path).first())
10
+ const { data: page } = await useAsyncData(collectionName.value, () => queryCollection(collectionName.value as keyof Omit<Collections, 'header' | 'footer'>).path(route.path).first())
11
11
  if (!page.value) {
12
12
  throw createError({ statusCode: 404, statusMessage: import.meta.dev ? `Page ${route.path} not found in ${collectionName.value}` : 'Page not found', fatal: true })
13
13
  }
package/content.config.ts CHANGED
@@ -7,6 +7,50 @@ const { options } = useNuxt()
7
7
  const cwd = joinURL(options.rootDir, 'content')
8
8
  const locales = options.i18n?.locales
9
9
 
10
+ const variantEnum = z.enum(['solid', 'outline', 'subtle', 'soft', 'ghost', 'link'])
11
+ const colorEnum = z.enum(['primary', 'secondary', 'neutral', 'error', 'warning', 'success', 'info'])
12
+ const sizeEnum = z.enum(['xs', 'sm', 'md', 'lg', 'xl'])
13
+ // const orientationEnum = z.enum(['vertical', 'horizontal'])
14
+
15
+ // const createBaseSchema = () => z.object({
16
+ // title: z.string().nonempty(),
17
+ // description: z.string().nonempty(),
18
+ // })
19
+
20
+ // const createFeatureItemSchema = () => createBaseSchema().extend({
21
+ // icon: z.string().nonempty().editor({ input: 'icon' }),
22
+ // })
23
+
24
+ const createLinkSchema = () => z.object({
25
+ class: z.string().optional(),
26
+ label: z.string().nonempty(),
27
+ to: z.string().nonempty(),
28
+ icon: z.string().optional().editor({ input: 'icon' }),
29
+ size: sizeEnum.optional(),
30
+ trailing: z.boolean().optional(),
31
+ trailingIcon: z.string().optional().editor({ input: 'icon' }),
32
+ target: z.string().optional(),
33
+ color: colorEnum.optional(),
34
+ variant: variantEnum.optional(),
35
+ })
36
+
37
+ const createNavigationSchema = () => z.object({
38
+ label: z.string().nonempty(),
39
+ class: z.string().optional(),
40
+ to: z.string().nonempty(),
41
+ icon: z.string().optional().editor({ input: 'icon' }),
42
+ size: sizeEnum.optional(),
43
+ trailing: z.boolean().optional(),
44
+ trailingIcon: z.string().optional().editor({ input: 'icon' }),
45
+ target: z.string().optional(),
46
+ color: colorEnum.optional(),
47
+ variant: variantEnum.optional(),
48
+ })
49
+
50
+ const createNavigationItemSchema = () => createNavigationSchema().extend({
51
+ children: z.array(createNavigationSchema()).optional(),
52
+ })
53
+
10
54
  const createPageSchema = () => z.object({
11
55
  links: z.array(z.object({
12
56
  label: z.string(),
@@ -16,6 +60,24 @@ const createPageSchema = () => z.object({
16
60
  })).optional(),
17
61
  })
18
62
 
63
+ const createHeaderSchema = () => z.object({
64
+ title: z.string().optional(),
65
+ logo: z.object({
66
+ light: z.string(),
67
+ dark: z.string().optional(),
68
+ alt: z.string().optional(),
69
+ }).optional(),
70
+ navigation: z.array(createNavigationItemSchema()).optional(),
71
+ })
72
+
73
+ const createFooterSchema = () => z.object({
74
+ sections: z.array(z.object({
75
+ label: z.string().nonempty(),
76
+ children: z.array(createLinkSchema()),
77
+ })),
78
+ socials: z.array(createLinkSchema()),
79
+ })
80
+
19
81
  let collections: Record<string, DefinedCollection>
20
82
 
21
83
  if (locales && Array.isArray(locales)) {
@@ -37,10 +99,34 @@ if (locales && Array.isArray(locales)) {
37
99
  cwd,
38
100
  include: `${code}/**/*`,
39
101
  prefix: `/${code}`,
40
- exclude: [`${code}/index.md`],
102
+ exclude: [
103
+ `${code}/index.md`,
104
+ `${code}/header.yml`,
105
+ `${code}/footer.yml`,
106
+ ],
41
107
  },
42
108
  schema: createPageSchema(),
43
109
  })
110
+
111
+ collections[`header_${code}`] = defineCollection({
112
+ type: 'data',
113
+ source: {
114
+ cwd,
115
+ include: `${code}/header.yml`,
116
+ prefix: `/${code}`,
117
+ },
118
+ schema: createHeaderSchema(),
119
+ })
120
+
121
+ collections[`footer_${code}`] = defineCollection({
122
+ type: 'data',
123
+ source: {
124
+ cwd,
125
+ include: `${code}/footer.yml`,
126
+ prefix: `/${code}`,
127
+ },
128
+ schema: createFooterSchema(),
129
+ })
44
130
  }
45
131
  }
46
132
  else {
@@ -57,16 +143,29 @@ else {
57
143
  source: {
58
144
  cwd,
59
145
  include: '**',
60
- exclude: ['index.md'],
146
+ exclude: [
147
+ 'index.md',
148
+ 'header.yml',
149
+ 'footer.yml',
150
+ ],
61
151
  },
62
152
  schema: createPageSchema(),
63
153
  }),
64
- contact: defineCollection({
65
- type: 'page',
154
+ header: defineCollection({
155
+ type: 'data',
156
+ source: {
157
+ cwd,
158
+ include: 'header.yml',
159
+ },
160
+ schema: createHeaderSchema(),
161
+ }),
162
+ footer: defineCollection({
163
+ type: 'data',
66
164
  source: {
67
165
  cwd,
68
- include: 'contact.md',
166
+ include: 'footer.yml',
69
167
  },
168
+ schema: createFooterSchema(),
70
169
  }),
71
170
  }
72
171
  }
@@ -9,13 +9,13 @@ export default defineNuxtModule({
9
9
 
10
10
  const isI18nEnabled = !!(nuxt.options.i18n && nuxt.options.i18n.locales)
11
11
 
12
- // Ensure useDocusI18n is available in the app
12
+ // Ensure useSiteI18n is available in the app
13
13
  nuxt.hook('imports:extend', (imports) => {
14
- if (imports.some(i => i.name === 'useDocusI18n')) return
14
+ if (imports.some(i => i.name === 'useSiteI18n')) return
15
15
 
16
16
  imports.push({
17
- name: 'useDocusI18n',
18
- from: resolve('../app/composables/useDocusI18n'),
17
+ name: 'useSiteI18n',
18
+ from: resolve('../app/composables/useSiteI18n'),
19
19
  })
20
20
  })
21
21
 
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": "1.0.3",
4
+ "version": "1.0.5",
5
5
  "type": "module",
6
6
  "main": "./nuxt.config.ts",
7
7
  "repository": {
@@ -29,7 +29,7 @@ export default eventHandler(async (event) => {
29
29
  }
30
30
  }
31
31
 
32
- const page = await queryCollection(event, collectionName as keyof Collections).path(path).first()
32
+ const page = await queryCollection(event, collectionName as keyof Omit<Collections, 'header' | 'footer'>).path(path).first()
33
33
  if (!page) {
34
34
  throw createError({ statusCode: 404, statusMessage: 'Page not found', fatal: true })
35
35
  }