Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 33 additions & 6 deletions docs/scripts/sync-workshop-content.js
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,20 @@ function loadLocalWorkshopEntries(sourceDir) {
}));
}

function parseFrontmatter(body) {
const match = body.match(/^---\r?\n([\s\S]*?)\r?\n---\r?\n?/u);
if (!match) return { frontmatter: {}, body };
const yaml = match[1];
const frontmatter = /** @type {Record<string, string>} */ ({});
for (const line of yaml.split('\n')) {
const colonIdx = line.indexOf(':');
if (colonIdx > 0) {
frontmatter[line.slice(0, colonIdx).trim()] = line.slice(colonIdx + 1).trim();
}
}
return { frontmatter, body: body.slice(match[0].length) };
}

function stripMarkdown(value) {
return String(value)
.replace(/!\[([^\]]*)\]\([^)]+\)/gu, '$1')
Expand Down Expand Up @@ -117,11 +131,21 @@ function extractSummary(body) {
}

function addEntryMetadata(entries) {
return entries.map((entry) => ({
...entry,
title: extractTitle(entry.body, entry.id),
summary: extractSummary(entry.body),
}));
return entries.map((entry) => {
const { frontmatter, body } = parseFrontmatter(entry.body);
// Frontmatter is present in fresh remote entries. Cached entries (loaded from the
// generated file on a transient fetch failure) have already had their frontmatter
// stripped, so parseFrontmatter returns empty frontmatter — fall back to the
// existing field value on the entry before falling back to the default.
return {
...entry,
body,
journey: frontmatter['journey'] || entry.journey || 'all',
adventure: frontmatter['adventure'] || entry.adventure || 'core',
title: extractTitle(body, entry.id),
summary: extractSummary(body),
};
});
}

function rewriteWorkshopMarkdownForAstro(body, rawBaseUrl = publicWorkshopRawBaseUrl) {
Expand Down Expand Up @@ -210,7 +234,8 @@ rmSync(markdownOutputDir, { recursive: true, force: true });
mkdirSync(markdownOutputDir, { recursive: true });

for (const entry of entries) {
writeFileSync(join(markdownOutputDir, entry.id), rewriteWorkshopMarkdownForAstro(entry.body, effectiveRawBaseUrl), 'utf8');
const { body } = parseFrontmatter(entry.body);
writeFileSync(join(markdownOutputDir, entry.id), rewriteWorkshopMarkdownForAstro(body, effectiveRawBaseUrl), 'utf8');
}

const output = `${[
Expand All @@ -228,6 +253,8 @@ const output = `${[
'',
'export type WorkshopContentEntry = {',
"\tid: string;",
"\tjourney: string;",
"\tadventure: string;",
"\ttitle: string;",
"\tsummary: string;",
"\tbody: string;",
Expand Down
66 changes: 50 additions & 16 deletions docs/src/components/workshop/WorkshopExperience.astro
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,15 @@ import {
workshopDefaults,
workshopEntryPaths,
workshopJourneys,
workshopRoutes,
workshopScenarioAdventures,
workshopScenarios,
} from '../../lib/workshop/manifest';
import { workshopContent, workshopSource, type WorkshopContentEntry } from '../../generated/workshop-content.ts';

type WorkshopEntry = {
id: string;
journey: string;
adventure: string;
title: string;
summary: string;
body: string;
Expand All @@ -21,6 +23,8 @@ type WorkshopEntry = {
type WorkshopStep = {
key: string;
file: string;
journey: string;
adventure: string;
title: string;
summary: string;
githubUrl: string;
Expand Down Expand Up @@ -330,6 +334,8 @@ function normalizeKey(value: string) {

const workshopEntries: WorkshopEntry[] = workshopContent.map((entry: WorkshopContentEntry) => ({
id: entry.id,
journey: entry.journey,
adventure: entry.adventure,
title: entry.title,
summary: entry.summary,
body: entry.body,
Expand All @@ -341,6 +347,8 @@ const workshopSteps: WorkshopStep[] = await Promise.all(workshopEntries.map(asyn
return {
key,
file: `${key}.md`,
journey: entry.journey,
adventure: entry.adventure,
title: entry.title,
summary: entry.summary,
githubUrl: new URL(`${key}.md`, workshopGithubBase).toString(),
Expand Down Expand Up @@ -509,6 +517,8 @@ const initialStep = workshopSteps.find((item) => item.key === initialStepKey) ??
set:html={safeJson(workshopSteps.map((step) => ({
key: step.key,
file: step.file,
journey: step.journey,
adventure: step.adventure,
title: step.title,
summary: step.summary,
githubUrl: step.githubUrl,
Expand All @@ -524,8 +534,8 @@ const initialStep = workshopSteps.find((item) => item.key === initialStepKey) ??
defaults: workshopDefaults,
entryPaths: workshopEntryPaths,
journeys: workshopJourneys,
routes: workshopRoutes,
scenarios: workshopScenarios,
scenarioAdventures: workshopScenarioAdventures,
})}
/>
<script is:inline>
Expand Down Expand Up @@ -553,20 +563,44 @@ const initialStep = workshopSteps.find((item) => item.key === initialStepKey) ??

const buildFlow = (journeyId, scenarioId) => {
const journey = manifest.journeys.find((item) => item.id === journeyId) || manifest.journeys[0];
const scenario = manifest.scenarios.find((item) => item.id === scenarioId) || manifest.scenarios[0];
const journeyRoute = manifest.routes.workspaces[journey.id];
const scenarioRoute = manifest.routes.scenarios[scenario.id];
const ordered = [
...journeyRoute.prelude,
scenarioRoute.designStep,
scenarioRoute.buildStepByWorkspace[journey.id],
...journeyRoute.postBuild,
...(manifest.routes.preSchedule || []),
journeyRoute.scheduleStep,
...manifest.routes.wrapUp,
];

return [...new Set(ordered)].filter((stepKey) => stepMap.has(stepKey));
const contentJourneyIds = journey.contentJourneyIds || [];
const scenarioAdventure = (manifest.scenarioAdventures || {})[scenarioId] || '';
const isCopilot = contentJourneyIds.includes('copilot');

const includedAdventures = new Set(['core', 'setup', 'advanced']);
if (scenarioAdventure) includedAdventures.add(scenarioAdventure);
if (isCopilot) includedAdventures.add('scenario-d');

const candidates = steps.filter((step) => {
if (isCopilot && step.journey === 'ui' && !['core', 'setup', 'advanced', 'scenario-d'].includes(step.adventure)) {
return false;
}
const journeyMatch = step.journey === 'all' || contentJourneyIds.includes(step.journey);
return journeyMatch && includedAdventures.has(step.adventure) && step.adventure !== 'side-quest';
Comment on lines +574 to +579
});

const hubPrefixes = new Set(
steps
.filter((s) => contentJourneyIds.includes(s.journey))
.map((s) => s.key.split('-')[0]),
);

return candidates
.filter((step) => {
if (step.journey !== 'all') return true;
const keyPrefix = step.key.split('-')[0];
// Case 1: exact prefix match (e.g. '11a' hub when '11a-build-*-ui' exists for this journey).
if (hubPrefixes.has(keyPrefix)) return false;
// Case 2: numeric-only prefix (e.g. '06'): hub if letter-variant specific entries exist ('06a', '06b').
const numericOnly = keyPrefix.match(/^(\d+)$/);
if (numericOnly) {
const hubRe = new RegExp(`^${numericOnly[1]}[a-z]`);
return ![...hubPrefixes].some((p) => hubRe.test(p));
}
return true;
})
.map((step) => step.key)
.filter((stepKey) => stepKey !== 'README' && stepMap.has(stepKey));
};

const parseHash = () => {
Expand Down
Loading
Loading