parent
50b0309590
commit
9492c61955
5 changed files with 172 additions and 42 deletions
@ -0,0 +1,15 @@ |
||||
export function joinPath(...parts: string[]) { |
||||
const separator = '/'; |
||||
|
||||
return parts |
||||
.map((part: string, index: number) => { |
||||
if (index) { |
||||
part = part.replace(new RegExp('^' + separator), ''); |
||||
} |
||||
if (index !== parts.length - 1) { |
||||
part = part.replace(new RegExp(separator + '$'), ''); |
||||
} |
||||
return part; |
||||
}) |
||||
.join(separator); |
||||
} |
@ -0,0 +1,148 @@ |
||||
import { joinPath } from './path'; |
||||
import type { RoadmapFrontmatter } from './roadmap'; |
||||
|
||||
// Generates URL from the topic file path e.g.
|
||||
// -> /src/roadmaps/vue/content/102-ecosystem/102-ssr/101-nuxt-js.md
|
||||
// /vue/ecosystem/ssr/nuxt-js
|
||||
// -> /src/roadmaps/vue/content/102-ecosystem
|
||||
// /vue/ecosystem
|
||||
function generateTopicUrl(filePath: string) { |
||||
return filePath |
||||
.replace('/src/roadmaps/', '/') // Remove the base `/src/roadmaps` from path
|
||||
.replace('/content', '') // Remove the `/[roadmapId]/content`
|
||||
.replace(/\/\d+-/g, '/') // Remove ordering info `/101-ecosystem`
|
||||
.replace(/\/index\.md$/, '') // Make the `/index.md` to become the parent folder only
|
||||
.replace(/\.md$/, ''); // Remove `.md` from the end of file
|
||||
} |
||||
|
||||
/** |
||||
* Generates breadcrumbs for the given topic URL from the given topic file details |
||||
* |
||||
* @param topicUrl Topic URL for which breadcrumbs are required |
||||
* @param topicFiles Topic file mapping to read the topic data from |
||||
*/ |
||||
function generateBreadcrumbs( |
||||
topicUrl: string, |
||||
topicFiles: Record<string, TopicFileType> |
||||
): BreadcrumbItem[] { |
||||
// We need to collect all the pages with permalinks to generate breadcrumbs
|
||||
// e.g. /backend/internet/how-does-internet-work/http
|
||||
// /backend
|
||||
// /backend/internet
|
||||
// /backend/internet/how-does-internet-work
|
||||
// /backend/internet/how-does-internet-work/http
|
||||
|
||||
const urlParts = topicUrl.split('/'); |
||||
const breadcrumbUrls = []; |
||||
const subLinks = []; |
||||
|
||||
for (let counter = 0; counter < urlParts.length; counter++) { |
||||
subLinks.push(urlParts[counter]); |
||||
|
||||
// Skip the following
|
||||
// -> [ '' ]
|
||||
// -> [ '', 'vue' ]
|
||||
if (subLinks.length > 2) { |
||||
breadcrumbUrls.push(subLinks.join('/')); |
||||
} |
||||
} |
||||
|
||||
const breadcrumbs = breadcrumbUrls.map((breadCrumbUrl): BreadcrumbItem => { |
||||
const topicFile = topicFiles[breadCrumbUrl]; |
||||
const topicFileContent = topicFile.file; |
||||
|
||||
const firstHeading = topicFileContent?.getHeadings()?.[0]; |
||||
|
||||
return { title: firstHeading?.text, url: breadCrumbUrl }; |
||||
}); |
||||
|
||||
return breadcrumbs; |
||||
} |
||||
|
||||
type BreadcrumbItem = { |
||||
title: string; |
||||
url: string; |
||||
}; |
||||
|
||||
type FileHeadingType = { |
||||
depth: number; |
||||
slug: string; |
||||
text: string; |
||||
}; |
||||
|
||||
export interface TopicFileContentType { |
||||
frontMatter: Record<string, string>; |
||||
file: string; |
||||
url: string; |
||||
Content: any; |
||||
getHeadings: () => FileHeadingType[]; |
||||
} |
||||
|
||||
export interface TopicFileType { |
||||
url: string; |
||||
file: TopicFileContentType; |
||||
roadmap: RoadmapFrontmatter; |
||||
roadmapId: string; |
||||
breadcrumbs: BreadcrumbItem[]; |
||||
} |
||||
|
||||
export async function getTopicFiles(): Promise<Record<string, TopicFileType>> { |
||||
const contentFiles = await import.meta.glob<string>( |
||||
'/src/roadmaps/*/content/**/*.md', |
||||
{ |
||||
eager: true, |
||||
} |
||||
); |
||||
|
||||
const mapping: Record<string, TopicFileType> = {}; |
||||
|
||||
for (let filePath of Object.keys(contentFiles)) { |
||||
const fileContent: TopicFileContentType = contentFiles[filePath] as any; |
||||
const fileHeadings = fileContent.getHeadings(); |
||||
const firstHeading = fileHeadings[0]; |
||||
|
||||
const [, roadmapId, pathInsideContent] = |
||||
filePath.match(/^\/src\/roadmaps\/(.+)?\/content\/(.+)?$/) || []; |
||||
|
||||
const topicUrl = generateTopicUrl(filePath); |
||||
|
||||
const currentRoadmap = await import( |
||||
`../roadmaps/${roadmapId}/${roadmapId}.md` |
||||
); |
||||
|
||||
mapping[topicUrl] = { |
||||
url: topicUrl, |
||||
file: fileContent, |
||||
roadmap: currentRoadmap.frontmatter, |
||||
roadmapId: roadmapId, |
||||
breadcrumbs: [], |
||||
}; |
||||
} |
||||
|
||||
// Populate breadcrumbs inside the mapping
|
||||
Object.keys(mapping).forEach((topicUrl) => { |
||||
const { |
||||
roadmap: currentRoadmap, |
||||
roadmapId, |
||||
file: currentTopic, |
||||
} = mapping[topicUrl]; |
||||
const roadmapUrl = `/${roadmapId}`; |
||||
|
||||
// Breadcrumbs for the file
|
||||
const breadcrumbs: BreadcrumbItem[] = [ |
||||
{ |
||||
title: currentRoadmap.featuredTitle, |
||||
url: `${roadmapUrl}`, |
||||
}, |
||||
{ |
||||
title: 'Topics', |
||||
url: `${roadmapUrl}/topics`, |
||||
}, |
||||
...generateBreadcrumbs(topicUrl, mapping), |
||||
]; |
||||
|
||||
mapping[topicUrl].breadcrumbs = breadcrumbs; |
||||
}); |
||||
|
||||
return mapping; |
||||
} |
@ -1,27 +1,27 @@ |
||||
--- |
||||
import MarkdownContent from "../components/MarkdownContent/MarkdownContent.astro"; |
||||
import BaseLayout from "../layouts/BaseLayout.astro"; |
||||
import { getTopicPathMapping, TopicFileType } from "../lib/roadmap"; |
||||
import MarkdownContent from '../components/MarkdownContent/MarkdownContent.astro'; |
||||
import BaseLayout from '../layouts/BaseLayout.astro'; |
||||
import { getTopicFiles, TopicFileType } from '../lib/topic'; |
||||
|
||||
export async function getStaticPaths() { |
||||
const topicPathMapping = await getTopicPathMapping(); |
||||
const topicPathMapping = await getTopicFiles(); |
||||
|
||||
// console.log(topicPathMapping); |
||||
|
||||
return Object.keys(topicPathMapping).map((topicSlug) => ({ |
||||
params: { topicId: topicSlug }, |
||||
params: { topicId: topicSlug.replace(/^\//, '') }, |
||||
props: topicPathMapping[topicSlug], |
||||
})); |
||||
} |
||||
|
||||
const { topicId } = Astro.params; |
||||
const props: TopicFileType = Astro.props as any; |
||||
const { file } = Astro.props as TopicFileType; |
||||
--- |
||||
|
||||
<BaseLayout title="What is this"> |
||||
<MarkdownContent> |
||||
<main id="main-content"> |
||||
<props.Content /> |
||||
<file.Content /> |
||||
</main> |
||||
</MarkdownContent> |
||||
</BaseLayout> |
||||
|
Loading…
Reference in new issue