@uxfront/layer-docs 0.4.0 → 0.5.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.
Files changed (99) hide show
  1. package/README.md +31 -220
  2. package/app/app.config.ts +8 -92
  3. package/app/components/content/FrameworkSwitcher.vue +66 -40
  4. package/app/components/docs/DocsAsideLeftTop.vue +9 -15
  5. package/app/components/docs/DocsFrameworkSelect.vue +4 -7
  6. package/app/composables/useFramework.ts +30 -38
  7. package/nuxt.config.ts +14 -170
  8. package/package.json +10 -77
  9. package/CHANGELOG.md +0 -180
  10. package/LICENSE +0 -21
  11. package/app/app.vue +0 -138
  12. package/app/assets/css/main.css +0 -15
  13. package/app/components/IconMenuToggle.vue +0 -92
  14. package/app/components/LanguageSelect.vue +0 -73
  15. package/app/components/MorphingGradientBackground.vue +0 -261
  16. package/app/components/OgImage/OgImageDocs.satori.vue +0 -40
  17. package/app/components/OgImage/OgImageLanding.satori.vue +0 -41
  18. package/app/components/app/AppFooter.vue +0 -13
  19. package/app/components/app/AppFooterCenter.vue +0 -17
  20. package/app/components/app/AppFooterLeft.vue +0 -21
  21. package/app/components/app/AppFooterRight.vue +0 -33
  22. package/app/components/app/AppHeader.vue +0 -123
  23. package/app/components/app/AppHeaderAttribution.vue +0 -45
  24. package/app/components/app/AppHeaderBody.vue +0 -14
  25. package/app/components/app/AppHeaderCTA.vue +0 -31
  26. package/app/components/app/AppHeaderCenter.vue +0 -10
  27. package/app/components/app/AppHeaderLogo.vue +0 -16
  28. package/app/components/app/AppOgDecoration.vue +0 -27
  29. package/app/components/app/AppOgLogo.vue +0 -19
  30. package/app/components/app/AppSearch.vue +0 -59
  31. package/app/components/app/AppSubHeader.vue +0 -21
  32. package/app/components/content/BrowserFrame.vue +0 -28
  33. package/app/components/content/GradientPageHero.vue +0 -35
  34. package/app/components/content/StorybookEmbed.vue +0 -160
  35. package/app/components/content/Video.vue +0 -103
  36. package/app/components/docs/DocsAsideLeftBody.vue +0 -20
  37. package/app/components/docs/DocsAsideRightBottom.vue +0 -15
  38. package/app/components/docs/DocsPageHeaderLinks.vue +0 -75
  39. package/app/composables/useDocsSections.ts +0 -57
  40. package/app/composables/useDocusI18n.ts +0 -49
  41. package/app/constants/sections.ts +0 -25
  42. package/app/error.vue +0 -140
  43. package/app/layouts/default.vue +0 -24
  44. package/app/pages/[[lang]]/[...slug].vue +0 -58
  45. package/app/pages/[[lang]]/docs/[section]/[...slug].vue +0 -180
  46. package/app/plugins/i18n.ts +0 -21
  47. package/app/plugins/posthog.client.ts +0 -56
  48. package/app/types/non-route-categories.ts +0 -12
  49. package/app/utils/flattenNavigation.ts +0 -22
  50. package/app/utils/foldNonRouteCategories.ts +0 -47
  51. package/app/utils/prerender.ts +0 -9
  52. package/app/utils/storybookEmbed.test.ts +0 -98
  53. package/app/utils/storybookEmbed.ts +0 -93
  54. package/i18n/locales/ar.json +0 -24
  55. package/i18n/locales/be.json +0 -24
  56. package/i18n/locales/bn.json +0 -24
  57. package/i18n/locales/ca.json +0 -24
  58. package/i18n/locales/ckb.json +0 -24
  59. package/i18n/locales/cs.json +0 -24
  60. package/i18n/locales/da.json +0 -24
  61. package/i18n/locales/de.json +0 -24
  62. package/i18n/locales/el.json +0 -24
  63. package/i18n/locales/en.json +0 -24
  64. package/i18n/locales/et.json +0 -24
  65. package/i18n/locales/fr.json +0 -24
  66. package/i18n/locales/he.json +0 -24
  67. package/i18n/locales/hi.json +0 -24
  68. package/i18n/locales/hy.json +0 -24
  69. package/i18n/locales/it.json +0 -24
  70. package/i18n/locales/ja.json +0 -24
  71. package/i18n/locales/kk.json +0 -24
  72. package/i18n/locales/km.json +0 -24
  73. package/i18n/locales/ko.json +0 -24
  74. package/i18n/locales/ky.json +0 -24
  75. package/i18n/locales/lb.json +0 -24
  76. package/i18n/locales/ms.json +0 -24
  77. package/i18n/locales/nb.json +0 -24
  78. package/i18n/locales/pl.json +0 -24
  79. package/i18n/locales/ru.json +0 -24
  80. package/i18n/locales/sl.json +0 -24
  81. package/i18n/locales/sv.json +0 -24
  82. package/i18n/locales/uk.json +0 -24
  83. package/i18n/locales/ur.json +0 -24
  84. package/i18n/locales/vi.json +0 -24
  85. package/modules/config.ts +0 -144
  86. package/modules/optimizeDeps.ts +0 -45
  87. package/modules/routing.ts +0 -20
  88. package/nuxt.schema.ts +0 -374
  89. package/server/plugins/llms-redirect.ts +0 -60
  90. package/server/routes/raw/[...slug].md.get.ts +0 -74
  91. package/storybook/index.test.ts +0 -110
  92. package/storybook/index.ts +0 -362
  93. package/test/brand-palette.ts +0 -235
  94. package/test/no-brand-leakage.test.ts +0 -124
  95. package/tsconfig.json +0 -17
  96. package/utils/accent.ts +0 -80
  97. package/utils/content.ts +0 -193
  98. package/utils/git.ts +0 -114
  99. package/utils/meta.ts +0 -28
@@ -1,103 +0,0 @@
1
- <script setup lang="ts">
2
- import { useIntersectionObserver } from "@vueuse/core";
3
-
4
- interface Props {
5
- poster?: string;
6
- src?: string;
7
- sources?: string[];
8
- controls?: boolean;
9
- autoplay?: boolean;
10
- loop?: boolean;
11
- muted?: boolean;
12
- playsinline?: boolean;
13
- pauseOnExit?: boolean;
14
- }
15
-
16
- const props = withDefaults(defineProps<Props>(), {
17
- poster: "",
18
- src: "",
19
- sources: undefined,
20
- controls: false,
21
- autoplay: false,
22
- loop: false,
23
- muted: true,
24
- playsinline: true,
25
- pauseOnExit: true,
26
- });
27
-
28
- const playing = ref(false);
29
- const loaded = ref(false);
30
- const isIntersecting = ref(false);
31
- const posterLoaded = ref(false);
32
-
33
- const videoRef = useTemplateRef<HTMLVideoElement | null>("video");
34
-
35
- const { stop } = useIntersectionObserver(videoRef, ([entry], observerElement) => {
36
- isIntersecting.value = entry?.isIntersecting || false;
37
- });
38
-
39
- const allSources = computed(() => props.sources ?? [props.src]);
40
-
41
- const lazyPoster = computed(() => {
42
- if (isIntersecting.value && !posterLoaded.value) {
43
- posterLoaded.value = true;
44
- return props.poster;
45
- } else if (posterLoaded.value) {
46
- return props.poster;
47
- } else {
48
- return "";
49
- }
50
- });
51
-
52
- onUnmounted(() => {
53
- stop();
54
- });
55
-
56
- watch(isIntersecting, (currentValue, previousValue) => {
57
- // Pause video if it was playing and is no longer intersecting
58
- if (props.pauseOnExit) {
59
- if (previousValue && playing.value) {
60
- videoRef.value?.pause();
61
- } else if (currentValue && !playing.value) {
62
- videoRef.value?.play();
63
- }
64
- }
65
-
66
- // Load if video is intersecting
67
- if (currentValue && !loaded.value) {
68
- videoRef.value?.load();
69
- }
70
- });
71
-
72
- const onPlay = () => {
73
- playing.value = true;
74
- };
75
-
76
- const onPause = () => {
77
- playing.value = false;
78
- };
79
-
80
- const onLoaded = () => {
81
- loaded.value = true;
82
- };
83
- </script>
84
-
85
- <template>
86
- <video
87
- ref="video"
88
- class="w-full h-full object-cover"
89
- preload="none"
90
- :poster="lazyPoster"
91
- :src="allSources[0]"
92
- :controls="controls"
93
- :autoplay="autoplay"
94
- :loop="loop"
95
- :muted="muted"
96
- playsinline
97
- @play="onPlay"
98
- @pause="onPause"
99
- @loadeddata="onLoaded"
100
- >
101
- <source v-for="source in allSources" :src="source" :key="source" />
102
- </video>
103
- </template>
@@ -1,20 +0,0 @@
1
- <script setup lang="ts">
2
- import type { ContentNavigationItem } from "@nuxt/content";
3
-
4
- const route = useRoute();
5
- const navigationMap = inject<Ref<Record<string, ContentNavigationItem[]> | null>>("navigation");
6
-
7
- const navigation = computed<ContentNavigationItem[]>(() => {
8
- const section = findDocsSectionBySlug(route.params.section as string);
9
- if (!section) return [];
10
- return navigationMap?.value?.[section.key] ?? [];
11
- });
12
- </script>
13
-
14
- <template>
15
- <UContentNavigation :key="route.params.section as string" highlight :navigation="navigation">
16
- <template #link-title="{ link }">
17
- {{ link.title }}
18
- </template>
19
- </UContentNavigation>
20
- </template>
@@ -1,15 +0,0 @@
1
- <script setup lang="ts">
2
- const appConfig = useAppConfig();
3
- const { t } = useDocusI18n();
4
- </script>
5
-
6
- <template>
7
- <div v-if="appConfig.toc?.bottom?.links?.length" class="hidden lg:block space-y-6">
8
- <USeparator type="dashed" />
9
-
10
- <UPageLinks
11
- :title="appConfig.toc?.bottom?.title || t('docs.links')"
12
- :links="appConfig.toc?.bottom?.links"
13
- />
14
- </div>
15
- </template>
@@ -1,75 +0,0 @@
1
- <script setup lang="ts">
2
- import { useClipboard } from "@vueuse/core";
3
-
4
- const route = useRoute();
5
-
6
- const { copy, copied } = useClipboard();
7
- const { t } = useDocusI18n();
8
-
9
- const markdownLink = computed(() => `${window?.location?.origin}/raw${route.path}.md`);
10
- const items = [
11
- {
12
- label: t("docs.copy.link"),
13
- icon: "i-lucide-link",
14
- onSelect() {
15
- copy(markdownLink.value);
16
- },
17
- },
18
- {
19
- label: t("docs.copy.view"),
20
- icon: "i-simple-icons:markdown",
21
- target: "_blank",
22
- to: markdownLink.value,
23
- },
24
- {
25
- label: t("docs.copy.gpt"),
26
- icon: "i-simple-icons:openai",
27
- target: "_blank",
28
- to: `https://chatgpt.com/?hints=search&q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
29
- },
30
- {
31
- label: t("docs.copy.claude"),
32
- icon: "i-simple-icons:anthropic",
33
- target: "_blank",
34
- to: `https://claude.ai/new?q=${encodeURIComponent(`Read ${markdownLink.value} so I can ask questions about it.`)}`,
35
- },
36
- ];
37
-
38
- async function copyPage() {
39
- const page = await $fetch<string>(`/raw${route.path}.md`);
40
- copy(page);
41
- }
42
- </script>
43
-
44
- <template>
45
- <UFieldGroup size="sm">
46
- <UButton
47
- :label="t('docs.copy.page')"
48
- :icon="copied ? 'i-lucide-check' : 'i-lucide-copy'"
49
- color="neutral"
50
- variant="soft"
51
- :ui="{
52
- leadingIcon: 'text-neutral size-3.5',
53
- }"
54
- @click="copyPage"
55
- />
56
-
57
- <UDropdownMenu
58
- size="sm"
59
- :items="items"
60
- :content="{
61
- align: 'end',
62
- side: 'bottom',
63
- sideOffset: 8,
64
- }"
65
- >
66
- <UButton
67
- icon="i-lucide-chevron-down"
68
- color="neutral"
69
- variant="soft"
70
- class="border-l border-muted"
71
- :aria-label="t('docs.copy.options')"
72
- />
73
- </UDropdownMenu>
74
- </UFieldGroup>
75
- </template>
@@ -1,57 +0,0 @@
1
- import type { ContentNavigationItem } from "@nuxt/content";
2
-
3
- export type DocsSectionLink = {
4
- label: string;
5
- icon: string;
6
- slug: string;
7
- to: string;
8
- active: boolean;
9
- };
10
-
11
- function findFirstLeafPath(items: ContentNavigationItem[] | undefined): string | undefined {
12
- if (!items?.length) return undefined;
13
- for (const item of items) {
14
- if (item.page !== false && item.path) return item.path;
15
- if (item.children?.length) {
16
- const nested = findFirstLeafPath(item.children);
17
- if (nested) return nested;
18
- }
19
- }
20
- return items[0]?.path;
21
- }
22
-
23
- /**
24
- * Flattens the consumer's `DOCS_SECTIONS` descriptor into the sub-header link
25
- * list, resolving each section's first content leaf as its landing target.
26
- * `DOCS_SECTIONS` / `findDocsSectionBySlug` are auto-imported from the
27
- * consumer's `app/constants/` directory (declared via `imports.dirs`).
28
- */
29
- export function useDocsSections() {
30
- const route = useRoute();
31
- const navigation = inject<Ref<Record<string, ContentNavigationItem[]> | null>>("navigation");
32
-
33
- const sections = computed<DocsSectionLink[]>(() =>
34
- DOCS_SECTIONS.map((section): DocsSectionLink => {
35
- const tree = navigation?.value?.[section.key];
36
- const firstPath = findFirstLeafPath(tree);
37
- return {
38
- label: section.label,
39
- icon: section.icon,
40
- slug: section.slug,
41
- to: firstPath || `/docs/${section.slug}`,
42
- active: route.params.section === section.slug,
43
- };
44
- }),
45
- );
46
-
47
- const activeSection = computed(() => sections.value.find((section) => section.active));
48
-
49
- /**
50
- * Whether the section switcher is worth rendering. A single section (or none)
51
- * gives the reader nothing to switch between, so the sub-header is suppressed
52
- * entirely rather than rendering an empty bar.
53
- */
54
- const hasSectionSwitcher = computed(() => sections.value.length > 1);
55
-
56
- return { sections, activeSection, hasSectionSwitcher };
57
- }
@@ -1,49 +0,0 @@
1
- import type { LocaleObject } from "@nuxtjs/i18n";
2
- import type { Ref } from "vue";
3
- import en from "../../i18n/locales/en.json";
4
-
5
- /**
6
- * `@nuxtjs/i18n` auto-imports. This layer ships with i18n disabled by default —
7
- * the module is not registered unless the consumer opts in — so the enabled
8
- * branch below is inert until then. These declarations let the file typecheck
9
- * without the module generating the globals; when i18n is enabled (the consumer
10
- * registers `@nuxtjs/i18n`) the real composables are used.
11
- */
12
- declare function useI18n(): { locale: Ref<string>; t: (key: string) => string };
13
- declare function useLocalePath(): (route: string, locale?: string) => string;
14
- declare function useSwitchLocalePath(): (locale: string) => string;
15
-
16
- export const useDocusI18n = () => {
17
- const config = useRuntimeConfig().public;
18
- const isEnabled = ref(!!config.i18n);
19
-
20
- if (!isEnabled.value) {
21
- return {
22
- isEnabled,
23
- locale: ref("en"),
24
- locales: [],
25
- localePath: (path: string) => path,
26
- switchLocalePath: (_locale?: string): string => "",
27
- t: (key: string): string => {
28
- const path = key.split(".");
29
- return path.reduce(
30
- (acc: unknown, curr) => (acc as Record<string, unknown>)?.[curr],
31
- en,
32
- ) as string;
33
- },
34
- };
35
- }
36
-
37
- const { locale, t } = useI18n();
38
- const filteredLocales =
39
- (config.docus as { filteredLocales: LocaleObject<string>[] })?.filteredLocales || [];
40
-
41
- return {
42
- isEnabled,
43
- locale,
44
- locales: filteredLocales,
45
- t,
46
- localePath: useLocalePath(),
47
- switchLocalePath: useSwitchLocalePath(),
48
- };
49
- };
@@ -1,25 +0,0 @@
1
- import type { DocsSectionDescriptor } from "../../utils/content";
2
-
3
- /**
4
- * Section descriptor as consumed by the theme's runtime nav/search components.
5
- * Extends the build-time {@link DocsSectionDescriptor} with the UI-only fields
6
- * (`icon`) those components read.
7
- */
8
- export interface DocsSectionItem extends DocsSectionDescriptor {
9
- /** Icon name (e.g. `i-lucide-book-open`) shown next to the section link. */
10
- icon: string;
11
- }
12
-
13
- /**
14
- * Neutral default topology: no sections. The consuming app supplies the real
15
- * `DOCS_SECTIONS` by placing its own `app/constants/sections.ts`; Nuxt
16
- * auto-import precedence (consumer app dir > layer dir) makes the consumer's
17
- * array win at build time. Shipping an empty default here keeps the layer's own
18
- * type-check and build green in isolation without leaking any brand topology.
19
- */
20
- export const DOCS_SECTIONS: readonly DocsSectionItem[] = [];
21
-
22
- /** Resolve a section by its URL slug, or `undefined` when none matches. */
23
- export function findDocsSectionBySlug(slug: string): DocsSectionItem | undefined {
24
- return DOCS_SECTIONS.find((section) => section.slug === slug);
25
- }
package/app/error.vue DELETED
@@ -1,140 +0,0 @@
1
- <script setup lang="ts">
2
- import type { NuxtError } from "#app";
3
- import type { ContentNavigationItem, PageCollections } from "@nuxt/content";
4
- import * as nuxtUiLocales from "@nuxt/ui/locale";
5
-
6
- const props = defineProps<{
7
- error: NuxtError;
8
- }>();
9
-
10
- const { locale, locales, isEnabled, t, switchLocalePath } = useDocusI18n();
11
- const appConfig = useAppConfig();
12
-
13
- const lang = computed(
14
- () => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales]?.code || "en",
15
- );
16
- const dir = computed(() => nuxtUiLocales[locale.value as keyof typeof nuxtUiLocales]?.dir || "ltr");
17
- useHead({
18
- htmlAttrs: {
19
- lang,
20
- dir,
21
- },
22
- });
23
-
24
- const localizedError = computed(() => {
25
- return {
26
- ...props.error,
27
- statusMessage: t("common.error.title"),
28
- };
29
- });
30
-
31
- useSeoMeta({
32
- title: () => t("common.error.title"),
33
- description: () => t("common.error.description"),
34
- });
35
-
36
- if (isEnabled.value) {
37
- const route = useRoute();
38
- const defaultLocale = (useRuntimeConfig().public.i18n as { defaultLocale: string }).defaultLocale;
39
- onMounted(() => {
40
- const currentLocale = route.path.split("/")[1];
41
- if (!locales.some((locale) => locale.code === currentLocale)) {
42
- return navigateTo(switchLocalePath(defaultLocale) as string);
43
- }
44
- });
45
- }
46
-
47
- const getCollectionName = (key: string) =>
48
- (isEnabled.value ? `docs_${key}_${locale.value}` : `docs_${key}`) as keyof PageCollections;
49
-
50
- const { data: navigation } = await useAsyncData(
51
- () => `navigation_${locale.value}`,
52
- async () => {
53
- const results = await Promise.all(
54
- DOCS_SECTIONS.map(async (section) => {
55
- const collectionName = getCollectionName(section.key);
56
- const data = await queryCollectionNavigation(collectionName);
57
- const rootResult =
58
- data.find((item) => item.path === `/docs/${section.slug}`)?.children ||
59
- data.find((item) => item.path === "/docs")?.children ||
60
- data ||
61
- [];
62
- const localeResult =
63
- rootResult.find((item) => item.path === `/${locale.value}`)?.children || rootResult;
64
- const sectionPath = `/docs/${section.slug}`;
65
- let result: ContentNavigationItem[];
66
- if (Array.isArray(section.folder)) {
67
- const rootIdx = "rootFolder" in section ? (section.rootFolder as number) : -1;
68
- const rootWrapper =
69
- localeResult.length === 1 && localeResult[0]?.path === sectionPath
70
- ? localeResult[0]
71
- : null;
72
- const allItems = rootWrapper ? (rootWrapper.children ?? []) : localeResult;
73
-
74
- if (rootIdx >= 0) {
75
- const nonRootPaths = new Set(
76
- section.folder
77
- .filter((_: string, i: number) => i !== rootIdx)
78
- .map((f: string) => `${sectionPath}/${f.replace(/^\d+\./, "")}`),
79
- );
80
- const items: ContentNavigationItem[] = [];
81
- for (let i = 0; i < section.folder.length; i++) {
82
- if (i === rootIdx) {
83
- items.push({
84
- ...rootWrapper,
85
- title: rootWrapper?.title ?? section.label,
86
- path: sectionPath,
87
- children: allItems.filter((item) => !nonRootPaths.has(item.path)),
88
- } as ContentNavigationItem);
89
- } else {
90
- const found = allItems.find(
91
- (item) =>
92
- item.path === `${sectionPath}/${section.folder[i]?.replace(/^\d+\./, "")}`,
93
- );
94
- if (found) items.push(found);
95
- }
96
- }
97
- result = items;
98
- } else {
99
- result = section.folder
100
- .map((folder: string) =>
101
- allItems.find(
102
- (item) => item.path === `${sectionPath}/${folder.replace(/^\d+\./, "")}`,
103
- ),
104
- )
105
- .filter((item): item is ContentNavigationItem => item !== undefined);
106
- }
107
- } else {
108
- result = localeResult;
109
- }
110
- return [
111
- section.key,
112
- foldNonRouteCategories(flattenNavigation(result), appConfig.nonRouteCategories ?? {}),
113
- ] as const;
114
- }),
115
- );
116
- return Object.fromEntries(results) as Record<string, ContentNavigationItem[]>;
117
- },
118
- { watch: [locale] },
119
- );
120
-
121
- const flatNavigation = computed(() =>
122
- navigation.value ? Object.values(navigation.value).flat() : [],
123
- );
124
-
125
- provide("navigation", navigation);
126
- </script>
127
-
128
- <template>
129
- <UApp>
130
- <AppHeader />
131
-
132
- <UError :error="localizedError" />
133
-
134
- <AppFooter />
135
-
136
- <ClientOnly>
137
- <AppSearch :navigation="flatNavigation" />
138
- </ClientOnly>
139
- </UApp>
140
- </template>
@@ -1,24 +0,0 @@
1
- <script setup lang="ts">
2
- const route = useRoute();
3
- const isDocs = computed(() => route.path.startsWith("/docs"));
4
- </script>
5
-
6
- <template>
7
- <AppHeader />
8
- <UMain>
9
- <slot v-if="!isDocs" />
10
- <UContainer v-else>
11
- <UPage>
12
- <template #left>
13
- <UPageAside>
14
- <DocsAsideLeftTop />
15
- <DocsAsideLeftBody />
16
- </UPageAside>
17
- </template>
18
-
19
- <slot />
20
- </UPage>
21
- </UContainer>
22
- </UMain>
23
- <AppFooter />
24
- </template>
@@ -1,58 +0,0 @@
1
- <script setup lang="ts">
2
- import type { Collections } from "@nuxt/content";
3
- import { kebabCase } from "scule";
4
-
5
- definePageMeta({
6
- layout: "default",
7
- });
8
-
9
- const route = useRoute();
10
- const { locale, isEnabled } = useDocusI18n();
11
-
12
- // Dynamic collection name based on i18n status
13
- const collectionName = computed(() => (isEnabled.value ? `landing_${locale.value}` : "landing"));
14
-
15
- const { data: page } = await useAsyncData(kebabCase(route.path), () =>
16
- queryCollection(collectionName.value as keyof Collections)
17
- .path(route.path)
18
- .first(),
19
- );
20
- if (!page.value) {
21
- throw createError({
22
- statusCode: 404,
23
- statusMessage: "Page not found",
24
- fatal: true,
25
- });
26
- }
27
-
28
- const title = page.value.seo?.title || page.value.title;
29
- const description = page.value.seo?.description || page.value.description;
30
-
31
- useSeoMeta({
32
- title,
33
- description,
34
- ogTitle: title,
35
- ogDescription: description,
36
- });
37
-
38
- const appConfig = useAppConfig();
39
-
40
- // An explicit `seo.ogImage` in the page's front matter wins; otherwise the card
41
- // is generated from the shared template.
42
- if (page.value?.seo?.ogImage) {
43
- useSeoMeta({
44
- ogImage: page.value.seo.ogImage,
45
- twitterImage: page.value.seo.ogImage,
46
- });
47
- } else {
48
- defineOgImage("DocsSatori", {
49
- headline: appConfig.header?.title,
50
- title,
51
- description,
52
- });
53
- }
54
- </script>
55
-
56
- <template>
57
- <ContentRenderer v-if="page" :value="page" />
58
- </template>