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.
- package/client/components/AppLink.vue +4 -0
- package/client/components/ClientOnly.ts +12 -0
- package/client/components/ValaxyDynamicComponent.vue +22 -14
- package/client/components/ValaxyLogo.vue +1 -1
- package/client/components/ValaxyOverlay.vue +1 -1
- package/client/composables/codeGroups.ts +3 -3
- package/client/composables/collections.ts +101 -2
- package/client/composables/common.ts +6 -5
- package/client/composables/dark.ts +1 -1
- package/client/composables/features/collapse-code.ts +1 -1
- package/client/composables/features/copy-markdown.ts +85 -0
- package/client/composables/features/index.ts +1 -0
- package/client/composables/outline/anchor.ts +1 -1
- package/client/composables/post/index.ts +104 -2
- package/client/config.ts +4 -2
- package/client/entry-ssr.ts +25 -0
- package/client/layouts/README.md +1 -1
- package/client/locales/en.yml +12 -0
- package/client/locales/zh-CN.yml +12 -0
- package/client/main.ts +122 -24
- package/client/modules/components.ts +2 -2
- package/client/modules/floating-vue.ts +2 -3
- package/client/modules/valaxy.ts +2 -3
- package/client/setup/main.ts +2 -3
- package/client/setups.ts +24 -3
- package/client/styles/common/code.scss +8 -8
- package/client/styles/common/hamburger.scss +2 -2
- package/client/styles/common/markdown.scss +2 -2
- package/client/styles/common/transition.scss +2 -2
- package/client/styles/components/code-group.scss +2 -2
- package/client/styles/components/custom-block.scss +1 -1
- package/client/styles/css-vars.scss +7 -6
- package/client/styles/vars.scss +4 -3
- package/client/tsconfig.json +1 -1
- package/client/types/collection.ts +27 -0
- package/client/types/index.ts +2 -2
- package/client/utils/time.ts +1 -1
- package/dist/node/cli/index.mjs +14 -9
- package/dist/node/index.d.mts +216 -11
- package/dist/node/index.mjs +19 -10
- package/dist/shared/{valaxy.DpV6HHc6.mjs → valaxy.DXqMwOZX.mjs} +4034 -2514
- package/dist/shared/{valaxy._i636HSR.d.mts → valaxy.JIuR8V4d.d.mts} +111 -2
- package/dist/types/index.d.mts +2 -2
- package/index.d.ts +1 -0
- package/package.json +36 -36
- package/shared/node/i18n.ts +15 -1
- package/types/config.ts +74 -0
- package/types/frontmatter/page.ts +6 -1
- package/types/posts.ts +9 -1
|
@@ -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,
|
|
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
|
-
|
|
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
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
<
|
|
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
|
|
33
|
+
transition: opacity var(--va-transition-duration-fast);
|
|
34
34
|
}
|
|
35
35
|
</style>
|
|
@@ -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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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 ?
|
|
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
|
|
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,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
|
|
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
|
|
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 }
|
package/client/layouts/README.md
CHANGED
|
@@ -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 [
|
|
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">
|
package/client/locales/en.yml
CHANGED
|
@@ -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
|
package/client/locales/zh-CN.yml
CHANGED
|
@@ -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} 个分类
|