valaxy 0.27.0 → 0.28.0-beta.2

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 (49) hide show
  1. package/client/components/AppLink.vue +4 -0
  2. package/client/components/ClientOnly.ts +12 -0
  3. package/client/components/ValaxyDynamicComponent.vue +22 -14
  4. package/client/components/ValaxyLogo.vue +1 -1
  5. package/client/components/ValaxyOverlay.vue +1 -1
  6. package/client/composables/codeGroups.ts +3 -3
  7. package/client/composables/collections.ts +101 -2
  8. package/client/composables/common.ts +6 -5
  9. package/client/composables/dark.ts +1 -1
  10. package/client/composables/features/collapse-code.ts +1 -1
  11. package/client/composables/features/copy-markdown.ts +85 -0
  12. package/client/composables/features/index.ts +1 -0
  13. package/client/composables/outline/anchor.ts +1 -1
  14. package/client/composables/post/index.ts +104 -2
  15. package/client/config.ts +4 -2
  16. package/client/entry-ssr.ts +25 -0
  17. package/client/layouts/README.md +1 -1
  18. package/client/locales/en.yml +12 -0
  19. package/client/locales/zh-CN.yml +12 -0
  20. package/client/main.ts +122 -24
  21. package/client/modules/components.ts +2 -2
  22. package/client/modules/floating-vue.ts +2 -3
  23. package/client/modules/valaxy.ts +2 -3
  24. package/client/setup/main.ts +2 -3
  25. package/client/setups.ts +24 -3
  26. package/client/styles/common/code.scss +8 -8
  27. package/client/styles/common/hamburger.scss +2 -2
  28. package/client/styles/common/markdown.scss +2 -2
  29. package/client/styles/common/transition.scss +2 -2
  30. package/client/styles/components/code-group.scss +2 -2
  31. package/client/styles/components/custom-block.scss +1 -1
  32. package/client/styles/css-vars.scss +7 -6
  33. package/client/styles/vars.scss +4 -3
  34. package/client/tsconfig.json +1 -1
  35. package/client/types/collection.ts +27 -0
  36. package/client/types/index.ts +2 -2
  37. package/client/utils/time.ts +1 -1
  38. package/dist/node/cli/index.mjs +14 -9
  39. package/dist/node/index.d.mts +216 -11
  40. package/dist/node/index.mjs +19 -10
  41. package/dist/shared/{valaxy.DpV6HHc6.mjs → valaxy.DXqMwOZX.mjs} +4034 -2514
  42. package/dist/shared/{valaxy._i636HSR.d.mts → valaxy.JIuR8V4d.d.mts} +111 -2
  43. package/dist/types/index.d.mts +2 -2
  44. package/index.d.ts +1 -0
  45. package/package.json +36 -36
  46. package/shared/node/i18n.ts +15 -1
  47. package/types/config.ts +74 -0
  48. package/types/frontmatter/page.ts +6 -1
  49. package/types/posts.ts +9 -1
@@ -4,6 +4,10 @@ import { computed } from 'vue'
4
4
  import { RouterLink } from 'vue-router'
5
5
  import { EXTERNAL_URL_RE } from '../../shared'
6
6
 
7
+ defineOptions({
8
+ inheritAttrs: false,
9
+ })
10
+
7
11
  const props = defineProps<{
8
12
  showExternalIcon?: boolean
9
13
  to?: string
@@ -0,0 +1,12 @@
1
+ import { defineComponent, onMounted, ref } from 'vue'
2
+
3
+ export default defineComponent({
4
+ name: 'ClientOnly',
5
+ setup(_, { slots }) {
6
+ const show = ref(false)
7
+ onMounted(() => {
8
+ show.value = true
9
+ })
10
+ return () => show.value ? slots.default?.() : null
11
+ },
12
+ })
@@ -1,6 +1,6 @@
1
1
  <script setup lang="ts">
2
2
  import type { Component } from 'vue'
3
- import { compile, defineAsyncComponent, shallowRef, watch } from 'vue'
3
+ import { compile, computed, defineComponent } from 'vue'
4
4
 
5
5
  const props = withDefaults(
6
6
  defineProps<{
@@ -12,22 +12,30 @@ const props = withDefaults(
12
12
  },
13
13
  )
14
14
 
15
- const dynamicComponent = shallowRef<Component | null>(null)
15
+ // Separate compile from component creation — compile() is expensive and
16
+ // should only re-run when the template string itself changes.
17
+ const compiledRender = computed(() => {
18
+ if (!props.templateStr)
19
+ return null
20
+ return compile(props.templateStr)
21
+ })
16
22
 
17
- async function createComponent() {
18
- const render = compile(props.templateStr)
19
- const componentDefinition = {
23
+ // defineComponent creates a proper component instance so that compile()'s
24
+ // render function can access data properties through the instance context.
25
+ // Avoids defineAsyncComponent which causes "missing template" warnings in SSG.
26
+ const dynamicComponent = computed<Component | null>(() => {
27
+ const render = compiledRender.value
28
+ if (!render)
29
+ return null
30
+ return defineComponent({
31
+ data: () => ({ ...props.data }),
20
32
  render,
21
- data: () => props.data,
22
- }
23
- dynamicComponent.value = defineAsyncComponent(() => Promise.resolve(componentDefinition))
24
- }
25
-
26
- watch(() => [props.templateStr, props.data], () => {
27
- createComponent()
28
- }, { immediate: true })
33
+ })
34
+ })
29
35
  </script>
30
36
 
31
37
  <template>
32
- <component :is="dynamicComponent" />
38
+ <div v-if="dynamicComponent">
39
+ <component :is="dynamicComponent" />
40
+ </div>
33
41
  </template>
@@ -30,6 +30,6 @@ import valaxyLogoPng from '../assets/images/valaxy-logo.png'
30
30
  height: 150%;
31
31
  background-image: linear-gradient(-45deg, rgb(108, 34, 236) 50%, rgba(59,130,246,var(--un-to-opacity, 1)) 50%);
32
32
  opacity: 0.3;
33
- transition: opacity 0.2s;
33
+ transition: opacity var(--va-transition-duration-fast);
34
34
  }
35
35
  </style>
@@ -21,7 +21,7 @@ withDefaults(defineProps<{
21
21
  position: fixed;
22
22
  inset: 0;
23
23
  z-index: calc(var(--va-z-overlay) - 1);
24
- transition: opacity 0.4s;
24
+ transition: opacity var(--va-transition-duration-moderate);
25
25
 
26
26
  &.fade-enter-from,
27
27
  &.fade-leave-to {
@@ -5,7 +5,7 @@ export function useCodeGroups() {
5
5
  if (import.meta.env.DEV) {
6
6
  onContentUpdated(() => {
7
7
  document.querySelectorAll('.vp-code-group > .blocks').forEach((el) => {
8
- Array.from(el.children).forEach((child) => {
8
+ [...el.children].forEach((child) => {
9
9
  child.classList.remove('active')
10
10
  })
11
11
  el.children[0].classList.add('active')
@@ -21,7 +21,7 @@ export function useCodeGroups() {
21
21
  if (!group)
22
22
  return
23
23
 
24
- const i = Array.from(group.querySelectorAll('input')).indexOf(el)
24
+ const i = [...group.querySelectorAll('input')].indexOf(el)
25
25
  if (i < 0)
26
26
  return
27
27
 
@@ -29,7 +29,7 @@ export function useCodeGroups() {
29
29
  if (!blocks)
30
30
  return
31
31
 
32
- const current = Array.from(blocks.children).find(child =>
32
+ const current = [...blocks.children].find(child =>
33
33
  child.classList.contains('active'),
34
34
  )
35
35
  if (!current)
@@ -1,8 +1,29 @@
1
+ import type { ComputedRef, MaybeRef } from 'vue'
1
2
  import type { CollectionConfig } from '../types'
2
3
  import collections from '#valaxy/blog/collections'
3
4
 
4
- import { computed } from 'vue'
5
+ import { computed, unref } from 'vue'
5
6
  import { useRoute } from 'vue-router'
7
+ import { usePageList } from './post'
8
+
9
+ export interface PostCollectionInfo {
10
+ collection: CollectionConfig
11
+ itemIndex: number
12
+ }
13
+
14
+ /**
15
+ * Resolve the href and external status for a collection item.
16
+ * `link` takes precedence over `key`. Internal links start with `/`.
17
+ */
18
+ export function resolveCollectionItemHref(collectionKey: string, item: { key?: string, link?: string }): { href: string, isExternal: boolean } {
19
+ if (item.link) {
20
+ const isExternal = /^https?:\/\//.test(item.link)
21
+ return { href: item.link, isExternal }
22
+ }
23
+ if (item.key)
24
+ return { href: `/collections/${collectionKey}/${item.key}`, isExternal: false }
25
+ return { href: '', isExternal: false }
26
+ }
6
27
 
7
28
  /**
8
29
  * Composable for Collections
@@ -11,7 +32,7 @@ import { useRoute } from 'vue-router'
11
32
  */
12
33
  export function useCollections() {
13
34
  return {
14
- collections: computed(() => collections),
35
+ collections: computed<CollectionConfig[]>(() => collections),
15
36
  }
16
37
  }
17
38
 
@@ -35,3 +56,81 @@ export function useCollection() {
35
56
  collection,
36
57
  }
37
58
  }
59
+
60
+ /**
61
+ * Get posts belonging to a specific collection
62
+ * Sorted by the order defined in collection.items
63
+ */
64
+ export function useCollectionPosts(collectionKey: MaybeRef<string>) {
65
+ const pageList = usePageList()
66
+ const { collections } = useCollections()
67
+
68
+ return computed(() => {
69
+ const key = unref(collectionKey)
70
+ if (!key)
71
+ return []
72
+
73
+ const prefix = `/collections/${key}/`
74
+ const pages = pageList.value.filter(
75
+ p => p.path?.startsWith(prefix) && p.path !== prefix && !p.path.endsWith('/'),
76
+ )
77
+
78
+ // Sort by collection items order if available
79
+ const col = collections.value.find(c => c.key === key)
80
+ if (col?.items) {
81
+ const orderMap = new Map(
82
+ col.items
83
+ .filter(item => item.key && !item.link)
84
+ .map((item, idx) => [item.key, idx]),
85
+ )
86
+ return pages.toSorted((a, b) => {
87
+ const aSlug = a.path?.split('/').pop() || ''
88
+ const bSlug = b.path?.split('/').pop() || ''
89
+ const aIdx = orderMap.get(aSlug) ?? Infinity
90
+ const bIdx = orderMap.get(bSlug) ?? Infinity
91
+ if (aIdx === bIdx)
92
+ return 0
93
+ return aIdx - bIdx
94
+ })
95
+ }
96
+
97
+ return pages
98
+ })
99
+ }
100
+
101
+ /**
102
+ * Find which collection(s) a given post path belongs to.
103
+ * Checks both key-based items (`/collections/{collKey}/{item.key}`)
104
+ * and internal link items (`item.link`).
105
+ */
106
+ export function usePostCollections(postPath: MaybeRef<string>): ComputedRef<PostCollectionInfo[]> {
107
+ return computed(() => {
108
+ const path = stripTrailingSlash(unref(postPath))
109
+ if (!path)
110
+ return []
111
+
112
+ const result: PostCollectionInfo[] = []
113
+ for (const col of collections) {
114
+ if (!col.items || !col.key)
115
+ continue
116
+ for (let i = 0; i < col.items.length; i++) {
117
+ const item = col.items[i]
118
+ // Match key-based items: /collections/{collKey}/{item.key}
119
+ if (item.key && stripTrailingSlash(`/collections/${col.key}/${item.key}`) === path) {
120
+ result.push({ collection: col, itemIndex: i })
121
+ break
122
+ }
123
+ // Match internal link items (non-http)
124
+ if (item.link && !/^https?:\/\//.test(item.link) && stripTrailingSlash(item.link) === path) {
125
+ result.push({ collection: col, itemIndex: i })
126
+ break
127
+ }
128
+ }
129
+ }
130
+ return result
131
+ })
132
+ }
133
+
134
+ function stripTrailingSlash(path: string): string {
135
+ return path.length > 1 && path.endsWith('/') ? path.slice(0, -1) : path
136
+ }
@@ -2,7 +2,7 @@ import type { PostFrontMatter } from '../../types'
2
2
  import type { ValaxyData } from '../app/data'
3
3
 
4
4
  import { isClient } from '@vueuse/core'
5
- import { computed, inject } from 'vue'
5
+ import { computed, hasInjectionContext, inject } from 'vue'
6
6
  import { useRoute } from 'vue-router'
7
7
  import { dataSymbol, useSiteConfig } from '../config'
8
8
 
@@ -73,10 +73,11 @@ export function useEncryptedPhotos() {
73
73
  * inject pageData
74
74
  */
75
75
  export function useData<FM = Record<string, any>>(): ValaxyData<FM> {
76
- const data = inject(dataSymbol, {} as any)
77
- if (!data) {
78
- throw new Error('Valaxy data not properly injected in app')
79
- }
76
+ if (!hasInjectionContext())
77
+ throw new Error('[Valaxy] useData() must be called inside setup() or a component lifecycle')
78
+ const data = inject<ValaxyData<FM>>(dataSymbol)
79
+ if (!data)
80
+ throw new Error('[Valaxy] data not properly injected in app')
80
81
  return data
81
82
  }
82
83
 
@@ -61,7 +61,7 @@ export function useValaxyDark(options: {
61
61
  transition.ready.then(() => {
62
62
  document.documentElement.animate(
63
63
  {
64
- clipPath: isDark.value ? [...clipPath].reverse() : clipPath,
64
+ clipPath: isDark.value ? clipPath.toReversed() : clipPath,
65
65
  },
66
66
  {
67
67
  duration: options.duration ?? 200,
@@ -46,7 +46,7 @@ export function useCollapseCode() {
46
46
  // determine whether to add folded class name
47
47
  onMounted(() => {
48
48
  const els = document.querySelectorAll('div[class*="language-"]')
49
- for (const el of Array.from(els)) {
49
+ for (const el of [...els]) {
50
50
  const elHeight = getHeightViaClone(el as HTMLElement)
51
51
  if (elHeight > codeHeightLimit)
52
52
  el.classList.add('folded')
@@ -0,0 +1,85 @@
1
+ import { isClient, useClipboard } from '@vueuse/core'
2
+ import { computed, ref, watchEffect } from 'vue'
3
+ import { useRoute } from 'vue-router'
4
+
5
+ /**
6
+ * Composable for copying raw Markdown content of the current post.
7
+ * Requires `siteConfig.llms.files: true` to have .md files available at build output.
8
+ *
9
+ * The `available` ref is initially `false` and becomes `true` after a HEAD request
10
+ * confirms the `.md` file exists. This allows themes to conditionally render
11
+ * the copy button only when the llms feature is enabled.
12
+ *
13
+ * @example
14
+ * ```vue
15
+ * <script setup>
16
+ * import { useCopyMarkdown } from 'valaxy'
17
+ * const { copy, copied, loading, available, error } = useCopyMarkdown()
18
+ * </script>
19
+ * <template>
20
+ * <button v-if="available" @click="copy" :disabled="loading">
21
+ * {{ copied ? 'Copied!' : 'Copy Markdown' }}
22
+ * </button>
23
+ * <span v-if="error" class="text-red">{{ error }}</span>
24
+ * </template>
25
+ * ```
26
+ */
27
+ export function useCopyMarkdown() {
28
+ const route = useRoute()
29
+ const copied = ref(false)
30
+ const loading = ref(false)
31
+ const available = ref(false)
32
+ const error = ref<string | null>(null)
33
+ const { copy: copyToClipboard } = useClipboard({ legacy: true })
34
+
35
+ const mdUrl = computed(() => {
36
+ const p = route.path !== '/' && route.path.endsWith('/')
37
+ ? route.path.slice(0, -1)
38
+ : route.path
39
+ return `${p}.md`
40
+ })
41
+
42
+ // Probe the .md file to detect availability (siteConfig.llms.files enabled at build time)
43
+ if (isClient) {
44
+ watchEffect(() => {
45
+ available.value = false
46
+ error.value = null
47
+ fetch(mdUrl.value, { method: 'HEAD' })
48
+ .then((res) => { available.value = res.ok })
49
+ .catch(() => { available.value = false })
50
+ })
51
+ }
52
+
53
+ async function copy() {
54
+ if (loading.value)
55
+ return
56
+
57
+ error.value = null
58
+ loading.value = true
59
+ try {
60
+ const res = await fetch(mdUrl.value)
61
+ if (!res.ok)
62
+ throw new Error(`Failed to fetch ${mdUrl.value}: ${res.status}`)
63
+
64
+ const text = await res.text()
65
+ await copyToClipboard(text)
66
+ copied.value = true
67
+ setTimeout(() => {
68
+ copied.value = false
69
+ }, 2000)
70
+ }
71
+ catch (err) {
72
+ const msg = err instanceof Error ? err.message : 'Unknown error'
73
+ error.value = msg
74
+ console.error('[valaxy] Failed to copy markdown:', err)
75
+ setTimeout(() => {
76
+ error.value = null
77
+ }, 3000)
78
+ }
79
+ finally {
80
+ loading.value = false
81
+ }
82
+ }
83
+
84
+ return { copy, copied, loading, mdUrl, available, error }
85
+ }
@@ -1,3 +1,4 @@
1
1
  export * from './collapse-code'
2
2
  export * from './copy-code'
3
+ export * from './copy-markdown'
3
4
  export * from './medium-zoom'
@@ -99,7 +99,7 @@ export function useActiveAnchor(
99
99
 
100
100
  // page bottom - highlight last link
101
101
  if (isBottom) {
102
- activateLink(headers[headers.length - 1].link)
102
+ activateLink(headers.at(-1)?.link || null)
103
103
  return
104
104
  }
105
105
 
@@ -1,5 +1,7 @@
1
1
  import type { Post, SiteConfig } from 'valaxy'
2
2
  import type { ComputedRef } from 'vue'
3
+ import type { CollectionConfig } from '../../types'
4
+ import collections from '#valaxy/blog/collections'
3
5
  import { orderByMeta, useSiteConfig } from 'valaxy'
4
6
  import { computed } from 'vue'
5
7
  import { useI18n } from 'vue-i18n'
@@ -31,8 +33,13 @@ export function usePageList() {
31
33
  .filter(i => i.meta!.frontmatter)
32
34
  .filter(i => i.path && !excludePages.includes(i.path))
33
35
  .map((i) => {
34
- return Object.assign({ path: i.path, excerpt: i.meta!.excerpt }, i.meta!.frontmatter || {}) as Post
36
+ return { path: i.path, excerpt: i.meta!.excerpt, ...i.meta!.frontmatter || {} } as Post
35
37
  })
38
+
39
+ // Sort by `top` so pages with higher `top` values appear first.
40
+ // This ensures frontmatter `top` affects ordering in sidebars and categories.
41
+ routes.sort((a, b) => (b.top || 0) - (a.top || 0))
42
+
36
43
  return routes
37
44
  })
38
45
  }
@@ -67,7 +74,83 @@ export function filterAndSortPosts(
67
74
  const topPosts = sortBySiteConfigOrderBy(routes.filter(i => i.top)).sort((a, b) => b.top! - a.top!)
68
75
  const otherPosts = sortBySiteConfigOrderBy(routes.filter(i => !i.top))
69
76
 
70
- return topPosts.concat(otherPosts)
77
+ return [...topPosts, ...otherPosts]
78
+ }
79
+
80
+ /**
81
+ * Merge collapsed collections into the post list.
82
+ * For collapsed collections, add a single synthetic entry representing the
83
+ * collection, using the latest article's date for sorting.
84
+ */
85
+ export function mergeCollapsedCollections(
86
+ posts: Post[],
87
+ allPages: Post[],
88
+ collectionConfigs: CollectionConfig[],
89
+ siteConfig: SiteConfig,
90
+ ): Post[] {
91
+ const collapsedCollections = collectionConfigs.filter(c => c.collapse !== false)
92
+
93
+ if (collapsedCollections.length === 0)
94
+ return posts
95
+
96
+ // Pre-index collection pages by key in a single pass
97
+ const collectionPagesMap = new Map<string, Post[]>()
98
+ for (const p of allPages) {
99
+ if (!p.path?.startsWith('/collections/') || !p.date)
100
+ continue
101
+ const parts = p.path.split('/')
102
+ // path format: /collections/{key}/{slug}
103
+ if (parts.length < 4 || !parts[3] || p.path.endsWith('/'))
104
+ continue
105
+ const colKey = parts[2]
106
+ if (!collectionPagesMap.has(colKey))
107
+ collectionPagesMap.set(colKey, [])
108
+ collectionPagesMap.get(colKey)!.push(p)
109
+ }
110
+
111
+ const collectionEntries: Post[] = []
112
+ for (const col of collapsedCollections) {
113
+ if (!col.key)
114
+ continue
115
+
116
+ const colPages = collectionPagesMap.get(col.key) || []
117
+
118
+ if (colPages.length === 0)
119
+ continue
120
+
121
+ // Find latest updated article for sort position
122
+ const latest = colPages.reduce((a, b) => {
123
+ const aTime = new Date(a.updated || a.date || '').getTime()
124
+ const bTime = new Date(b.updated || b.date || '').getTime()
125
+ return bTime > aTime ? b : a
126
+ })
127
+
128
+ // Create synthetic post entry representing the collapsed collection
129
+ collectionEntries.push({
130
+ title: col.title || latest.title,
131
+ path: `/collections/${col.key}/`,
132
+ cover: col.cover || latest.cover,
133
+ date: latest.date,
134
+ updated: latest.updated,
135
+ categories: col.categories || latest.categories,
136
+ tags: col.tags || latest.tags,
137
+ _collection: col,
138
+ })
139
+ }
140
+
141
+ if (collectionEntries.length === 0)
142
+ return posts
143
+
144
+ function sortBySiteConfigOrderBy(entries: Post[]) {
145
+ return orderByMeta(entries, siteConfig.orderBy)
146
+ }
147
+
148
+ // Re-sort only when collection entries were actually added
149
+ // to preserve the topPosts-first ordering from filterAndSortPosts
150
+ const topPosts = posts.filter(i => i.top)
151
+ const otherPosts = posts.filter(i => !i.top)
152
+ const mergedOther = sortBySiteConfigOrderBy([...otherPosts, ...collectionEntries])
153
+ return [...topPosts, ...mergedOther]
71
154
  }
72
155
 
73
156
  /**
@@ -83,3 +166,22 @@ export function usePostList(params: {
83
166
  return filterAndSortPosts(pageList.value, siteConfig.value, params)
84
167
  })
85
168
  }
169
+
170
+ /**
171
+ * Get post list merged with collapsed collection entries.
172
+ * Collapsed collections are represented by a single synthetic entry
173
+ * (card) that is appended and re-sorted with the existing posts.
174
+ *
175
+ * @experimental
176
+ */
177
+ export function usePostListWithCollections(params: {
178
+ type?: string
179
+ } = {}) {
180
+ const siteConfig = useSiteConfig()
181
+ const pageList = usePageList()
182
+
183
+ return computed(() => {
184
+ const posts = filterAndSortPosts(pageList.value, siteConfig.value, params)
185
+ return mergeCollapsedCollections(posts, pageList.value, collections as CollectionConfig[], siteConfig.value)
186
+ })
187
+ }
package/client/config.ts CHANGED
@@ -6,7 +6,7 @@ import type { ComputedRef, InjectionKey } from 'vue'
6
6
  // https://github.com/microsoft/TypeScript/issues/42873
7
7
  import type { DefaultTheme, ValaxyConfig } from '../types'
8
8
  import type { ValaxyData } from './app/data'
9
- import { computed, inject, readonly, shallowRef } from 'vue'
9
+ import { computed, hasInjectionContext, inject, readonly, shallowRef } from 'vue'
10
10
 
11
11
  // @ts-expect-error virtual module @valaxyjs/config
12
12
  import valaxyConfig from '/@valaxyjs/config'
@@ -62,10 +62,12 @@ export function initContext() {
62
62
  * @public
63
63
  */
64
64
  export function useValaxyConfig<ThemeConfig = DefaultTheme.Config>() {
65
+ if (!hasInjectionContext())
66
+ throw new Error('[Valaxy] useValaxyConfig() must be called inside setup() or a component lifecycle')
65
67
  const config = inject<ComputedRef<ValaxyConfig<ThemeConfig>>>(valaxyConfigSymbol)
66
68
  if (!config)
67
69
  throw new Error('[Valaxy] site config not properly injected in app')
68
- return config!
70
+ return config
69
71
  }
70
72
 
71
73
  /**
@@ -0,0 +1,25 @@
1
+ import { createHead } from '@unhead/vue/server'
2
+ import { renderToString } from 'vue/server-renderer'
3
+ import { createValaxyApp, routesWithLayout } from './main'
4
+
5
+ export async function render(routePath: string) {
6
+ const ctx = createValaxyApp({ routePath, createHead })
7
+ const { app, router, head } = ctx
8
+
9
+ await router.push(routePath)
10
+ await router.isReady()
11
+
12
+ const ssrCtx: Record<string, any> = {}
13
+ const html = await renderToString(app, ssrCtx)
14
+ await ctx.triggerOnSSRAppRendered()
15
+
16
+ return {
17
+ html,
18
+ head,
19
+ modules: ssrCtx.modules as Set<string> | undefined,
20
+ teleports: ssrCtx.teleports as Record<string, string> | undefined,
21
+ initialState: ctx.initialState,
22
+ }
23
+ }
24
+
25
+ export { routesWithLayout as routes }
@@ -4,7 +4,7 @@ Vue components in this dir are used as layouts.
4
4
 
5
5
  By default, `default.vue` will be used unless an alternative is specified in the route meta.
6
6
 
7
- With [unplugin-vue-router](https://github.com/posva/unplugin-vue-router) and [`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts), you can specify the layout in the page's SFCs like this:
7
+ With [vue-router](https://router.vuejs.org/) file-based routing and [`vite-plugin-vue-layouts-next`](https://github.com/loicduong/vite-plugin-vue-layouts-next), you can specify the layout in the page's SFCs like this:
8
8
 
9
9
  ```html
10
10
  <route lang="yaml">
@@ -50,6 +50,14 @@ post:
50
50
  related_posts: Related posts
51
51
  view_link: View link
52
52
  read_more: READ MORE
53
+ copy_markdown: Copy Markdown
54
+ copy_markdown_link: Copy Markdown Link
55
+ copy_page: Copy page
56
+ copied_markdown: Copied!
57
+ view_as_markdown: View as Markdown
58
+ open_in_github: Open in GitHub
59
+ open_in_chatgpt: Open in ChatGPT
60
+ open_in_claude: Open in Claude
53
61
  cover: Cover
54
62
  time_warning: This article was last updated {ago}. The information described in this article may have changed.
55
63
  copyright:
@@ -68,6 +76,10 @@ footer:
68
76
  total_views: Total Views
69
77
  total_visitors: Total Visitors
70
78
 
79
+ collection:
80
+ badge: Collection
81
+ empty: No items yet
82
+
71
83
  counter:
72
84
  archives: No posts | 1 post | {count} posts
73
85
  categories: No categories | 1 category | {count} categories
@@ -50,6 +50,14 @@ post:
50
50
  related_posts: 相关文章
51
51
  view_link: 查看链接
52
52
  read_more: 阅读更多
53
+ copy_markdown: 复制 Markdown
54
+ copy_markdown_link: 复制 Markdown 链接
55
+ copy_page: 复制页面
56
+ copied_markdown: 已复制!
57
+ view_as_markdown: 查看 Markdown
58
+ open_in_github: 在 GitHub 中打开
59
+ open_in_chatgpt: 在 ChatGPT 中打开
60
+ open_in_claude: 在 Claude 中打开
53
61
  cover: 封面
54
62
  time_warning: '本文最后更新于 {ago},文中所描述的信息可能已发生改变。'
55
63
  copyright:
@@ -67,6 +75,10 @@ footer:
67
75
  total_views: 总访问量
68
76
  total_visitors: 总访客量
69
77
 
78
+ collection:
79
+ badge: 合集
80
+ empty: 暂无内容
81
+
70
82
  counter:
71
83
  archives: 暂无文章 | 共计 1 篇文章 | 共计 {count} 篇文章
72
84
  categories: 暂无分类 | 共计 1 个分类 | 共计 {count} 个分类