-
Notifications
You must be signed in to change notification settings - Fork 93
docs: implement automated tag-based related labs system #777
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
205c4b8
0105d8c
cd6d13f
9f38b30
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,5 +1,11 @@ | ||
| --- | ||
| title: Architecture | ||
| title: Architecture Overview | ||
| sidebar_label: Architecture | ||
| tags: | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the zh copies of these six pages get no tags, so the feature silently does not exist in zh. also note docs tags render visible tag chips and generate /docs/tags pages, confirm that side effect is wanted. |
||
| - installation | ||
| - nvidia | ||
| - hami | ||
| - local-setup | ||
| --- | ||
|
|
||
| The overall architecture of HAMi is shown as below: | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -25,6 +25,55 @@ function getDocEditUrl(versionDocsDirPath, docPath) { | |
| return `${githubEditBaseUrl}${[versionDocsDirPath, docPath].filter(Boolean).join("/")}`; | ||
| } | ||
|
|
||
| /** | ||
| * Build-time helper: scans tutorials/labs/*.md and extracts the metadata | ||
| * that the RelatedLabs component needs at runtime. This avoids the need | ||
| * to cross-reference two separate docs-plugin instances on the client. | ||
| */ | ||
| function getLabData() { | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. hand rolled frontmatter regex is fragile, level: also matches toc_max_heading_level on unlucky ordering. gray-matter ships with docusaurus, use it. |
||
| const fs = require("fs"); | ||
| const path = require("path"); | ||
| const labsDir = path.join(__dirname, "tutorials", "labs"); | ||
| if (!fs.existsSync(labsDir)) return {}; | ||
| const files = fs.readdirSync(labsDir).filter((f) => f.endsWith(".md")); | ||
| const labs = {}; | ||
| for (const file of files) { | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
| const raw = fs.readFileSync(path.join(labsDir, file), "utf8"); | ||
| // Normalise CRLF → LF so the regex works on every OS | ||
| const content = raw.replace(/\r\n/g, "\n"); | ||
| const fmMatch = content.match(/^---\n([\s\S]*?)\n---/); | ||
| if (!fmMatch) continue; | ||
| const fm = fmMatch[1]; | ||
|
|
||
| const titleMatch = fm.match(/^title:\s*"?(.+?)"?\s*$/m); | ||
| const descMatch = fm.match(/^description:\s*"?(.+?)"?\s*$/m); | ||
| const levelMatch = fm.match(/level:\s*(.+)/); | ||
| const durationMatch = fm.match(/duration:\s*(.+)/); | ||
| const tagsMatch = fm.match(/^tags:\s*\n((?:\s+-\s+.*\n?)+)/m); | ||
|
|
||
| const tags = tagsMatch | ||
| ? tagsMatch[1] | ||
| .split("\n") | ||
| .filter((l) => l.trim().startsWith("-")) | ||
| .map((l) => l.replace(/^\s*-\s*/, "").trim()) | ||
| .filter(Boolean) | ||
| : []; | ||
|
|
||
| if (tags.length === 0) continue; | ||
|
|
||
| const docId = `labs/${file.replace(".md", "")}`; | ||
| labs[docId] = { | ||
| title: titleMatch ? titleMatch[1] : file.replace(".md", ""), | ||
| description: descMatch ? descMatch[1] : "", | ||
| level: levelMatch ? levelMatch[1].trim() : "", | ||
| duration: durationMatch ? durationMatch[1].trim() : "", | ||
| tags, | ||
| href: `/tutorials/${docId}`, | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. href is not locale prefixed, zh concept pages will link to the english labs. build the url with the active locale, same issue i flagged on #774. |
||
| }; | ||
| } | ||
| return labs; | ||
| } | ||
|
|
||
| async function localizedBlogPlugin(context, opts) { | ||
| const p = await require("@docusaurus/plugin-content-blog").default(context, opts); | ||
| const orig = p.postBuild?.bind(p); | ||
|
|
@@ -71,6 +120,7 @@ module.exports = { | |
| }, | ||
| customFields: { | ||
| defaultOgImage: "/img/hami-graph-color.png", | ||
| labData: getLabData(), | ||
| }, | ||
| markdown: { | ||
| mermaid: true, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,96 @@ | ||
| /** | ||
| * RelatedLabs – tag-based, cross-plugin related-labs section. | ||
| * | ||
| * Concept pages (docs plugin) and lab pages (tutorials plugin) live in | ||
| * two separate Docusaurus docs-plugin instances, so we cannot use | ||
| * useDocsSidebar/useDocsVersion to reach across the boundary. | ||
| * | ||
| * Instead, the build-time helper `getLabData()` in docusaurus.config.js | ||
| * extracts every lab's metadata (title, description, level, duration, | ||
| * tags, href) and injects it into `siteConfig.customFields.labData`. | ||
| * This component reads that static map and matches tags at render time. | ||
| */ | ||
| import React from "react"; | ||
| import Link from "@docusaurus/Link"; | ||
| import Translate from "@docusaurus/Translate"; | ||
| import { useDoc } from "@docusaurus/plugin-content-docs/client"; | ||
| import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; | ||
| import LevelBadge from "./LevelBadge"; | ||
| import styles from "./RelatedLabs.module.css"; | ||
| import gridStyles from "./LabCardGrid.module.css"; | ||
|
|
||
| import { DURATIONS } from "./LabCardGridAuto"; | ||
|
|
||
| /** | ||
| * Renders a "Related Hands-on Labs" card grid at the bottom of doc pages. | ||
| * | ||
| * Reads the current page's frontmatter `tags` and matches them against | ||
| * pre-extracted lab metadata from `siteConfig.customFields.labData`. | ||
| * Labs with the most overlapping tags appear first. If the page has no | ||
| * tags or no labs match, the component renders nothing. | ||
| * | ||
| * @returns {React.ReactElement|null} A styled card grid of related labs, or null. | ||
| */ | ||
| export default function RelatedLabs() { | ||
| const { frontMatter } = useDoc(); | ||
| const { siteConfig } = useDocusaurusContext(); | ||
| const pageTags = frontMatter?.tags ?? []; | ||
|
|
||
| // Nothing to match against – render nothing. | ||
| if (pageTags.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| const labData = siteConfig.customFields?.labData ?? {}; | ||
|
coderabbitai[bot] marked this conversation as resolved.
|
||
|
|
||
| // Build cards for labs whose tags overlap with the current page's tags. | ||
| const matchedCards = Object.entries(labData) | ||
| .map(([docId, lab]) => { | ||
| const labTags = lab.tags ?? []; | ||
| const matchCount = labTags.filter((tag) => pageTags.includes(tag)).length; | ||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. the theme is shared across both docs plugin instances, so lab pages themselves render this section and match their own tags, including a card linking to the page you are already on. exclude the current doc id, or skip rendering inside the tutorials instance. |
||
| if (matchCount === 0) return null; | ||
| return { | ||
| key: docId, | ||
| href: lab.href, | ||
| title: lab.title, | ||
| description: lab.description, | ||
| level: lab.level, | ||
| duration: lab.duration, | ||
| matchCount, | ||
| }; | ||
| }) | ||
| .filter(Boolean) | ||
| .sort((a, b) => b.matchCount - a.matchCount || a.key.localeCompare(b.key)) | ||
| .slice(0, 4); // Cap to top 4 to avoid long lists of loosely related labs | ||
|
|
||
| if (matchedCards.length === 0) { | ||
| return null; | ||
| } | ||
|
|
||
| return ( | ||
| <div className={styles.container}> | ||
| <h3 className={styles.heading}> | ||
| <span className={styles.icon}>🔬</span> | ||
| <Translate id="theme.docs.relatedLabs.title" description="Title for related labs section"> | ||
| Related Hands-on Labs | ||
| </Translate> | ||
| </h3> | ||
| <div className={gridStyles.grid}> | ||
| {matchedCards.map((card) => ( | ||
| <Link key={card.key} to={card.href} className={gridStyles.card}> | ||
| <div className={gridStyles.cardHeader}> | ||
| <span className={gridStyles.cardTitle}>{card.title}</span> | ||
| <LevelBadge level={card.level} /> | ||
| </div> | ||
| {card.description && <p className={gridStyles.cardDescription}>{card.description}</p>} | ||
| {card.duration && ( | ||
| <div className={gridStyles.cardFooter}> | ||
| {DURATIONS[card.duration] ?? card.duration} | ||
| </div> | ||
| )} | ||
| </Link> | ||
| ))} | ||
| </div> | ||
| </div> | ||
| ); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,27 @@ | ||
| .container { | ||
| margin: 3rem 0; | ||
| padding: 1.5rem; | ||
| background-color: var(--ifm-color-emphasis-100); | ||
| border-radius: var(--ifm-global-radius); | ||
| border-left: 4px solid var(--ifm-color-primary); | ||
| } | ||
|
|
||
| .heading { | ||
| display: flex; | ||
| align-items: center; | ||
| gap: 0.5rem; | ||
| margin-top: 0; | ||
| margin-bottom: 1rem; | ||
| font-size: 1.25rem; | ||
| color: var(--ifm-color-emphasis-900); | ||
| } | ||
|
|
||
| .icon { | ||
| font-size: 1.5rem; | ||
| } | ||
|
|
||
| /* Ensure grid overrides its top margin when inside container */ | ||
| .container > div { | ||
| margin-top: 0; | ||
| margin-bottom: 0; | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
four page titles are renamed in this pr with no stated reason, and the zh titles now diverge. revert or explain.