Show page contributors from git history, linked to Gitea
This commit is contained in:
@@ -82,6 +82,23 @@ const editLink = computed(() => {
|
|||||||
].filter(Boolean).join('/')
|
].filter(Boolean).join('/')
|
||||||
})
|
})
|
||||||
|
|
||||||
|
const contributors = computed(() => (page.value as unknown as Record<string, unknown>)?.contributors as string[] | undefined)
|
||||||
|
|
||||||
|
const historyLink = computed(() => {
|
||||||
|
if (!giteaUrl.value) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
return [
|
||||||
|
giteaUrl.value,
|
||||||
|
'commits',
|
||||||
|
'branch',
|
||||||
|
'main',
|
||||||
|
'content',
|
||||||
|
`${page.value?.stem}.${page.value?.extension}`,
|
||||||
|
].filter(Boolean).join('/')
|
||||||
|
})
|
||||||
|
|
||||||
// Add the page path to the prerender list
|
// Add the page path to the prerender list
|
||||||
addPrerenderPath(`/raw${route.path}.md`)
|
addPrerenderPath(`/raw${route.path}.md`)
|
||||||
</script>
|
</script>
|
||||||
@@ -149,6 +166,25 @@ addPrerenderPath(`/raw${route.path}.md`)
|
|||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</USeparator>
|
</USeparator>
|
||||||
|
<div
|
||||||
|
v-if="contributors?.length"
|
||||||
|
class="flex items-center gap-2 text-sm text-muted"
|
||||||
|
>
|
||||||
|
<UIcon
|
||||||
|
name="i-lucide-users"
|
||||||
|
class="size-4 shrink-0"
|
||||||
|
/>
|
||||||
|
<span>{{ locale === 'fr' ? (contributors.length > 1 ? 'Contributeurs' : 'Contributeur') : (contributors.length > 1 ? 'Contributors' : 'Contributor') }}:</span>
|
||||||
|
<ULink
|
||||||
|
v-if="historyLink"
|
||||||
|
:to="historyLink"
|
||||||
|
target="_blank"
|
||||||
|
class="text-highlighted hover:underline"
|
||||||
|
>
|
||||||
|
{{ contributors.join(', ') }}
|
||||||
|
</ULink>
|
||||||
|
<span v-else>{{ contributors.join(', ') }}</span>
|
||||||
|
</div>
|
||||||
<UContentSurround :surround="surround" />
|
<UContentSurround :surround="surround" />
|
||||||
</UPageBody>
|
</UPageBody>
|
||||||
|
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ const createDocsSchema = () => z.object({
|
|||||||
hideHeader: z.boolean().optional(),
|
hideHeader: z.boolean().optional(),
|
||||||
hideCopyPage: z.boolean().optional(),
|
hideCopyPage: z.boolean().optional(),
|
||||||
hideToc: z.boolean().optional(),
|
hideToc: z.boolean().optional(),
|
||||||
|
contributors: z.array(z.string()).optional(),
|
||||||
})
|
})
|
||||||
|
|
||||||
let collections: Record<string, DefinedCollection>
|
let collections: Record<string, DefinedCollection>
|
||||||
|
|||||||
@@ -1,8 +1,27 @@
|
|||||||
import { fileURLToPath } from 'node:url'
|
import { fileURLToPath } from 'node:url'
|
||||||
|
import { execFileSync } from 'node:child_process'
|
||||||
import { createResolver } from '@nuxt/kit'
|
import { createResolver } from '@nuxt/kit'
|
||||||
|
|
||||||
const { resolve } = createResolver(import.meta.url)
|
const { resolve } = createResolver(import.meta.url)
|
||||||
|
|
||||||
|
// Contributors per page: read straight from git history rather than an
|
||||||
|
// API, so it needs no token and no network call, but the CI checkout must
|
||||||
|
// fetch full history (not a shallow clone) or every file will only show
|
||||||
|
// its most recent author.
|
||||||
|
function getContributors(absoluteFilePath: string): string[] {
|
||||||
|
try {
|
||||||
|
const output = execFileSync(
|
||||||
|
'git',
|
||||||
|
['log', '--format=%an', '--follow', '--', absoluteFilePath],
|
||||||
|
{ cwd: resolve('.'), encoding: 'utf8' },
|
||||||
|
)
|
||||||
|
return [...new Set(output.split('\n').map(line => line.trim()).filter(Boolean))]
|
||||||
|
}
|
||||||
|
catch {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// @nuxt/image resolves its file-storage root differently in `nuxi dev`
|
// @nuxt/image resolves its file-storage root differently in `nuxi dev`
|
||||||
// (needs an absolute path) vs a production build (needs the plain
|
// (needs an absolute path) vs a production build (needs the plain
|
||||||
// `public/` string — an absolute path there breaks the SVG content-type
|
// `public/` string — an absolute path there breaks the SVG content-type
|
||||||
@@ -60,6 +79,12 @@ export default defineNuxtConfig({
|
|||||||
name: 'Français',
|
name: 'Français',
|
||||||
}],
|
}],
|
||||||
},
|
},
|
||||||
|
hooks: {
|
||||||
|
'content:file:afterParse': (ctx: { file: { path?: string }, content: Record<string, unknown> }) => {
|
||||||
|
if (!ctx.file.path?.endsWith('.md')) return
|
||||||
|
ctx.content.contributors = getContributors(ctx.file.path)
|
||||||
|
},
|
||||||
|
},
|
||||||
// Permanent (301) redirects from the old site's French URLs (root-level,
|
// Permanent (301) redirects from the old site's French URLs (root-level,
|
||||||
// no locale prefix) to their new /fr/... equivalents, so bookmarks and
|
// no locale prefix) to their new /fr/... equivalents, so bookmarks and
|
||||||
// search engine rankings carry over.
|
// search engine rankings carry over.
|
||||||
|
|||||||
Reference in New Issue
Block a user