From 27ad01c560e371248e396e6162f8c03bce9c4b2c Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Thu, 2 Nov 2023 08:55:50 +0100 Subject: [PATCH 1/7] feat: parse local docs into nested structure --- scripts/fetch-docs-local.mjs | 89 +++++++++++++++++++++++------------- 1 file changed, 57 insertions(+), 32 deletions(-) diff --git a/scripts/fetch-docs-local.mjs b/scripts/fetch-docs-local.mjs index 1d9ad648c..3303b7da9 100644 --- a/scripts/fetch-docs-local.mjs +++ b/scripts/fetch-docs-local.mjs @@ -72,6 +72,61 @@ async function getHeadings(source) { }) } +export async function parseDocs(fileNames, topicSlug, topicDirectory) { + const parsedDocs = await Promise.all( + fileNames.map(async docFilename => { + try { + const path = `${topicDirectory}/${docFilename}` + + if(fs.lstatSync(path).isDirectory()) { + const subDocSlugs = fs.readdirSync(path) + + const subDocs = await parseDocs( + subDocSlugs, + docFilename, + path + ) + const subTopic = { + slug: docFilename, + fullSlug: `${topicSlug}/${docFilename}`, + docs: subDocs.filter(Boolean).sort((a, b) => a.order - b.order), + } + return subTopic + }else { + const rawDoc = fs.readFileSync( + path, + 'utf8', + ) + + const parsedDoc = matter(rawDoc) + + const doc = { + content: await serialize(parsedDoc.content, { + mdxOptions: { + remarkPlugins: [remarkGfm], + }, + }), + title: parsedDoc.data.title, + slug: docFilename.replace('.mdx', ''), + label: parsedDoc.data.label, + order: parsedDoc.data.order, + desc: parsedDoc.data.desc || '', + keywords: parsedDoc.data.keywords || '', + headings: await getHeadings(parsedDoc.content), + } + + return doc + } + + } catch (error) { + const msg = error instanceof Error ? error.message : error || 'Unknown error' + console.error(`Error fetching ${docFilename}: ${msg}`) // eslint-disable-line no-console + } + }), + ) + return parsedDocs +} + const fetchDocs = async () => { const topics = await Promise.all( topicOrder.map(async unsanitizedTopicSlug => { @@ -80,41 +135,11 @@ const fetchDocs = async () => { const topicDirectory = path.join(docsDirectory, `./${topicSlug}`) const docSlugs = fs.readdirSync(topicDirectory) - const parsedDocs = await Promise.all( - docSlugs.map(async docFilename => { - try { - const rawDoc = fs.readFileSync( - `${docsDirectory}/${topicSlug.toLowerCase()}/${docFilename}`, - 'utf8', - ) - - const parsedDoc = matter(rawDoc) - - const doc = { - content: await serialize(parsedDoc.content, { - mdxOptions: { - remarkPlugins: [remarkGfm], - }, - }), - title: parsedDoc.data.title, - slug: docFilename.replace('.mdx', ''), - label: parsedDoc.data.label, - order: parsedDoc.data.order, - desc: parsedDoc.data.desc || '', - keywords: parsedDoc.data.keywords || '', - headings: await getHeadings(parsedDoc.content), - } - - return doc - } catch (error) { - const msg = err instanceof Error ? err.message : err || 'Unknown error' - console.error(`Error fetching ${docFilename}: ${msg}`) // eslint-disable-line no-console - } - }), - ) + const parsedDocs = await parseDocs(docSlugs, topicSlug, `${docsDirectory}/${topicSlug}`) const topic = { slug: unsanitizedTopicSlug, + fullSlug: unsanitizedTopicSlug, docs: parsedDocs.filter(Boolean).sort((a, b) => a.order - b.order), } From e1f6d2d7e9a63f6995688515af4cefee52be46f5 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Thu, 2 Nov 2023 08:56:58 +0100 Subject: [PATCH 2/7] feat: include sub-docs in getTopics() --- src/app/(pages)/docs/api.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/app/(pages)/docs/api.ts b/src/app/(pages)/docs/api.ts index 4baea1b9d..7b6818382 100644 --- a/src/app/(pages)/docs/api.ts +++ b/src/app/(pages)/docs/api.ts @@ -1,5 +1,5 @@ import content from '../../docs.json' -import type { Doc, DocPath, Topic } from './types' +import type { Doc, DocMeta, DocPath, Topic } from './types' export async function getTopics(): Promise { return content.map(topic => ({ @@ -9,12 +9,13 @@ export async function getTopics(): Promise { label: doc?.label || '', slug: doc?.slug || '', order: doc?.order || 0, + docs: (doc?.docs as DocMeta[]) || null, })), })) } export async function getDoc({ topic: topicSlug, doc: docSlug }: DocPath): Promise { - const matchedTopic = content.find(topic => topic.slug.toLowerCase() === topicSlug) + const matchedTopic = content.find(topic => topic.fullSlug.toLowerCase() === topicSlug) const matchedDoc = matchedTopic?.docs?.find(doc => doc?.slug === docSlug) || null return matchedDoc } From db765efdbf4bd7ea64e68fed497f9e6da28b8463 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Thu, 2 Nov 2023 08:57:11 +0100 Subject: [PATCH 3/7] feat: render sub-docs --- src/app/(pages)/docs/client_layout.tsx | 183 +++++++++++++++---------- src/app/(pages)/docs/types.ts | 17 ++- 2 files changed, 117 insertions(+), 83 deletions(-) diff --git a/src/app/(pages)/docs/client_layout.tsx b/src/app/(pages)/docs/client_layout.tsx index 79d80aa5a..daff3f37e 100644 --- a/src/app/(pages)/docs/client_layout.tsx +++ b/src/app/(pages)/docs/client_layout.tsx @@ -20,9 +20,108 @@ type Props = { children: React.ReactNode } -export const RenderDocs: React.FC = ({ topics, children }) => { - const [topicParam, docParam] = useSelectedLayoutSegments() +type RenderSidebarProps = { + topics: Topic[] + openTopicPreferences?: string[] + setOpenTopicPreferences: (topics: string[]) => void + init?: boolean +} +export const RenderSidebarTopics: React.FC = ({ + topics, + setOpenTopicPreferences, + openTopicPreferences, + init, +}) => { const [currentTopicIsOpen, setCurrentTopicIsOpen] = useState(true) + const [topicParam, docParam, subDocParam] = useSelectedLayoutSegments() + + return ( + <> + {topics.map(topic => { + const topicSlug = topic.slug.toLowerCase() + const isCurrentTopic = topicParam === topicSlug || docParam === topicSlug + const isActive = + openTopicPreferences?.includes(topicSlug) || (isCurrentTopic && currentTopicIsOpen) + + return ( + + + +
    + {topic.docs.map((doc: DocMeta) => { + const isDocActive = docParam === doc.slug && topicParam === topicSlug + + if ('docs' in doc && doc?.docs) { + return ( + + ) + } + return ( +
  • + + {doc.label} + +
  • + ) + })} +
+
+
+ ) + })} + + ) +} + +export const RenderDocs: React.FC = ({ topics, children }) => { + const [topicParam, docParam, subDocParam] = useSelectedLayoutSegments() const [openTopicPreferences, setOpenTopicPreferences] = useState() const [init, setInit] = useState(false) const [navOpen, setNavOpen] = useState(false) @@ -60,80 +159,12 @@ export const RenderDocs: React.FC = ({ topics, children }) => { .filter(Boolean) .join(' ')} > - {topics.map(topic => { - const topicSlug = topic.slug.toLowerCase() - const isCurrentTopic = topicParam === topicSlug - const isActive = - openTopicPreferences?.includes(topicSlug) || (isCurrentTopic && currentTopicIsOpen) - - return ( - - - -
    - {topic.docs.map((doc: DocMeta) => { - const isDocActive = docParam === doc.slug && topicParam === topicSlug - - return ( -
  • - - {doc.label} - -
  • - ) - })} -
-
-
- ) - })} +
{children}
diff --git a/src/app/(pages)/docs/types.ts b/src/app/(pages)/docs/types.ts index 65174386b..97383510a 100644 --- a/src/app/(pages)/docs/types.ts +++ b/src/app/(pages)/docs/types.ts @@ -5,13 +5,15 @@ export interface Heading { } export interface Doc { - content: any // eslint-disable-line - order: number - title: string - label: string - desc: string - keywords: string - headings: Heading[] + content?: any // eslint-disable-line + order?: number + title?: string + label?: string + desc?: string + keywords?: string + headings?: Heading[] + docs?: Doc[] + fullSlug?: string } export interface NextDoc { @@ -31,6 +33,7 @@ export interface DocMeta { label: string slug: string order: number + docs?: DocMeta[] } export interface Topic { From ccb812dee6ee2076e5822ded31f71f55dd0868c3 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Thu, 2 Nov 2023 10:59:49 +0100 Subject: [PATCH 4/7] feat: ability to load nested docs --- scripts/fetch-docs-local.mjs | 11 +++-- .../{[doc] => [...doc]}/client_page.tsx | 0 .../{[doc] => [...doc]}/index.module.scss | 0 .../docs/[topic]/{[doc] => [...doc]}/page.tsx | 48 +++++++++++-------- src/app/(pages)/docs/api.ts | 36 ++++++++++++-- src/app/(pages)/docs/client_layout.tsx | 8 +++- src/app/(pages)/docs/types.ts | 3 ++ src/graphics/ChevronIcon/index.tsx | 8 +++- 8 files changed, 82 insertions(+), 32 deletions(-) rename src/app/(pages)/docs/[topic]/{[doc] => [...doc]}/client_page.tsx (100%) rename src/app/(pages)/docs/[topic]/{[doc] => [...doc]}/index.module.scss (100%) rename src/app/(pages)/docs/[topic]/{[doc] => [...doc]}/page.tsx (65%) diff --git a/scripts/fetch-docs-local.mjs b/scripts/fetch-docs-local.mjs index 3303b7da9..f4e3ce9e8 100644 --- a/scripts/fetch-docs-local.mjs +++ b/scripts/fetch-docs-local.mjs @@ -72,7 +72,7 @@ async function getHeadings(source) { }) } -export async function parseDocs(fileNames, topicSlug, topicDirectory) { +export async function parseDocs(fileNames, topicSlugs, topicDirectory) { const parsedDocs = await Promise.all( fileNames.map(async docFilename => { try { @@ -83,16 +83,16 @@ export async function parseDocs(fileNames, topicSlug, topicDirectory) { const subDocs = await parseDocs( subDocSlugs, - docFilename, + topicSlugs.concat(docFilename), path ) const subTopic = { slug: docFilename, - fullSlug: `${topicSlug}/${docFilename}`, + fullSlug: `${topicSlugs.join('/')}/${docFilename}`, docs: subDocs.filter(Boolean).sort((a, b) => a.order - b.order), } return subTopic - }else { + } else { const rawDoc = fs.readFileSync( path, 'utf8', @@ -108,6 +108,7 @@ export async function parseDocs(fileNames, topicSlug, topicDirectory) { }), title: parsedDoc.data.title, slug: docFilename.replace('.mdx', ''), + fullSlug: `${topicSlugs.join('/')}/${docFilename.replace('.mdx', '')}`, label: parsedDoc.data.label, order: parsedDoc.data.order, desc: parsedDoc.data.desc || '', @@ -135,7 +136,7 @@ const fetchDocs = async () => { const topicDirectory = path.join(docsDirectory, `./${topicSlug}`) const docSlugs = fs.readdirSync(topicDirectory) - const parsedDocs = await parseDocs(docSlugs, topicSlug, `${docsDirectory}/${topicSlug}`) + const parsedDocs = await parseDocs(docSlugs, [topicSlug], `${docsDirectory}/${topicSlug}`) const topic = { slug: unsanitizedTopicSlug, diff --git a/src/app/(pages)/docs/[topic]/[doc]/client_page.tsx b/src/app/(pages)/docs/[topic]/[...doc]/client_page.tsx similarity index 100% rename from src/app/(pages)/docs/[topic]/[doc]/client_page.tsx rename to src/app/(pages)/docs/[topic]/[...doc]/client_page.tsx diff --git a/src/app/(pages)/docs/[topic]/[doc]/index.module.scss b/src/app/(pages)/docs/[topic]/[...doc]/index.module.scss similarity index 100% rename from src/app/(pages)/docs/[topic]/[doc]/index.module.scss rename to src/app/(pages)/docs/[topic]/[...doc]/index.module.scss diff --git a/src/app/(pages)/docs/[topic]/[doc]/page.tsx b/src/app/(pages)/docs/[topic]/[...doc]/page.tsx similarity index 65% rename from src/app/(pages)/docs/[topic]/[doc]/page.tsx rename to src/app/(pages)/docs/[topic]/[...doc]/page.tsx index 0d890708f..c57ef2ff3 100644 --- a/src/app/(pages)/docs/[topic]/[doc]/page.tsx +++ b/src/app/(pages)/docs/[topic]/[...doc]/page.tsx @@ -8,8 +8,9 @@ import { NextDoc } from '../../types' import { RenderDoc } from './client_page' const Doc = async ({ params }) => { - const { topic, doc: docSlug } = params - const doc = await getDoc({ topic, doc: docSlug }) + const { topic, doc: docSlugs } = params + const fullDocSlug = topic + '/' + docSlugs.join('/') + const doc = await getDoc({ topic, doc: fullDocSlug }) const topics = await getTopics() const relatedThreads = await fetchRelatedThreads() @@ -29,7 +30,9 @@ const Doc = async ({ params }) => { let next: NextDoc | null = null if (parentTopic) { - const docIndex = parentTopic?.docs.findIndex(({ slug }) => slug === docSlug) + const docIndex = parentTopic?.docs.findIndex( + ({ fullSlug, slug }) => slug === fullDocSlug || fullSlug === fullDocSlug, + ) if (parentTopic?.docs?.[docIndex + 1]) { next = { @@ -57,7 +60,7 @@ export default Doc type Param = { topic: string - doc: string + doc: string[] } export async function generateStaticParams() { @@ -65,33 +68,36 @@ export async function generateStaticParams() { const topics = await getTopics() + function extractParams(docs, topicSlug: string, parentSlugs: string[] = []): Param[] { + return docs.flatMap(doc => { + // If doc has subdocs, recursively call extractParams + if (doc.docs && doc.docs.length > 0) { + return extractParams(doc.docs, topicSlug, [...parentSlugs, doc.slug]) + } else if (doc.slug) { + // If there are no subdocs, add the doc (including parent slugs if any) + return [{ topic: topicSlug.toLowerCase(), doc: [...parentSlugs, doc.slug] }] + } + return [] // If doc has no slug, return an empty array to avoid null values + }) + } + const result = topics.reduce((params: Param[], topic) => { - return params.concat( - topic.docs - .map(doc => { - if (!doc.slug) return null as any - - return { - topic: topic.slug.toLowerCase(), - doc: doc.slug, - } - }) - .filter(Boolean), - ) - }, []) + const topicParams = extractParams(topic.docs, topic.slug) + return params.concat(topicParams) + }, [] as Param[]) return result } - -export async function generateMetadata({ params: { topic: topicSlug, doc: docSlug } }) { - const doc = await getDoc({ topic: topicSlug, doc: docSlug }) +export async function generateMetadata({ params: { topic: topicSlug, doc: docSlugs } }) { + const fullSlug = topicSlug + '/' + docSlugs.join('/') + const doc = await getDoc({ topic: topicSlug, doc: fullSlug }) return { title: `${doc?.title ? `${doc.title} | ` : ''}Documentation | Payload CMS`, description: doc?.desc || `Payload CMS ${topicSlug} Documentation`, openGraph: mergeOpenGraph({ title: `${doc?.title ? `${doc.title} | ` : ''}Documentation | Payload CMS`, - url: `/docs/${topicSlug}/${docSlug}`, + url: `/docs/${topicSlug}/${fullSlug}`, images: [ { url: `/api/og?topic=${topicSlug}&title=${doc?.title}`, diff --git a/src/app/(pages)/docs/api.ts b/src/app/(pages)/docs/api.ts index 7b6818382..7333cc831 100644 --- a/src/app/(pages)/docs/api.ts +++ b/src/app/(pages)/docs/api.ts @@ -4,18 +4,48 @@ import type { Doc, DocMeta, DocPath, Topic } from './types' export async function getTopics(): Promise { return content.map(topic => ({ slug: topic.slug, + fullSlug: topic.fullSlug || topic.slug, docs: topic.docs.map(doc => ({ title: doc?.title || '', label: doc?.label || '', slug: doc?.slug || '', order: doc?.order || 0, docs: (doc?.docs as DocMeta[]) || null, + fullSlug: doc?.fullSlug || doc?.slug, })), })) } export async function getDoc({ topic: topicSlug, doc: docSlug }: DocPath): Promise { - const matchedTopic = content.find(topic => topic.fullSlug.toLowerCase() === topicSlug) - const matchedDoc = matchedTopic?.docs?.find(doc => doc?.slug === docSlug) || null - return matchedDoc + // Find the matched topic first + const matchedTopic = content.find( + topic => + topic.slug.toLowerCase() === topicSlug.toLowerCase() || + topic.fullSlug.toLowerCase() === topicSlug.toLowerCase(), + ) + + // If there's no matched topic, return null early. + if (!matchedTopic) return null + + // Recursive function to find a doc by slug within a topic or sub-docs + function findDoc(docs: Doc[], slug: string): Doc | null { + for (const doc of docs) { + // Check if the current doc matches the slug + if (doc.slug === slug || doc.fullSlug === slug) { + return doc + } + // If the current doc has subdocs, search within them recursively + if (doc.docs && doc.docs.length > 0) { + const subDoc = findDoc(doc.docs, slug.toLowerCase()) + if (subDoc) { + return subDoc + } + } + } + // If no doc matches, return null + return null + } + + // Use the recursive function to find the matched doc or subdoc + return findDoc(matchedTopic.docs || [], docSlug.toLowerCase()) } diff --git a/src/app/(pages)/docs/client_layout.tsx b/src/app/(pages)/docs/client_layout.tsx index daff3f37e..46a067e21 100644 --- a/src/app/(pages)/docs/client_layout.tsx +++ b/src/app/(pages)/docs/client_layout.tsx @@ -25,12 +25,14 @@ type RenderSidebarProps = { openTopicPreferences?: string[] setOpenTopicPreferences: (topics: string[]) => void init?: boolean + nesting: number } export const RenderSidebarTopics: React.FC = ({ topics, setOpenTopicPreferences, openTopicPreferences, init, + nesting, }) => { const [currentTopicIsOpen, setCurrentTopicIsOpen] = useState(true) const [topicParam, docParam, subDocParam] = useSelectedLayoutSegments() @@ -50,6 +52,7 @@ export const RenderSidebarTopics: React.FC = ({ className={[classes.topic, isActive && classes['topic--open']] .filter(Boolean) .join(' ')} + style={nesting >= 1 ? { marginLeft: 8, marginTop: 5 } : {}} onClick={() => { if (isCurrentTopic) { if (openTopicPreferences?.includes(topicSlug) && currentTopicIsOpen) { @@ -75,6 +78,7 @@ export const RenderSidebarTopics: React.FC = ({ }} > = 1 ? { marginRight: 3 } : {}} className={[classes.toggleChevron, isActive && classes.activeToggleChevron] .filter(Boolean) .join(' ')} @@ -94,13 +98,14 @@ export const RenderSidebarTopics: React.FC = ({ openTopicPreferences={openTopicPreferences} init={init} key={doc.slug} + nesting={nesting + 1} /> ) } return (
  • = ({ topics, children }) => { setOpenTopicPreferences={setOpenTopicPreferences} openTopicPreferences={openTopicPreferences} init={init} + nesting={0} />
    diff --git a/src/app/(pages)/docs/types.ts b/src/app/(pages)/docs/types.ts index 97383510a..4fed74da3 100644 --- a/src/app/(pages)/docs/types.ts +++ b/src/app/(pages)/docs/types.ts @@ -13,6 +13,7 @@ export interface Doc { keywords?: string headings?: Heading[] docs?: Doc[] + slug?: string fullSlug?: string } @@ -34,9 +35,11 @@ export interface DocMeta { slug: string order: number docs?: DocMeta[] + fullSlug?: string } export interface Topic { docs: DocMeta[] slug: string + fullSlug?: string } diff --git a/src/graphics/ChevronIcon/index.tsx b/src/graphics/ChevronIcon/index.tsx index 553e88b85..890cf8c93 100644 --- a/src/graphics/ChevronIcon/index.tsx +++ b/src/graphics/ChevronIcon/index.tsx @@ -1,6 +1,9 @@ -import React from 'react' +import React, { CSSProperties } from 'react' -export const ChevronIcon: React.FC<{ className?: string }> = ({ className }) => { +export const ChevronIcon: React.FC<{ className?: string; style?: CSSProperties }> = ({ + className, + style, +}) => { return ( = ({ className }) => fill="none" xmlns="http://www.w3.org/2000/svg" className={className} + style={style} > From d54229550678dfb72fe5e62c61f820b2604d13ce Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Thu, 2 Nov 2023 20:49:18 +0100 Subject: [PATCH 5/7] chore: get active sidebar doc to work --- scripts/fetch-docs-local.mjs | 6 ++--- .../(pages)/docs/[topic]/[...doc]/page.tsx | 12 +++++----- src/app/(pages)/docs/api.ts | 23 +++++++++---------- src/app/(pages)/docs/client_layout.tsx | 19 ++++++++++----- src/app/(pages)/docs/types.ts | 8 ++++--- 5 files changed, 38 insertions(+), 30 deletions(-) diff --git a/scripts/fetch-docs-local.mjs b/scripts/fetch-docs-local.mjs index f4e3ce9e8..22258bf98 100644 --- a/scripts/fetch-docs-local.mjs +++ b/scripts/fetch-docs-local.mjs @@ -88,7 +88,7 @@ export async function parseDocs(fileNames, topicSlugs, topicDirectory) { ) const subTopic = { slug: docFilename, - fullSlug: `${topicSlugs.join('/')}/${docFilename}`, + path: topicSlugs.join('/')+'/', docs: subDocs.filter(Boolean).sort((a, b) => a.order - b.order), } return subTopic @@ -108,7 +108,7 @@ export async function parseDocs(fileNames, topicSlugs, topicDirectory) { }), title: parsedDoc.data.title, slug: docFilename.replace('.mdx', ''), - fullSlug: `${topicSlugs.join('/')}/${docFilename.replace('.mdx', '')}`, + path: topicSlugs.join('/')+'/', label: parsedDoc.data.label, order: parsedDoc.data.order, desc: parsedDoc.data.desc || '', @@ -140,7 +140,7 @@ const fetchDocs = async () => { const topic = { slug: unsanitizedTopicSlug, - fullSlug: unsanitizedTopicSlug, + path: '/', docs: parsedDocs.filter(Boolean).sort((a, b) => a.order - b.order), } diff --git a/src/app/(pages)/docs/[topic]/[...doc]/page.tsx b/src/app/(pages)/docs/[topic]/[...doc]/page.tsx index c57ef2ff3..b710f44ab 100644 --- a/src/app/(pages)/docs/[topic]/[...doc]/page.tsx +++ b/src/app/(pages)/docs/[topic]/[...doc]/page.tsx @@ -9,8 +9,8 @@ import { RenderDoc } from './client_page' const Doc = async ({ params }) => { const { topic, doc: docSlugs } = params - const fullDocSlug = topic + '/' + docSlugs.join('/') - const doc = await getDoc({ topic, doc: fullDocSlug }) + const docPathWithSlug = topic + '/' + docSlugs.join('/') + const doc = await getDoc({ topic, doc: docPathWithSlug }) const topics = await getTopics() const relatedThreads = await fetchRelatedThreads() @@ -31,7 +31,7 @@ const Doc = async ({ params }) => { if (parentTopic) { const docIndex = parentTopic?.docs.findIndex( - ({ fullSlug, slug }) => slug === fullDocSlug || fullSlug === fullDocSlug, + ({ path, slug }) => path + slug === docPathWithSlug, ) if (parentTopic?.docs?.[docIndex + 1]) { @@ -89,15 +89,15 @@ export async function generateStaticParams() { return result } export async function generateMetadata({ params: { topic: topicSlug, doc: docSlugs } }) { - const fullSlug = topicSlug + '/' + docSlugs.join('/') - const doc = await getDoc({ topic: topicSlug, doc: fullSlug }) + const docPathWithSlug = topicSlug + docSlugs.join('/') + const doc = await getDoc({ topic: topicSlug, doc: docPathWithSlug }) return { title: `${doc?.title ? `${doc.title} | ` : ''}Documentation | Payload CMS`, description: doc?.desc || `Payload CMS ${topicSlug} Documentation`, openGraph: mergeOpenGraph({ title: `${doc?.title ? `${doc.title} | ` : ''}Documentation | Payload CMS`, - url: `/docs/${topicSlug}/${fullSlug}`, + url: `/docs/${docPathWithSlug}`, images: [ { url: `/api/og?topic=${topicSlug}&title=${doc?.title}`, diff --git a/src/app/(pages)/docs/api.ts b/src/app/(pages)/docs/api.ts index 7333cc831..456283a7a 100644 --- a/src/app/(pages)/docs/api.ts +++ b/src/app/(pages)/docs/api.ts @@ -4,39 +4,38 @@ import type { Doc, DocMeta, DocPath, Topic } from './types' export async function getTopics(): Promise { return content.map(topic => ({ slug: topic.slug, - fullSlug: topic.fullSlug || topic.slug, + path: topic.path || '/', docs: topic.docs.map(doc => ({ title: doc?.title || '', label: doc?.label || '', slug: doc?.slug || '', order: doc?.order || 0, docs: (doc?.docs as DocMeta[]) || null, - fullSlug: doc?.fullSlug || doc?.slug, + path: doc?.path || '/', })), })) } -export async function getDoc({ topic: topicSlug, doc: docSlug }: DocPath): Promise { +export async function getDoc({ + topic: topicSlug, + doc: docPathWithSlug, +}: DocPath): Promise { // Find the matched topic first - const matchedTopic = content.find( - topic => - topic.slug.toLowerCase() === topicSlug.toLowerCase() || - topic.fullSlug.toLowerCase() === topicSlug.toLowerCase(), - ) + const matchedTopic = content.find(topic => topic.slug.toLowerCase() === topicSlug.toLowerCase()) // If there's no matched topic, return null early. if (!matchedTopic) return null // Recursive function to find a doc by slug within a topic or sub-docs - function findDoc(docs: Doc[], slug: string): Doc | null { + function findDoc(docs: Doc[], pathAndSlug: string): Doc | null { for (const doc of docs) { // Check if the current doc matches the slug - if (doc.slug === slug || doc.fullSlug === slug) { + if (doc && (doc.path || '/') + (doc.slug || '/') === pathAndSlug) { return doc } // If the current doc has subdocs, search within them recursively if (doc.docs && doc.docs.length > 0) { - const subDoc = findDoc(doc.docs, slug.toLowerCase()) + const subDoc = findDoc(doc.docs, pathAndSlug.toLowerCase()) if (subDoc) { return subDoc } @@ -47,5 +46,5 @@ export async function getDoc({ topic: topicSlug, doc: docSlug }: DocPath): Promi } // Use the recursive function to find the matched doc or subdoc - return findDoc(matchedTopic.docs || [], docSlug.toLowerCase()) + return findDoc(matchedTopic.docs || [], docPathWithSlug.toLowerCase()) } diff --git a/src/app/(pages)/docs/client_layout.tsx b/src/app/(pages)/docs/client_layout.tsx index 46a067e21..22ae5f881 100644 --- a/src/app/(pages)/docs/client_layout.tsx +++ b/src/app/(pages)/docs/client_layout.tsx @@ -13,7 +13,7 @@ import { DocMeta, Topic } from './types' import classes from './index.module.scss' -const openTopicsLocalStorageKey = 'docs-open-topics' +const openTopicsLocalStorageKey = 'docs-open-topics' as const type Props = { topics: Topic[] @@ -35,13 +35,14 @@ export const RenderSidebarTopics: React.FC = ({ nesting, }) => { const [currentTopicIsOpen, setCurrentTopicIsOpen] = useState(true) - const [topicParam, docParam, subDocParam] = useSelectedLayoutSegments() + const [...params] = useSelectedLayoutSegments() + const middleParams = params[1].split('/') return ( <> {topics.map(topic => { const topicSlug = topic.slug.toLowerCase() - const isCurrentTopic = topicParam === topicSlug || docParam === topicSlug + const isCurrentTopic = params[0] === topicSlug || params[1] === topicSlug const isActive = openTopicPreferences?.includes(topicSlug) || (isCurrentTopic && currentTopicIsOpen) @@ -88,7 +89,12 @@ export const RenderSidebarTopics: React.FC = ({
      {topic.docs.map((doc: DocMeta) => { - const isDocActive = docParam === doc.slug && topicParam === topicSlug + // Check if doc slug matches, and if topic matches + const isDocActive = + params.join('/') === doc.path + doc.slug && + (middleParams.length >= 2 + ? middleParams[middleParams.length - 2] + : params[0] === topicSlug) if ('docs' in doc && doc?.docs) { return ( @@ -105,7 +111,7 @@ export const RenderSidebarTopics: React.FC = ({ return (
    • = ({ } export const RenderDocs: React.FC = ({ topics, children }) => { - const [topicParam, docParam, subDocParam] = useSelectedLayoutSegments() + const [topicParam] = useSelectedLayoutSegments() const [openTopicPreferences, setOpenTopicPreferences] = useState() const [init, setInit] = useState(false) const [navOpen, setNavOpen] = useState(false) @@ -184,6 +190,7 @@ export const RenderDocs: React.FC = ({ topics, children }) => { {navOpen && }
    + ; ) } diff --git a/src/app/(pages)/docs/types.ts b/src/app/(pages)/docs/types.ts index 4fed74da3..e13ab2f0b 100644 --- a/src/app/(pages)/docs/types.ts +++ b/src/app/(pages)/docs/types.ts @@ -14,7 +14,7 @@ export interface Doc { headings?: Heading[] docs?: Doc[] slug?: string - fullSlug?: string + path?: string } export interface NextDoc { @@ -35,11 +35,13 @@ export interface DocMeta { slug: string order: number docs?: DocMeta[] - fullSlug?: string + /** Path, not including the slug, ends with "/" **/ + path?: string } export interface Topic { docs: DocMeta[] slug: string - fullSlug?: string + /** Path, not including the slug, ends with "/" **/ + path?: string } From d349aae2f177380531e1849dcfb31fef83ea2b28 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Thu, 2 Nov 2023 21:20:00 +0100 Subject: [PATCH 6/7] feat: support fetching nested docs from GitHub --- scripts/fetch-docs-local.mjs | 2 +- scripts/fetch-docs.mjs | 100 +++++++++++++++++++++++------------ 2 files changed, 68 insertions(+), 34 deletions(-) diff --git a/scripts/fetch-docs-local.mjs b/scripts/fetch-docs-local.mjs index 22258bf98..6c93d6809 100644 --- a/scripts/fetch-docs-local.mjs +++ b/scripts/fetch-docs-local.mjs @@ -88,7 +88,7 @@ export async function parseDocs(fileNames, topicSlugs, topicDirectory) { ) const subTopic = { slug: docFilename, - path: topicSlugs.join('/')+'/', + path: topicSlugs.concat(docFilename).join('/')+'/', docs: subDocs.filter(Boolean).sort((a, b) => a.order - b.order), } return subTopic diff --git a/scripts/fetch-docs.mjs b/scripts/fetch-docs.mjs index a15bb9037..753b65251 100644 --- a/scripts/fetch-docs.mjs +++ b/scripts/fetch-docs.mjs @@ -35,6 +35,7 @@ function slugify(string) { } const githubAPI = 'https://api.github.com/repos/payloadcms/payload' +const branch = 'main' const topicOrder = [ 'Getting-Started', @@ -78,6 +79,66 @@ async function getHeadings(source) { }) } +export async function parseDocs(docFilenames, topicSlugs, topicURL) { + const parsedDocs = await Promise.all( + docFilenames.map(async docFilename => { + try { + const path = `${topicURL}/${docFilename}` + const isDirectory = docFilename.includes('.md') ? false : true + if(isDirectory) { + const subDocs = await fetch(`${path}?ref=${branch}`, { + headers, + }).then(res => res.json()) + const subDocFilenames = subDocs.map(({ name }) => name) + + const parsedSubDocs = await parseDocs( + subDocFilenames, + topicSlugs.concat(docFilename), + path + ) + + const subTopic = { + slug: docFilename, + path: topicSlugs.concat(docFilename).join('/')+'/', + docs: parsedSubDocs.filter(Boolean).sort((a, b) => a.order - b.order), + } + return subTopic + } else { + const json = await fetch(`${path}?ref=${branch}`, { + headers, + }).then(res => res.json()) + + const parsedDoc = matter(decodeBase64(json.content)) + + + const doc = { + content: await serialize(parsedDoc.content, { + mdxOptions: { + remarkPlugins: [remarkGfm], + }, + }), + title: parsedDoc.data.title, + slug: docFilename.replace('.mdx', ''), + path: topicSlugs.join('/')+'/', + label: parsedDoc.data.label, + order: parsedDoc.data.order, + desc: parsedDoc.data.desc || '', + keywords: parsedDoc.data.keywords || '', + headings: await getHeadings(parsedDoc.content), + } + + return doc + } + + } catch (error) { + const msg = error instanceof Error ? error.message : error || 'Unknown error' + console.error(`Error fetching ${docFilename}: ${msg}`) // eslint-disable-line no-console + } + }), + ) + return parsedDocs +} + const fetchDocs = async () => { if (!process.env.GITHUB_ACCESS_TOKEN) { console.log('No GitHub access token found - skipping docs retrieval') // eslint-disable-line no-console @@ -88,46 +149,18 @@ const fetchDocs = async () => { topicOrder.map(async unsanitizedTopicSlug => { const topicSlug = unsanitizedTopicSlug.toLowerCase() - const docs = await fetch(`${githubAPI}/contents/docs/${topicSlug}`, { + const docs = await fetch(`${githubAPI}/contents/docs/${topicSlug}?ref=${branch}`, { headers, }).then(res => res.json()) - const docFilenames = docs.map(({ name }) => name) - const parsedDocs = await Promise.all( - docFilenames.map(async docFilename => { - try { - const json = await fetch(`${githubAPI}/contents/docs/${topicSlug}/${docFilename}`, { - headers, - }).then(res => res.json()) - - const parsedDoc = matter(decodeBase64(json.content)) - - const doc = { - content: await serialize(parsedDoc.content, { - mdxOptions: { - remarkPlugins: [remarkGfm], - }, - }), - title: parsedDoc.data.title, - slug: docFilename.replace('.mdx', ''), - label: parsedDoc.data.label, - order: parsedDoc.data.order, - desc: parsedDoc.data.desc || '', - keywords: parsedDoc.data.keywords || '', - headings: await getHeadings(parsedDoc.content), - } - - return doc - } catch (err) { - const msg = err instanceof Error ? err.message : err || 'Unknown error' - console.error(`Error fetching ${docFilename}: ${msg}`) // eslint-disable-line no-console - } - }), - ) + const topicURL = `${githubAPI}/contents/docs/${topicSlug}` + + const parsedDocs = await parseDocs(docFilenames, [topicSlug], topicURL) const topic = { slug: unsanitizedTopicSlug, + path: '/', docs: parsedDocs.filter(Boolean).sort((a, b) => a.order - b.order), } @@ -135,6 +168,7 @@ const fetchDocs = async () => { }), ) + const data = JSON.stringify(topics, null, 2) const docsFilename = path.resolve(__dirname, './src/app/docs.json') From c4073f42c88118b0ad7d1487057639307c936944 Mon Sep 17 00:00:00 2001 From: Alessio Gravili Date: Thu, 2 Nov 2023 21:34:51 +0100 Subject: [PATCH 7/7] chore: fix build due to type errors --- src/app/(pages)/docs/api.ts | 10 +++++----- src/app/(pages)/docs/types.ts | 30 ++++++++++++++++++++++-------- 2 files changed, 27 insertions(+), 13 deletions(-) diff --git a/src/app/(pages)/docs/api.ts b/src/app/(pages)/docs/api.ts index 456283a7a..57c4905d9 100644 --- a/src/app/(pages)/docs/api.ts +++ b/src/app/(pages)/docs/api.ts @@ -1,5 +1,5 @@ import content from '../../docs.json' -import type { Doc, DocMeta, DocPath, Topic } from './types' +import type { Doc, DocMeta, DocOrTopic, DocPath, Topic } from './types' export async function getTopics(): Promise { return content.map(topic => ({ @@ -10,7 +10,7 @@ export async function getTopics(): Promise { label: doc?.label || '', slug: doc?.slug || '', order: doc?.order || 0, - docs: (doc?.docs as DocMeta[]) || null, + docs: ((doc as any)?.docs as DocMeta[]) || null, path: doc?.path || '/', })), })) @@ -27,14 +27,14 @@ export async function getDoc({ if (!matchedTopic) return null // Recursive function to find a doc by slug within a topic or sub-docs - function findDoc(docs: Doc[], pathAndSlug: string): Doc | null { + function findDoc(docs: DocOrTopic[], pathAndSlug: string): Doc | null { for (const doc of docs) { // Check if the current doc matches the slug - if (doc && (doc.path || '/') + (doc.slug || '/') === pathAndSlug) { + if (doc && (doc.path || '/') + (doc.slug || '/') === pathAndSlug && !('docs' in doc)) { return doc } // If the current doc has subdocs, search within them recursively - if (doc.docs && doc.docs.length > 0) { + if ('docs' in doc && doc?.docs && doc.docs.length > 0) { const subDoc = findDoc(doc.docs, pathAndSlug.toLowerCase()) if (subDoc) { return subDoc diff --git a/src/app/(pages)/docs/types.ts b/src/app/(pages)/docs/types.ts index e13ab2f0b..e15f58290 100644 --- a/src/app/(pages)/docs/types.ts +++ b/src/app/(pages)/docs/types.ts @@ -5,18 +5,32 @@ export interface Heading { } export interface Doc { - content?: any // eslint-disable-line - order?: number - title?: string - label?: string - desc?: string - keywords?: string - headings?: Heading[] - docs?: Doc[] + content: any // eslint-disable-line + order: number + title: string + label: string + desc: string + keywords: string + headings: Heading[] slug?: string path?: string } +export type DocOrTopic = + | Doc + | { + content?: any // eslint-disable-line + order?: number + title?: string + label?: string + desc?: string + keywords?: string + headings?: Heading[] + docs: Doc[] + slug?: string + path?: string + } + export interface NextDoc { slug: string title: string