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
24 changes: 24 additions & 0 deletions .github/workflows/content-ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -91,6 +91,30 @@ jobs:
fi
echo "All touched artifacts updated their CHANGELOG.md."

# Validate every lesson MDX body actually compiles as MDX/JSX (mirrors the
# local `mdx-syntax` Lefthook hook). Catches parser-desyncing authoring
# mistakes — most commonly a backslash-escaped double quote inside a plain
# JSX attribute (e.g. note="...\"...") — which JSX doesn't treat as an
# escape. Without this, a malformed lesson lands on main clean and only
# fails later when apps/web runs Velite, which silently drops it from the
# catalog (404 at runtime) instead of failing the build. See PR #9
# (tailwind-from-scratch lessons 22 + 23).
validate-mdx:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4

- name: Setup Node.js
uses: actions/setup-node@v4
with:
node-version: '20'

- name: Install MDX compiler
run: npm install --no-save @mdx-js/mdx remark-directive

- name: Validate lesson MDX syntax
run: node scripts/check-mdx-syntax.mjs

# Validate code samples can at least be parsed
validate-code:
runs-on: ubuntu-latest
Expand Down
3 changes: 3 additions & 0 deletions courses/tailwind-from-scratch/CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,9 @@ copy are recorded under their own dated heading.
### Changed
- Removed `test_only: true` from `course.yaml`. User decision: it's fine to promote the course to production now — visibility will be gated via the admin portal instead of keeping the whole course out of the catalog. `code/m3-resort-brava` and the microlearn units remain outstanding and can be added in a follow-up publish without re-gating.

### Fixed
- Lessons `22-modu-sukuru` and `23-tailwind-cli-v4`: fixed a `<CodeDiff note="...">` attribute that embedded a backslash-escaped double quote (`\"`), which JSX plain-attribute strings don't support as an escape — it desynced the tag parser and Velite dropped both lessons from the catalog (404 at runtime). Swapped the inner quotes to single quotes; no meaning change. See deznode/skola-content#9.

## [2026-07-07] — publish

### Added
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ Na v3, bu konfigura dark mode na `tailwind.config.js`. Na v4 kel linha sumiu —
lang="css"
filename="di v3 config pa v4 @custom-variant"
title="Dark mode: di v3 pa v4"
note="v3 ta pega `darkMode: 'class'` na JavaScript config; v4 ta uza `@custom-variant` na bu blok `<style type=\"text/tailwindcss\">`."
note="v3 ta pega `darkMode: 'class'` na JavaScript config; v4 ta uza `@custom-variant` na bu blok `<style type='text/tailwindcss'>`."
diff={[
{ type: "del", t: "/* v3 — ka ta funsiona na v4 */" },
{ type: "del", t: "module.exports = {" },
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,7 +51,7 @@ Si bo seguir tutoriais v3 antigu, bo ta odja outru workflow. **Kuazi tudu muda n
lang="css"
filename="input.css"
title="Di trez `@tailwind` pa un `@import`"
note="Un só `@import \"tailwindcss\";` ta inkluí base, komponenti i utilidadis — ka ten más trez direktivas separadu."
note="Un só `@import 'tailwindcss';` ta inkluí base, komponenti i utilidadis — ka ten más trez direktivas separadu."
diff={[
{ type: "del", t: '/* v3 — kaduku na v4 */' },
{ type: "del", t: '@tailwind base;' },
Expand Down
83 changes: 83 additions & 0 deletions scripts/check-mdx-syntax.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
#!/usr/bin/env node
// check-mdx-syntax.mjs — fail if any course lesson .mdx doesn't compile as MDX/JSX.
//
// Mirrors the existing `validate-code` job's py_compile/mvn-compile parse checks
// (content-ci.yml), but for lesson MDX. Catches parser-desyncing authoring
// mistakes — most commonly a backslash-escaped double quote inside a plain JSX
// attribute (e.g. `note="...\"..."`), which JSX does NOT treat as an escape: the
// `\` is literal, so the following `"` ends the attribute early and desyncs the
// rest of the tag.
//
// Without this check a malformed lesson still lands on `main` clean, and only
// fails later when apps/web runs Velite — which silently drops the lesson from
// the catalog (404 at runtime) instead of failing the build. See PR #9
// (tailwind-from-scratch lessons 22 + 23).
//
// Usage: node scripts/check-mdx-syntax.mjs
// Requires @mdx-js/mdx + remark-directive to be installed (CI installs them
// transiently; this repo has no package.json of its own).
//
// remarkDirective MUST be passed to compile() — it's the same plugin
// apps/web's Velite config wires in (see apps/web/velite.config.ts). Without
// it, a plain compile() misparses legitimate `:::callout{type=tip}` directive
// blocks (the `{type=tip}` attribute shorthand isn't valid bare MDX/JS) and
// throws a false positive on content Velite compiles just fine.
import { compile } from "@mdx-js/mdx";
import remarkDirective from "remark-directive";
import { readFile, readdir } from "node:fs/promises";
import path from "node:path";

function stripFrontmatter(content) {
return content.replace(/^---\n[\s\S]*?\n---\n?/, "");
}

// Portable recursive walk for lesson MDX under courses/<slug>/lessons/<NN-slug>/*.mdx.
// Avoids fs.promises.glob (Node 22+ only) so this runs on the Node 20 CI pins elsewhere.
async function* findLessonMdx(root) {
let coursesEntries;
try {
coursesEntries = await readdir(path.join(root, "courses"), { withFileTypes: true });
} catch {
return;
}
for (const course of coursesEntries) {
if (!course.isDirectory()) continue;
const lessonsDir = path.join(root, "courses", course.name, "lessons");
let lessonDirs;
try {
lessonDirs = await readdir(lessonsDir, { withFileTypes: true });
} catch {
continue;
}
for (const lesson of lessonDirs) {
if (!lesson.isDirectory()) continue;
const lessonPath = path.join(lessonsDir, lesson.name);
const files = await readdir(lessonPath, { withFileTypes: true });
for (const file of files) {
if (file.isFile() && file.name.endsWith(".mdx")) {
yield path.join(lessonPath, file.name);
}
}
}
}
}

let failures = 0;

for await (const file of findLessonMdx(process.cwd())) {
const raw = await readFile(file, "utf8");
const body = stripFrontmatter(raw);
try {
await compile(body, { jsx: true, remarkPlugins: [remarkDirective] });
} catch (e) {
failures++;
console.error(`✖ ${file}`);
console.error(` ${e.message.split("\n")[0]}`);
}
}

if (failures > 0) {
console.error(`\n${failures} lesson(s) failed MDX syntax validation.`);
process.exit(1);
}
console.log("All lesson MDX files compile cleanly.");
Loading