mirror of
https://github.com/hanzo-docs/docs.git
synced 2026-10-26 23:49:51 +00:00
MDX: Support last modified timestamp for Git
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
'next-docs-mdx': minor
|
||||
---
|
||||
|
||||
**Support last modified timestamp for Git**
|
||||
@@ -97,9 +97,7 @@ export function Previews(): JSX.Element {
|
||||
>
|
||||
@joulev
|
||||
</a>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
Moderator at Next.js Discord
|
||||
</p>
|
||||
<p className="text-xs text-muted-foreground">Next.js Expert</p>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ExternalLinkIcon } from 'lucide-react';
|
||||
import type { Metadata } from 'next';
|
||||
import { Card, Cards } from 'next-docs-ui/mdx/card';
|
||||
import { DocsPage, DocsBody } from 'next-docs-ui/page';
|
||||
import { getGithubLastEdit } from 'next-docs-zeta/server';
|
||||
import { notFound } from 'next/navigation';
|
||||
import { utils, type Page } from '@/utils/source';
|
||||
import { createMetadata } from '@/utils/metadata';
|
||||
@@ -12,11 +11,7 @@ interface Param {
|
||||
slug: string[];
|
||||
}
|
||||
|
||||
export default async function Page({
|
||||
params,
|
||||
}: {
|
||||
params: Param;
|
||||
}): Promise<JSX.Element> {
|
||||
export default function Page({ params }: { params: Param }): JSX.Element {
|
||||
const page = utils.getPage(params.slug);
|
||||
|
||||
if (!page) {
|
||||
@@ -24,21 +19,12 @@ export default async function Page({
|
||||
}
|
||||
|
||||
const path = `apps/docs/content/docs/${page.file.path}`;
|
||||
const time = await getGithubLastEdit({
|
||||
owner: 'fuma-nama',
|
||||
repo: 'next-docs',
|
||||
path,
|
||||
token: process.env.GIT_TOKEN
|
||||
? `Bearer ${process.env.GIT_TOKEN}`
|
||||
: undefined,
|
||||
});
|
||||
|
||||
const preview = page.data.preview?.trim();
|
||||
|
||||
return (
|
||||
<DocsPage
|
||||
toc={page.data.exports.toc}
|
||||
lastUpdate={time}
|
||||
lastUpdate={page.data.exports.lastModified}
|
||||
tableOfContent={{
|
||||
footer: (
|
||||
<a
|
||||
@@ -125,9 +111,7 @@ export function generateMetadata({ params }: { params: Param }): Metadata {
|
||||
}
|
||||
|
||||
export function generateStaticParams(): Param[] {
|
||||
return (
|
||||
utils.getPages()?.map<Param>((page) => ({
|
||||
slug: page.slugs,
|
||||
})) ?? []
|
||||
);
|
||||
return utils.getPages().map<Param>((page) => ({
|
||||
slug: page.slugs,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -127,7 +127,7 @@ export async function generateStaticParams(): Promise<
|
||||
{ lang: string; slug: string[] }[]
|
||||
> {
|
||||
return languages.flatMap((lang) =>
|
||||
getPages(lang)!.map((docs) => ({
|
||||
getPages(lang).map((docs) => ({
|
||||
slug: docs.slug.split('/'),
|
||||
lang: lang,
|
||||
})),
|
||||
|
||||
@@ -22,6 +22,7 @@ const config = {
|
||||
|
||||
const withNextDocs = createNextDocs({
|
||||
mdxOptions: {
|
||||
lastModifiedTime: 'git',
|
||||
remarkPlugins: [
|
||||
remarkMath,
|
||||
remarkDynamicContent,
|
||||
|
||||
@@ -26,7 +26,7 @@ export default async function Page({
|
||||
}
|
||||
|
||||
export async function generateStaticParams(): Promise<{ slug: string[] }[]> {
|
||||
return getPages()!.map((page) => ({
|
||||
return getPages().map((page) => ({
|
||||
slug: page.slugs,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getPages } from '@/app/source';
|
||||
import { createSearchAPI } from 'next-docs-zeta/search/server';
|
||||
|
||||
export const { GET } = createSearchAPI('advanced', {
|
||||
indexes: getPages()!.map((page) => ({
|
||||
indexes: getPages().map((page) => ({
|
||||
title: page.data.title,
|
||||
structuredData: page.data.exports.structuredData,
|
||||
id: page.url,
|
||||
|
||||
@@ -27,7 +27,7 @@ export default async function Page({
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return getPages()!.map((page) => ({
|
||||
return getPages().map((page) => ({
|
||||
slug: page.slugs,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getPages } from '@/app/source';
|
||||
import { createSearchAPI } from 'next-docs-zeta/search/server';
|
||||
|
||||
export const { GET } = createSearchAPI('simple', {
|
||||
indexes: getPages()!.map((page) => ({
|
||||
indexes: getPages().map((page) => ({
|
||||
title: page.data.title,
|
||||
content: page.data.body.raw,
|
||||
url: page.url,
|
||||
|
||||
@@ -26,11 +26,9 @@ export default async function Page({
|
||||
}
|
||||
|
||||
export async function generateStaticParams(): Promise<{ slug: string[] }[]> {
|
||||
return (
|
||||
getPages()?.map((page) => ({
|
||||
slug: page.slugs,
|
||||
})) ?? []
|
||||
);
|
||||
return getPages().map((page) => ({
|
||||
slug: page.slugs,
|
||||
}));
|
||||
}
|
||||
|
||||
export function generateMetadata({ params }: { params: { slug?: string[] } }) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getPages } from '@/app/source';
|
||||
import { createSearchAPI } from 'next-docs-zeta/search/server';
|
||||
|
||||
export const { GET } = createSearchAPI('advanced', {
|
||||
indexes: getPages()!.map((page) => ({
|
||||
indexes: getPages().map((page) => ({
|
||||
id: page.data._id,
|
||||
title: page.data.title,
|
||||
url: page.url,
|
||||
|
||||
@@ -26,11 +26,9 @@ export default async function Page({
|
||||
}
|
||||
|
||||
export async function generateStaticParams(): Promise<{ slug: string[] }[]> {
|
||||
return (
|
||||
getPages()?.map((page) => ({
|
||||
slug: page.slugs,
|
||||
})) ?? []
|
||||
);
|
||||
return getPages().map((page) => ({
|
||||
slug: page.slugs,
|
||||
}));
|
||||
}
|
||||
|
||||
export function generateMetadata({ params }: { params: { slug?: string[] } }) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { getPages } from '@/app/source';
|
||||
import { createSearchAPI } from 'next-docs-zeta/search/server';
|
||||
|
||||
export const { GET } = createSearchAPI('advanced', {
|
||||
indexes: getPages()!.map((page) => ({
|
||||
indexes: getPages().map((page) => ({
|
||||
title: page.data.title,
|
||||
structuredData: page.data.exports.structuredData,
|
||||
id: page.url,
|
||||
|
||||
@@ -27,7 +27,7 @@ export default async function Page({
|
||||
}
|
||||
|
||||
export async function generateStaticParams() {
|
||||
return getPages()!.map((page) => ({
|
||||
return getPages().map((page) => ({
|
||||
slug: page.slugs,
|
||||
}));
|
||||
}
|
||||
|
||||
@@ -50,14 +50,11 @@ export interface LoaderOutput<Config extends LoaderConfig> {
|
||||
files: Node<Config['source']['metaData'], Config['source']['pageData']>[];
|
||||
|
||||
/**
|
||||
* Get list of pages from language
|
||||
* Get list of pages from language, empty if language hasn't specified
|
||||
*
|
||||
* @param language - If empty, the default language will be used
|
||||
*/
|
||||
getPages: (
|
||||
language?: string,
|
||||
) => Page<Config['source']['pageData']>[] | undefined;
|
||||
|
||||
getPages: (language?: string) => Page<Config['source']['pageData']>[];
|
||||
/**
|
||||
* @param language - If empty, the default language will be used
|
||||
*/
|
||||
@@ -155,7 +152,7 @@ function createOutput({
|
||||
pageTree: pageTree as LoaderOutput<LoaderConfig>['pageTree'],
|
||||
files: result.storage.list(),
|
||||
getPages(language = '') {
|
||||
return i18nMap.get(language);
|
||||
return i18nMap.get(language) ?? [];
|
||||
},
|
||||
getPage(slugs_ = [], language = '') {
|
||||
const path = slugs_.join('/');
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
},
|
||||
"dependencies": {
|
||||
"@mdx-js/mdx": "^3.0.0",
|
||||
"cross-spawn": "^7.0.3",
|
||||
"estree-util-value-to-estree": "^3.0.1",
|
||||
"fast-glob": "^3.3.1",
|
||||
"gray-matter": "^4.0.3",
|
||||
@@ -52,6 +53,7 @@
|
||||
"zod": "^3.22.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/cross-spawn": "^6.0.4",
|
||||
"@types/mdast": "^4.0.3",
|
||||
"@types/mdx": "^2.0.8",
|
||||
"@types/react": "18.2.0",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import path from 'node:path';
|
||||
import type { ProcessorOptions } from '@mdx-js/mdx';
|
||||
import type { NextConfig } from 'next';
|
||||
import {
|
||||
rehypeNextDocs,
|
||||
@@ -13,12 +12,10 @@ import type { Configuration } from 'webpack';
|
||||
import { NextDocsWebpackPlugin } from './webpack-plugins/next-docs';
|
||||
import remarkMdxExport from './mdx-plugins/remark-exports';
|
||||
import type { LoaderOptions } from './loader';
|
||||
|
||||
type WithMDX = (config: NextConfig) => NextConfig;
|
||||
type Loader = (options: ProcessorOptions) => WithMDX;
|
||||
import type { Options as MDXLoaderOptions } from './loader-mdx';
|
||||
|
||||
type MDXOptions = Omit<
|
||||
NonNullable<ProcessorOptions>,
|
||||
NonNullable<MDXLoaderOptions>,
|
||||
'rehypePlugins' | 'remarkPlugins'
|
||||
> & {
|
||||
rehypePlugins?: ResolvePlugins;
|
||||
@@ -42,11 +39,6 @@ interface NextDocsMDXOptions {
|
||||
|
||||
mdxOptions?: MDXOptions;
|
||||
|
||||
/**
|
||||
* Custom MDX loader
|
||||
*/
|
||||
loader?: Loader;
|
||||
|
||||
/**
|
||||
* Where the root map.ts should be, relative to cwd
|
||||
*
|
||||
@@ -87,6 +79,7 @@ const createNextDocs =
|
||||
'structuredData',
|
||||
'toc',
|
||||
'frontmatter',
|
||||
'lastModified',
|
||||
...(mdxOptions.valueToExport ?? []),
|
||||
];
|
||||
const _mapPath = path.resolve(cwd, rootMapPath);
|
||||
@@ -136,7 +129,7 @@ const createNextDocs =
|
||||
...mdxOptions,
|
||||
remarkPlugins,
|
||||
rehypePlugins,
|
||||
},
|
||||
} satisfies MDXLoaderOptions,
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -3,8 +3,15 @@ import { createProcessor, type ProcessorOptions } from '@mdx-js/mdx';
|
||||
import type { Processor } from '@mdx-js/mdx/lib/core';
|
||||
import grayMatter from 'gray-matter';
|
||||
import { type LoaderContext } from 'webpack';
|
||||
import { getGitTimestamp } from './utils/git-timestamp';
|
||||
|
||||
type Options = ProcessorOptions;
|
||||
export interface Options extends ProcessorOptions {
|
||||
/**
|
||||
* Fetch last modified time with specified version control
|
||||
* @defaultValue 'none'
|
||||
*/
|
||||
lastModifiedTime?: 'git' | 'none';
|
||||
}
|
||||
|
||||
export interface NextDocsBuildInfo {
|
||||
__next_docs?: {
|
||||
@@ -23,27 +30,30 @@ const cache = new Map<string, Processor>();
|
||||
*
|
||||
* it supports frontmatter by parsing and injecting the data in `vfile.data.frontmatter`
|
||||
*/
|
||||
export default function loader(
|
||||
export default async function loader(
|
||||
this: LoaderContext<Options>,
|
||||
source: string,
|
||||
callback: LoaderContext<Options>['callback'],
|
||||
): void {
|
||||
): Promise<void> {
|
||||
this.cacheable(true);
|
||||
const context = this.context;
|
||||
const filePath = this.resourcePath;
|
||||
const options = this.getOptions();
|
||||
const { content, data: frontmatter } = grayMatter(source);
|
||||
const format = options.format ?? filePath.endsWith('.mdx') ? 'mdx' : 'md';
|
||||
const config: ProcessorOptions = {
|
||||
format,
|
||||
development: this.mode === 'development',
|
||||
...options,
|
||||
};
|
||||
|
||||
let timestamp: number | undefined;
|
||||
let processor = cache.get(format);
|
||||
|
||||
if (options.lastModifiedTime === 'git')
|
||||
timestamp = (await getGitTimestamp(filePath))?.getTime();
|
||||
|
||||
if (!processor) {
|
||||
processor = createProcessor(config);
|
||||
processor = createProcessor({
|
||||
format,
|
||||
development: this.mode === 'development',
|
||||
...options,
|
||||
});
|
||||
|
||||
cache.set(format, processor);
|
||||
}
|
||||
|
||||
@@ -52,6 +62,7 @@ export default function loader(
|
||||
value: content,
|
||||
path: filePath,
|
||||
data: {
|
||||
lastModified: timestamp,
|
||||
frontmatter,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -21,6 +21,7 @@ export type SourceFile<Meta extends MetaData, Fronmatter extends PageData> =
|
||||
export interface MDXExport<Frontmatter = PageData> {
|
||||
default: (props: MDXProps) => JSX.Element;
|
||||
frontmatter: Frontmatter;
|
||||
lastModified: number | undefined;
|
||||
toc: TableOfContents;
|
||||
structuredData: StructuredData;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import path from 'node:path';
|
||||
import fs from 'node:fs';
|
||||
import { spawn } from 'cross-spawn';
|
||||
|
||||
const cache = new Map<string, Date>();
|
||||
|
||||
export function getGitTimestamp(file: string): Promise<Date | undefined> {
|
||||
const cachedTimestamp = cache.get(file);
|
||||
if (cachedTimestamp) return Promise.resolve(cachedTimestamp);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
const cwd = path.dirname(file);
|
||||
if (!fs.existsSync(cwd)) {
|
||||
resolve(undefined);
|
||||
return;
|
||||
}
|
||||
const fileName = path.basename(file);
|
||||
const child = spawn('git', ['log', '-1', '--pretty="%ai"', fileName], {
|
||||
cwd,
|
||||
});
|
||||
|
||||
let output: Date | undefined;
|
||||
child.stdout.on('data', (d) => (output = new Date(String(d))));
|
||||
child.on('close', () => {
|
||||
if (output) cache.set(file, output);
|
||||
resolve(output);
|
||||
});
|
||||
child.on('error', reject);
|
||||
});
|
||||
}
|
||||
@@ -37,7 +37,7 @@ export interface DocsPageProps {
|
||||
items: NonNullable<FooterProps['items']>;
|
||||
}>;
|
||||
|
||||
lastUpdate?: Date | null;
|
||||
lastUpdate?: Date | string | number;
|
||||
|
||||
children: ReactNode;
|
||||
}
|
||||
@@ -53,7 +53,9 @@ export function DocsPage({
|
||||
<article className="flex w-0 flex-1 flex-col gap-6 py-10">
|
||||
{replaceOrDefault(breadcrumb, <Breadcrumb />)}
|
||||
{props.children}
|
||||
{props.lastUpdate ? <LastUpdate date={props.lastUpdate} /> : null}
|
||||
{props.lastUpdate ? (
|
||||
<LastUpdate date={new Date(props.lastUpdate)} />
|
||||
) : null}
|
||||
{replaceOrDefault(footer, <Footer items={footer.items} />)}
|
||||
</article>
|
||||
{replaceOrDefault(
|
||||
|
||||
Generated
+6
@@ -454,6 +454,9 @@ importers:
|
||||
'@mdx-js/mdx':
|
||||
specifier: ^3.0.0
|
||||
version: 3.0.0
|
||||
cross-spawn:
|
||||
specifier: ^7.0.3
|
||||
version: 7.0.3
|
||||
estree-util-value-to-estree:
|
||||
specifier: ^3.0.1
|
||||
version: 3.0.1
|
||||
@@ -470,6 +473,9 @@ importers:
|
||||
specifier: ^3.22.4
|
||||
version: 3.22.4
|
||||
devDependencies:
|
||||
'@types/cross-spawn':
|
||||
specifier: ^6.0.4
|
||||
version: 6.0.4
|
||||
'@types/mdast':
|
||||
specifier: ^4.0.3
|
||||
version: 4.0.3
|
||||
|
||||
Reference in New Issue
Block a user