Breaking: Pre-bundle page urls into raw pages

This commit is contained in:
SonMooSans
2023-12-06 21:03:34 +08:00
parent b25cb588bd
commit 9ef047da40
26 changed files with 185 additions and 212 deletions
+9
View File
@@ -0,0 +1,9 @@
---
'next-docs-zeta': major
'next-docs-mdx': major
'next-docs-ui': major
---
**Pre-bundle page urls into raw pages.**
This means you don't need `getPageUrl` anymore for built-in adapters, including `next-docs-mdx` and Contentlayer. It is now replaced by the `url` property from the pages array provided by your adapter.
@@ -24,7 +24,7 @@ export default async function Page({ params }: { params: Param }) {
notFound()
}
const neighbours = findNeighbour(tab.tree, tab.getPageUrl(page.slugs))
const neighbours = findNeighbour(tab.tree, page.url)
const path = resolve('apps/docs/content/docs/', page.file.path)
const time = await getGithubLastEdit({
@@ -87,7 +87,7 @@ function Category({ page, tab }: { page: Page; tab: Utils }) {
key={page.file.id}
title={page.matter.title}
description={page.matter.description ?? 'No Description'}
href={tab.getPageUrl(page.slugs)}
href={page.url}
/>
))}
</Cards>
+5 -5
View File
@@ -12,7 +12,7 @@ const frontmatterSchema = defaultValidators.frontmatter.extend({
index: z.boolean().default(false)
})
const slugs = (file: FileInfo) =>
const getSlugs = (file: FileInfo) =>
file.flattenedPath
.split('/')
.filter(p => !['index', ''].includes(p))
@@ -22,7 +22,7 @@ export const tabs: Record<string, Utils> = {
ui: fromMap(map, {
rootDir: 'ui',
baseUrl: '/docs/ui',
slugs,
getSlugs,
validate: {
frontmatter: frontmatterSchema
}
@@ -30,7 +30,7 @@ export const tabs: Record<string, Utils> = {
headless: fromMap(map, {
rootDir: 'headless',
baseUrl: '/docs/headless',
slugs,
getSlugs,
validate: {
frontmatter: frontmatterSchema
}
@@ -38,7 +38,7 @@ export const tabs: Record<string, Utils> = {
mdx: fromMap(map, {
rootDir: 'mdx',
baseUrl: '/docs/mdx',
slugs,
getSlugs,
validate: {
frontmatter: frontmatterSchema
}
@@ -80,7 +80,7 @@ if (
id: page.file.id,
title: page.matter.title,
description: page.matter.description,
url: tab.getPageUrl(page.slugs, page.file.locale),
url: page.url,
structuredData: page.data.structuredData
}))
})
+1 -2
View File
@@ -1,4 +1,3 @@
import { getPageUrl } from '@/app/source'
import { allDocs } from 'contentlayer/generated'
import { createSearchAPI } from 'next-docs-zeta/search/server'
@@ -7,7 +6,7 @@ export const { GET } = createSearchAPI('advanced', {
id: page._id,
title: page.title,
content: page.body.raw,
url: getPageUrl(page.slug),
url: page.url,
structuredData: page.structuredData,
tag: ''
})),
@@ -1,4 +1,4 @@
import { getPage, getPageUrl, tree } from '@/app/source'
import { getPage, tree } from '@/app/source'
import { allDocs } from 'contentlayer/generated'
import type { Metadata } from 'next'
import { MDXContent } from 'next-docs-ui/mdx'
@@ -19,7 +19,7 @@ export default async function Page({
}
const toc = await getTableOfContents(page.body.raw)
const neighbour = findNeighbour(tree, getPageUrl(params.slug))
const neighbour = findNeighbour(tree, page.url)
return (
<DocsPage toc={toc} footer={neighbour}>
+1 -4
View File
@@ -1,7 +1,4 @@
import { allDocs, allMeta } from 'contentlayer/generated'
import { createContentlayer } from 'next-docs-zeta/contentlayer'
export const { tree, getPage, getPageUrl } = createContentlayer(
allMeta,
allDocs
)
export const { tree, getPage } = createContentlayer(allMeta, allDocs)
+2 -2
View File
@@ -1,4 +1,4 @@
import { getPageUrl, pages } from '@/app/source'
import { pages } from '@/app/source'
import { createSearchAPI } from 'next-docs-zeta/search/server'
export const { GET } = createSearchAPI('advanced', {
@@ -6,6 +6,6 @@ export const { GET } = createSearchAPI('advanced', {
title: page.matter.title,
structuredData: page.data.structuredData,
id: page.file.id,
url: getPageUrl(page.slugs)
url: page.url
}))
})
@@ -1,4 +1,4 @@
import { getPage, getPageUrl, pages, tree } from '@/app/source'
import { getPage, pages, tree } from '@/app/source'
import type { Metadata } from 'next'
import { MDXContent } from 'next-docs-ui/mdx'
import { DocsPage } from 'next-docs-ui/page'
@@ -17,7 +17,7 @@ export default async function Page({
}
const MDX = page.data.default
const neighbour = findNeighbour(tree, getPageUrl(params.slug))
const neighbour = findNeighbour(tree, page.url)
return (
<DocsPage toc={page.data.toc} footer={neighbour}>
+1 -1
View File
@@ -1,4 +1,4 @@
import { map } from '@/_map'
import { fromMap } from 'next-docs-mdx/map'
export const { getPage, getPageUrl, pages, tree } = fromMap(map)
export const { getPage, pages, tree } = fromMap(map)
+1 -2
View File
@@ -1,4 +1,3 @@
import { getPageUrl } from '@/app/source'
import { allDocs } from 'contentlayer/generated'
import { createSearchAPI } from 'next-docs-zeta/search/server'
@@ -6,6 +5,6 @@ export const { GET } = createSearchAPI('simple', {
indexes: allDocs.map(page => ({
title: page.title,
content: page.body.raw,
url: getPageUrl(page.slug)
url: page.url
}))
})
@@ -1,4 +1,4 @@
import { getPage, getPageUrl, tree } from '@/app/source'
import { getPage, tree } from '@/app/source'
import { allDocs } from 'contentlayer/generated'
import type { Metadata } from 'next'
import { MDXContent } from 'next-docs-ui/mdx'
@@ -19,7 +19,7 @@ export default async function Page({
}
const toc = await getTableOfContents(page.body.raw)
const neighbour = findNeighbour(tree, getPageUrl(params.slug))
const neighbour = findNeighbour(tree, page.url)
return (
<DocsPage toc={toc} footer={neighbour}>
+1 -4
View File
@@ -1,7 +1,4 @@
import { allDocs, allMeta } from 'contentlayer/generated'
import { createContentlayer } from 'next-docs-zeta/contentlayer'
export const { getPage, getPageUrl, tree } = createContentlayer(
allMeta,
allDocs
)
export const { getPage, tree } = createContentlayer(allMeta, allDocs)
+2 -7
View File
@@ -6,17 +6,12 @@ import type { ReactElement } from 'react'
import type { ResolvedFiles } from './resolve-files'
export type BuilderOptions = {
/**
* Get url from slugs and locale, override the default getUrl function
*/
getUrl: (slugs: string[], locale?: string) => string
resolveIcon?: (icon: string) => ReactElement | undefined
}
export function getPageTreeBuilder(
{ metas, pages }: ResolvedFiles,
{ getUrl, resolveIcon = () => undefined }: BuilderOptions
{ resolveIcon = () => undefined }: BuilderOptions
): PageTreeBuilder {
return createPageTreeBuilder({
metas: metas.map(meta => ({
@@ -28,7 +23,7 @@ export function getPageTreeBuilder(
pages: pages.map(page => ({
file: page.file,
title: page.matter.title,
url: getUrl(page.slugs, page.file.locale),
url: page.url,
icon: page.matter.icon
})),
resolveIcon
+12 -8
View File
@@ -1,4 +1,8 @@
import type { BuildPageTreeOptions, PageTree } from 'next-docs-zeta/server'
import {
createGetUrl,
type BuildPageTreeOptions,
type PageTree
} from 'next-docs-zeta/server'
import { getPageTreeBuilder, type BuilderOptions } from './build-tree'
import { createPageUtils, type PageUtils } from './page-utils'
import {
@@ -25,7 +29,8 @@ type UtilsOptions<Langs extends string[] | undefined> = {
pageTreeOptions: BuildPageTreeOptions
slugs: ResolveOptions['slugs']
getSlugs: ResolveOptions['getSlugs']
getUrl?: ResolveOptions['getUrl']
validate: ResolveOptions['validate']
} & BuilderOptions
@@ -44,8 +49,8 @@ function fromMap<Langs extends string[] | undefined = undefined>(
{
baseUrl = '/docs',
rootDir = '',
slugs,
getUrl,
getSlugs,
getUrl = createGetUrl(baseUrl),
resolveIcon,
pageTreeOptions = { root: rootDir },
languages,
@@ -55,15 +60,14 @@ function fromMap<Langs extends string[] | undefined = undefined>(
const resolved = resolveFiles({
map,
rootDir,
slugs,
getSlugs,
getUrl,
validate
})
const pageUtils = createPageUtils(resolved, baseUrl, languages ?? [])
if (getUrl) pageUtils.getPageUrl = getUrl
const pageUtils = createPageUtils(resolved, languages ?? [])
const builder = getPageTreeBuilder(resolved, {
getUrl: pageUtils.getPageUrl,
resolveIcon
})
-18
View File
@@ -4,12 +4,10 @@ import type { Page } from './types'
export type PageUtils = {
getPages(locale?: string): Page[]
getPage(slugs: string[] | undefined, locale?: string): Page | null
getPageUrl(slugs: string[] | undefined, locale?: string): string
}
export function createPageUtils(
{ pages }: ResolvedFiles,
baseUrl: string,
languages: string[]
): PageUtils {
const pageMap = new Map<string, Page>()
@@ -42,28 +40,12 @@ export function createPageUtils(
getPages(locale = '') {
return i18nMap.get(locale)!
},
getPageUrl(slugs = [], locale) {
return joinPaths(baseUrl, ...slugs, locale ?? null)
},
getPage(slugs, locale) {
return getPage(pages, slugs, locale)
}
}
}
/**
* Join paths with leading slash
*/
function joinPaths(...paths: (string | null)[]): string {
const relative = paths
// filter slashes & empty
.filter(path => path != null && path.length !== 0 && path !== '/')
.join('/')
if (relative.startsWith('/')) return relative
return '/' + relative
}
function getPage(
pages: Page[],
slugs: string[] | undefined = [],
+12 -3
View File
@@ -29,12 +29,17 @@ export type ResolveOptions = {
/**
* Generate slugs from file info
*/
slugs?: (file: FileInfo) => string[]
getSlugs?: (file: FileInfo) => string[]
/**
* Check frontmatter/meta objects, transform allowed
*/
validate?: ValidateOptions
/**
* Get url from slugs and locale, override the default getUrl function
*/
getUrl: (slugs: string[], locale?: string) => string
}
type ValidateOptions = Partial<{
@@ -86,7 +91,8 @@ function parse<T>(schema: AnyZodObject, object: unknown, errorName: string): T {
export function resolveFiles({
map,
slugs = pathToSlugs,
getSlugs = pathToSlugs,
getUrl,
rootDir = '',
validate = defaultValidators
}: ResolveOptions): ResolvedFiles {
@@ -119,9 +125,12 @@ export function resolveFiles({
if (pageTypes.includes(file.type)) {
const data = v as MDXExport
const slugs = getSlugs(file)
const page: Page = {
file,
slugs: slugs(file),
slugs,
url: getUrl(slugs, file.locale),
matter: parse<Frontmatter>(
validate.frontmatter ?? defaultValidators.frontmatter,
data.frontmatter,
+1
View File
@@ -66,6 +66,7 @@ export interface Meta {
export interface Page {
file: FileInfo
url: string
slugs: string[]
data: MDXExport
matter: Frontmatter
@@ -8,7 +8,13 @@
import { rehypeImgSize, rehypeNextDocs, remarkGfm } from '@/mdx-plugins'
import type { RehypeNextDocsOptions } from '@/mdx-plugins/rehype-next-docs'
import type { Args, ComputedFields, FieldDef } from 'contentlayer/source-files'
import { createGetUrl } from '@/server/utils'
import type {
Args,
ComputedFields,
FieldDef,
LocalDocument
} from 'contentlayer/source-files'
import { defineDocumentType } from 'contentlayer/source-files'
import type { Options as ImgSizeOptions } from 'rehype-img-size'
@@ -33,7 +39,7 @@ function removePattern(path: string, pattern: string): string {
return removeSlash(path.slice(pattern.length))
}
type Options = {
export type Options = Partial<{
/**
* Where the docs files located
* @default "docs"
@@ -51,26 +57,43 @@ type Options = {
*/
imgDirPath: string
baseUrl: string
/**
* Get url from slugs and locale, override the default getUrl function
*/
getUrl: (slugs: string[], locale?: string) => string
pluginOptions: RehypeNextDocsOptions
docFields: Record<string, FieldDef>
docsComputedFields: ComputedFields<'Docs'>
metaFields: Record<string, FieldDef>
metaComputedFields: ComputedFields<'Meta'>
}
}>
export function createConfig(options: Partial<Options> = {}): Args {
export function createConfig(options: Options = {}): Args {
const {
docsPattern = 'docs',
contentDirPath = 'content',
imgDirPath = './public',
docFields,
metaFields,
baseUrl = '/docs',
getUrl = createGetUrl(baseUrl),
docsComputedFields,
pluginOptions,
metaComputedFields
} = options
function getSlugs(doc: LocalDocument): string {
return removePattern(doc._raw.flattenedPath.split('.')[0], docsPattern)
}
function getLocale(doc: LocalDocument): string {
return doc._raw.flattenedPath.split('.')[1]
}
const Docs = defineDocumentType(() => ({
name: 'Docs',
filePathPattern: `${docsPattern}/**/*.mdx`,
@@ -95,17 +118,16 @@ export function createConfig(options: Partial<Options> = {}): Args {
computedFields: {
locale: {
type: 'string',
resolve: post => {
return post._raw.flattenedPath.split('.')[1]
}
resolve: post => getLocale(post)
},
slug: {
type: 'string',
resolve: post => getSlugs(post)
},
url: {
type: 'string',
resolve: post => {
return removePattern(
post._raw.flattenedPath.split('.')[0],
docsPattern
)
return getUrl(getSlugs(post).split('/'), getLocale(post))
}
},
...docsComputedFields
@@ -5,17 +5,10 @@ import {
import type { RawDocumentData } from 'contentlayer/source-files'
import type { ReactElement } from 'react'
import type { DocsPageBase, MetaPageBase, PagesContext } from './types'
import { getPageUrl } from './utils'
export type ContextOptions = {
languages: string[]
baseUrl: string
/**
* Get url from slugs and locale, override the default getUrl function
*/
getUrl: (slugs: string[], locale?: string) => string
resolveIcon: (icon: string) => ReactElement | undefined
}
@@ -24,8 +17,6 @@ export function loadContext<Docs extends DocsPageBase>(
docsPages: Docs[],
{
languages = [],
baseUrl = '/docs',
getUrl = (slugs, locale) => getPageUrl(slugs, baseUrl, locale),
resolveIcon = () => undefined
}: Partial<ContextOptions> = {}
): PagesContext<Docs> {
@@ -33,7 +24,7 @@ export function loadContext<Docs extends DocsPageBase>(
pages: docsPages.map(page => ({
file: getFileData(page._raw, page.locale),
title: page.title,
url: getUrl(page.slug.split('/'), page.locale),
url: page.url,
icon: page.icon
})),
metas: metaPages.map(meta => ({
@@ -47,8 +38,7 @@ export function loadContext<Docs extends DocsPageBase>(
return {
builder,
i18nMap: getI18nPages(docsPages, languages),
getUrl
i18nMap: getI18nPages(docsPages, languages)
}
}
+1 -1
View File
@@ -25,6 +25,7 @@ export type DocsPageBase = {
description?: string | undefined
locale?: string
icon?: string
url: string
slug: string
}
@@ -35,5 +36,4 @@ export type PagesContext<Docs extends DocsPageBase = DocsPageBase> = {
* Language -> Page[]
*/
i18nMap: Map<string, Docs[]>
getUrl: (slugs: string[], locale?: string) => string
}
@@ -1,17 +1,5 @@
import type { DocsPageBase, PagesContext } from './types'
export function getPageUrl(slug: string[], baseUrl: string, locale?: string) {
const url = [baseUrl, locale, ...slug]
.filter(segment => segment != null && segment.length > 0)
.join('/')
if (url.startsWith('//')) {
return url.slice(1)
}
return url
}
export type ContentlayerUtils<Docs extends DocsPageBase> = {
/**
* Get list of pages from language
@@ -24,11 +12,6 @@ export type ContentlayerUtils<Docs extends DocsPageBase> = {
* @param language If empty, the default language will be used
*/
getPage: (slugs: string[] | undefined, language?: string) => Docs | undefined
getPageUrl: (
slugs: string | string[] | undefined,
language?: string
) => string
}
export function createUtils<Docs extends DocsPageBase>(
@@ -42,11 +25,6 @@ export function createUtils<Docs extends DocsPageBase>(
const path = slugs.join('/')
return context.i18nMap.get(language)?.find(page => page.slug === path)
},
getPageUrl(slug = [], language) {
const slugs = typeof slug === 'string' ? slug.split('/') : slug
return context.getUrl(slugs, language)
}
}
}
+1 -1
View File
@@ -1,7 +1,7 @@
export * from './server/get-toc'
export * from './server/types'
export { getTableOfContentsFromPortableText } from './server/get-toc-sanity'
export * from './server/tree-utils'
export * from './server/utils'
export * from './server/git-api'
export {
createPageTreeBuilder,
-20
View File
@@ -6,26 +6,6 @@ type Response = {
}
}[]
/**
* @param repo The repository, example: "fuma/next-docs"
* @param path the path to file
* @deprecated use `getGithubLastEdit` instead
*/
export async function getGitLastEditTime(
repo: string,
path: string,
customParams: Record<string, string> = {},
init: RequestInit = {}
): Promise<Date | null> {
return getGithubLastEdit({
repo: repo.split('/')[1],
owner: repo.split('/')[0],
path,
options: init,
params: customParams
})
}
export type GetGithubLastCommitOptions = {
/**
* Repository name, like "next-docs"
@@ -1,5 +1,6 @@
import type { ReactElement } from 'react'
import type { FileNode, FolderNode, PageTree, TreeNode } from './types'
import { joinPaths, splitPath } from './utils'
export type AbstractFile = {
locale?: string
@@ -318,41 +319,3 @@ function pathToName(path: string[]): string {
const name = path[path.length - 1] ?? 'docs'
return name.slice(0, 1).toUpperCase() + name.slice(1)
}
/**
* Split path into segments, trailing/leading slashes are removed
*/
export function splitPath(path: string): string[] {
return path.split('/').filter(p => p.length > 0)
}
/**
* Convert paths to an array, slashes within the path will be ignored
* @param paths
* @param slash Add a trailing/leading slash to path
* @example
* ```
* ['a','b','c'] // 'a/b/c'
* ['/a'] // 'a'
* ['a', '/b'] // 'a/b'
* ['a', 'b/c'] // 'a/b/c'
* ```
*/
export function joinPaths(
paths: string[],
slash: 'leading' | 'trailing' | 'none' = 'none'
): string {
const joined = paths
// avoid slashes in path and filter empty
.flatMap(path => splitPath(path))
.join('/')
switch (slash) {
case 'leading':
return '/' + joined
case 'trailing':
return joined + '/'
default:
return joined
}
}
@@ -1,43 +0,0 @@
import type { FileNode, PageTree, TreeNode } from './types'
/**
* Flatten tree to an array of page nodes
*/
export function flattenTree(tree: TreeNode[]): FileNode[] {
return tree.flatMap(node => {
if (node.type === 'separator') return []
if (node.type === 'folder') {
const child = flattenTree(node.children)
if (node.index) return [node.index, ...child]
return child
}
return [node]
})
}
/**
* Get neighbours of a page, useful for implementing "previous & next" buttons
*/
export function findNeighbour(
tree: PageTree,
url: string
): {
previous?: FileNode
next?: FileNode
} {
const list = flattenTree(tree.children)
for (let i = 0; i < list.length; i++) {
if (list[i].url === url) {
return {
next: list[i + 1],
previous: list[i - 1]
}
}
}
return {}
}
+91
View File
@@ -0,0 +1,91 @@
import type { FileNode, PageTree, TreeNode } from './types'
/**
* Flatten tree to an array of page nodes
*/
export function flattenTree(tree: TreeNode[]): FileNode[] {
return tree.flatMap(node => {
if (node.type === 'separator') return []
if (node.type === 'folder') {
const child = flattenTree(node.children)
if (node.index) return [node.index, ...child]
return child
}
return [node]
})
}
/**
* Get neighbours of a page, useful for implementing "previous & next" buttons
*/
export function findNeighbour(
tree: PageTree,
url: string
): {
previous?: FileNode
next?: FileNode
} {
const list = flattenTree(tree.children)
for (let i = 0; i < list.length; i++) {
if (list[i].url === url) {
return {
next: list[i + 1],
previous: list[i - 1]
}
}
}
return {}
}
/**
* Split path into segments, trailing/leading slashes are removed
*/
export function splitPath(path: string): string[] {
return path.split('/').filter(p => p.length > 0)
}
/**
* Convert paths to an array, slashes within the path will be ignored
* @param paths
* @param slash whether to add a trailing/leading slash to path
* @example
* ```
* ['a','b','c'] // 'a/b/c'
* ['/a'] // 'a'
* ['a', '/b'] // 'a/b'
* ['a', 'b/c'] // 'a/b/c'
* ```
*/
export function joinPaths(
paths: string[],
slash: 'leading' | 'trailing' | 'none' = 'none'
): string {
const joined = paths
// avoid slashes in path and filter empty
.flatMap(path => splitPath(path))
.join('/')
switch (slash) {
case 'leading':
return '/' + joined
case 'trailing':
return joined + '/'
default:
return joined
}
}
export function createGetUrl(baseUrl: string): {
(slugs: string[], locale?: string): string
} {
return (slugs, locale) => {
const paths = [baseUrl, ...slugs]
if (locale) paths.push(locale)
return joinPaths(paths, 'leading')
}
}