Skip to content

Commit 2cd042a

Browse files
authored
Merge pull request #181 from CodexRaunak/validation
Add build time validation and instructor tool kit page
2 parents 495fdd9 + 3289b75 commit 2cd042a

20 files changed

Lines changed: 779 additions & 7 deletions

.gitignore

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ resources/
2424

2525
#hugo
2626
.hugo_build.lock
27-
content/
27+
content/*
28+
!content/instructor-toolkit.md
2829

2930
.DS_Store

README.md

Lines changed: 89 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,95 @@ In addition to this core structure, the following content types are available:
2525
final-test)
2626
- **Certification** - Certification programs
2727

28+
### Where Content Should Live
29+
30+
Keep publishable Academy content and any org-specific supporting files inside
31+
the organization directory. Files outside the org directory are not part of
32+
the publishable Academy content tree. Validation catches these cases, and the
33+
files may be skipped during publication or change shared site behavior in
34+
unexpected ways.
35+
36+
Use this general layout:
37+
38+
```text
39+
content/
40+
learning-paths/
41+
<org-id>/
42+
_index.md
43+
course-1/
44+
_index.md
45+
module-1/
46+
_index.md
47+
certifications/
48+
<org-id>/
49+
_index.md
50+
challenges/
51+
<org-id>/
52+
_index.md
53+
layouts/
54+
shortcodes/
55+
<org-id>/
56+
*.html
57+
static/
58+
<org-id>/
59+
...
60+
data/
61+
<org-id>/
62+
...
63+
```
64+
65+
Put org-specific content, shortcodes, static files, and data under the org
66+
directory. If you place them elsewhere, the Academy may not publish them and
67+
the build can emit warnings or break shared assumptions for other content.
68+
69+
Shared theme assets, icons, and reusable partials should stay in
70+
academy-theme itself rather than in a consuming org repository.
71+
72+
## ID Validation
73+
74+
The theme checks publishable root Academy content during Hugo builds and emits
75+
warnings by default. This is intentionally warning-first so authors can keep
76+
working locally while still seeing what needs to be fixed before publication.
77+
78+
Configure the shared registry URL in the theme, and set the Instructor Console
79+
URL in each content repo:
80+
81+
```yaml
82+
params:
83+
academy:
84+
registryURL: "<shared-academy-registry-api-url>"
85+
consoleURL: "<per-repo-instructor-console-url>"
86+
validationMode: warn
87+
```
88+
89+
The registry URL is the same for all Academy sites. The console URL varies by
90+
domain, so each content repo should set its own value. For example, Exoscale
91+
uses `https://exoscale.layer5.io/academy/instructors-console`, while Meshery
92+
uses `https://cloud.meshery.io/academy/instructors-console`.
93+
94+
Supported `validationMode` values:
95+
96+
- `warn` keeps builds running and prints actionable warnings.
97+
- `error` fails only for broken publishable content, for stricter CI use.
98+
- `off` suppresses content health warnings and skips registry lookups.
99+
100+
Registry lookups are scoped to root learning paths, challenges, and
101+
certifications. Draft root content is not warning-producing and is hidden from
102+
the Instructor Toolkit report. Nested course/module/page/test IDs are not
103+
checked against the Cloud curriculum registry.
104+
105+
If `params.academy.registryURL` is not configured, the build prints one
106+
configuration warning and does not report existing IDs as invalid because they
107+
have not been checked against the registry.
108+
109+
The registry response is cached once per org and registry URL in Hugo's site
110+
store during a build. The remote response uses a cache key scoped to the current
111+
Hugo process, so incremental rebuilds reuse the same response. Rebuild the
112+
site completely to refresh the cached registry API response for fresh ID
113+
lookups.
114+
115+
The Instructor Toolkit surfaces the same build-generated report.
116+
28117
## Important Notes
29118

30119
⚠️ **Deprecated Features**

assets/scss/_styles_project.scss

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -290,6 +290,19 @@ a:not([href]):not([class]):hover {
290290
background-color: rgba($primary, 0.1);
291291
color: dark;
292292
}
293+
294+
&.academy-sidebar-toolkit {
295+
color: $dark;
296+
font-weight: $font-weight-bold;
297+
}
298+
299+
&.active.academy-sidebar-toolkit {
300+
border: 2px solid $lightslategray !important;
301+
padding: 0.25rem;
302+
padding-left: 0.5rem !important;
303+
background-color: rgba($primary, 0.1);
304+
color: $dark;
305+
}
293306
}
294307
}
295308

content/instructor-toolkit.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
---
2+
title: "Instructor Toolkit"
3+
draft: false
4+
---
5+
6+
{{< instructor-toolkit >}}

data/hextra/icons.yaml

Lines changed: 55 additions & 0 deletions
Large diffs are not rendered by default.

hugo.yaml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,10 @@ params:
8484
# set taxonomyPageHeader = [] to hide taxonomies on the page headers
8585
taxonomyPageHeader: [tags, categories]
8686

87+
academy:
88+
domain: cloud.meshery.io
89+
registryURL: https://cloud.meshery.io/api/academy/curricula
90+
8791
privacy_policy: https://policies.google.com/privacy
8892

8993
# First one is picked as the Twitter card image if not set on page.

layouts/baseof.html

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
</head>
77

88
<body class="td-{{ .Kind }}{{ with .Page.Params.body_class }} {{ . }}{{ end }}">
9+
{{ partial "academy-validation/validate-academy-build.html" . }}
910
<div class="container-fluid td-outer">
1011
<div class="td-main">
1112
<div class="row flex-xl-nowrap">

layouts/list.html

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
{{ define "main" }}
2+
{{ partial "academy-validation/validate-learning-path-structure.html" (dict "page" .) }}
23
<div class="td-content">
34
<h1>{{ .Title }}</h1>
45
{{ with .Params.description }}<div class="lead">{{ . | markdownify }}</div>{{ end }}
@@ -25,4 +26,4 @@ <h1>{{ .Title }}</h1>
2526
{{ partial "pager.html" . }}
2627
{{ partial "page-meta-lastmod.html" . -}}
2728
</div>
28-
{{ end -}}
29+
{{ end -}}
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
{{- $site := .site -}}
2+
{{- $cacheKey := "academy-theme-content-health-report" -}}
3+
{{- if not ($site.Store.Get $cacheKey) -}}
4+
5+
{{- $registryURL := "" -}}
6+
{{- with $site.Params.academy.registryURL -}}
7+
{{- $registryURL = printf "%v" . | strings.TrimSpace -}}
8+
{{- end -}}
9+
10+
{{- $consoleURL := "" -}}
11+
{{- with $site.Params.academy.consoleURL -}}
12+
{{- $consoleURL = printf "%v" . | strings.TrimSpace -}}
13+
{{- end -}}
14+
{{- if eq $consoleURL "" -}}
15+
{{- $consoleURL = "/academy/instructors-console" -}}
16+
{{- end -}}
17+
{{- $academyBaseURL := "" -}}
18+
{{- with $site.Params.academy.domain -}}
19+
{{- $academyBaseURL = printf "%v" . | strings.TrimSpace -}}
20+
{{- else -}}
21+
{{- with $site.Params.academy.baseURL -}}
22+
{{- $academyBaseURL = printf "%v" . | strings.TrimSpace -}}
23+
{{- else -}}
24+
{{- with $site.Params.academy.consoleURL -}}
25+
{{- $academyBaseURL = printf "%v" . | strings.TrimSpace -}}
26+
{{- end -}}
27+
{{- end -}}
28+
{{- end -}}
29+
{{- if and (ne $academyBaseURL "") (not (hasPrefix $academyBaseURL "http://")) (not (hasPrefix $academyBaseURL "https://")) -}}
30+
{{- $academyBaseURL = printf "https://%s" $academyBaseURL -}}
31+
{{- end -}}
32+
{{- $academyBaseURL = replaceRE `/academy/.*$` "" $academyBaseURL -}}
33+
{{- $academyBaseURL = strings.TrimSuffix "/" $academyBaseURL -}}
34+
{{- if and (ne $academyBaseURL "") (ne $consoleURL "") (not (hasPrefix $consoleURL "http://")) (not (hasPrefix $consoleURL "https://")) -}}
35+
{{- $consoleURL = printf "%s/academy/instructors-console" $academyBaseURL -}}
36+
{{- end -}}
37+
38+
{{- $validationMode := "warn" -}}
39+
{{- with $site.Params.academy.validationMode -}}
40+
{{- $validationMode = printf "%v" . | strings.TrimSpace | lower -}}
41+
{{- end -}}
42+
{{- $registryValidationEnabled := ne $validationMode "off" -}}
43+
44+
{{- $items := slice -}}
45+
{{- $good := 0 -}}
46+
{{- $broken := 0 -}}
47+
{{- $draft := 0 -}}
48+
{{- $draftWarning := 0 -}}
49+
{{- $unchecked := 0 -}}
50+
51+
{{- range $site.Pages -}}
52+
{{- if and .File (partial "academy-validation/is-root-content.html" .) -}}
53+
{{- $filePath := .File.Path -}}
54+
{{- $parts := split $filePath "/" -}}
55+
{{- $orgID := cond (ge (len $parts) 2) (index $parts 1) "" -}}
56+
{{- $contentID := "" -}}
57+
{{- with .Params.id -}}
58+
{{- $contentID = printf "%v" . | strings.TrimSpace -}}
59+
{{- end -}}
60+
61+
{{- $title := .Title -}}
62+
{{- if not $title -}}
63+
{{- $title = replaceRE `[_-]+` " " (replaceRE `\.[^.]+$` "" (path.Base $filePath)) | title -}}
64+
{{- end -}}
65+
66+
{{- $contentType := replace (index $parts 0) "-" " " | title -}}
67+
{{- $status := "good" -}}
68+
{{- $issue := "" -}}
69+
{{- $fix := "" -}}
70+
{{- $registryStatus := "" -}}
71+
{{- $isDraft := or .Draft (eq (printf "%v" .Params.draft | lower) "true") -}}
72+
{{- if not $isDraft -}}
73+
{{- $sourcePath := printf "content/%s" $filePath -}}
74+
{{- $source := try (os.ReadFile $sourcePath) -}}
75+
{{- if not $source.Err -}}
76+
{{- $frontMatter := index (findRE `(?s)^---\s.*?\s---` $source.Value 1) 0 | default "" -}}
77+
{{- if gt (len (findRE `(?m)^draft:\s*true\s*$` $frontMatter 1)) 0 -}}
78+
{{- $isDraft = true -}}
79+
{{- end -}}
80+
{{- end -}}
81+
{{- end -}}
82+
83+
{{- if $isDraft -}}
84+
{{- $draft = add $draft 1 -}}
85+
{{- end -}}
86+
87+
{{- if not (partial "academy-validation/validate-academy-org-id.html" $orgID) -}}
88+
{{- $status = "broken" -}}
89+
{{- $issue = "Content is outside a valid organization directory." -}}
90+
{{- $fix = "Move this root content under content/<type>/<org-id>/<slug>/." -}}
91+
{{- else if eq $contentID "" -}}
92+
{{- $status = "broken" -}}
93+
{{- $issue = "Missing Content ID." -}}
94+
{{- $fix = "Create or register this content in the Instructor Console, then add the returned ID to front matter." -}}
95+
{{- else if not $registryValidationEnabled -}}
96+
{{- $status = "unchecked" -}}
97+
{{- $issue = "Content health validation is turned off." -}}
98+
{{- $fix = "Enable Academy content health validation before publishing." -}}
99+
{{- else -}}
100+
{{- $registryCacheKey := printf "academy-theme-registry-ids-%s-%s" $orgID (md5 $registryURL) -}}
101+
{{- partial "academy-validation/validate-academy-registry-ids.html" (dict "site" $site "orgID" $orgID "registryURL" $registryURL) -}}
102+
{{- $registry := $site.Store.Get $registryCacheKey -}}
103+
{{- $registryStatus = $registry.status -}}
104+
{{- if ne $registry.status "ok" -}}
105+
{{- $status = "unchecked" -}}
106+
{{- $issue = $registry.message -}}
107+
{{- $fix = "Configure params.academy.registryURL to check this ID against the Academy registry." -}}
108+
{{- else if not (in $registry.ids $contentID) -}}
109+
{{- $status = "broken" -}}
110+
{{- $issue = printf "Content ID '%s' does not exist in the Academy registry for org '%s'." $contentID $orgID -}}
111+
{{- $fix = "Use the registered ID from the Instructor Console, or create/register this content before publishing." -}}
112+
{{- end -}}
113+
{{- end -}}
114+
115+
{{- if and $isDraft (ne $status "good") -}}
116+
{{- $status = "draft_warning" -}}
117+
{{- if eq $issue "" -}}
118+
{{- $issue = "Draft content could not be fully validated." -}}
119+
{{- end -}}
120+
{{- $fix = "This draft will not block publishing, but fix the ID before removing draft mode." -}}
121+
{{- end -}}
122+
123+
{{- if eq $status "good" -}}
124+
{{- $good = add $good 1 -}}
125+
{{- else if eq $status "broken" -}}
126+
{{- $broken = add $broken 1 -}}
127+
{{- else if eq $status "unchecked" -}}
128+
{{- $unchecked = add $unchecked 1 -}}
129+
{{- else if eq $status "draft_warning" -}}
130+
{{- $draftWarning = add $draftWarning 1 -}}
131+
{{- end -}}
132+
133+
{{- $items = $items | append (dict
134+
"title" $title
135+
"contentType" $contentType
136+
"sourcePath" $filePath
137+
"relPermalink" .RelPermalink
138+
"productionPermalink" (cond (ne $academyBaseURL "") (printf "%s/academy%s" $academyBaseURL .RelPermalink) "")
139+
"orgID" $orgID
140+
"contentID" $contentID
141+
"status" $status
142+
"issue" $issue
143+
"fix" $fix
144+
"draft" $isDraft
145+
"registryStatus" $registryStatus
146+
"registryURL" $registryURL
147+
) -}}
148+
{{- end -}}
149+
{{- end -}}
150+
151+
{{- $summary := "ready" -}}
152+
{{- if gt $broken 0 -}}
153+
{{- $summary = "broken" -}}
154+
{{- else if gt $unchecked 0 -}}
155+
{{- $summary = "unchecked" -}}
156+
{{- end -}}
157+
158+
{{- $report := dict
159+
"items" $items
160+
"counts" (dict "good" $good "broken" $broken "draft" $draft "draftWarning" $draftWarning "unchecked" $unchecked "total" (len $items))
161+
"summary" $summary
162+
"registryURLConfigured" (ne $registryURL "")
163+
"consoleURL" $consoleURL
164+
"academyBaseURL" $academyBaseURL
165+
-}}
166+
{{- $site.Store.Set $cacheKey $report -}}
167+
{{- end -}}
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{{- $page := . -}}
2+
{{- $isRootContent := false -}}
3+
{{- if $page.File -}}
4+
{{- $parts := split $page.File.Path "/" -}}
5+
{{- if and (eq (len $parts) 4) (eq (index $parts 0) "learning-paths") (eq (index $parts 3) "_index.md") -}}
6+
{{- $isRootContent = true -}}
7+
{{- else if and (eq (len $parts) 4) (in (slice "certifications" "challenges") (index $parts 0)) (in (slice "_index.md" "index.md") (index $parts 3)) -}}
8+
{{- $isRootContent = true -}}
9+
{{- end -}}
10+
{{- end -}}
11+
{{- if $isRootContent -}}true{{- end -}}

0 commit comments

Comments
 (0)