Skip to content

Commit f2a3610

Browse files
oto-macenauerclaude
andcommitted
feat: live preview rebuild, SharePoint video macro, CMS OAuth + preview styling
- run.js --preview: watch source + auto-rebuild dist so CMS/local edits (incl. uploaded media) appear without a manual rebuild - pack.py: add --no-package flag for fast watch rebuilds - mkdocs-macros-plugin + main.py sharepoint_video() macro for embedding SharePoint/Stream videos in Markdown docs - admin/config.yml: relative public_folder (base-path-safe media) and Cloudflare Worker OAuth (base_url + auth_endpoint) - admin/index.html: register theme CSS + #docs-root/.prose wrapper so the docs preview matches the published theme Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
1 parent ba20b96 commit f2a3610

8 files changed

Lines changed: 230 additions & 4 deletions

File tree

admin/config.yml

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,11 +8,21 @@ backend:
88
name: github
99
repo: AbsaOSS/knowledge-base-docs-example
1010
branch: master
11+
# OAuth handled by a Cloudflare Worker (sveltia-cms-auth). base_url overrides
12+
# Decap's default Netlify OAuth host; auth_endpoint matches the Worker's /auth
13+
# route. The Worker holds the GitHub OAuth client secret.
14+
base_url: https://sveltia-cms-auth.kb-cms.workers.dev
15+
auth_endpoint: auth
1116

1217
# local_backend: true # uncomment for local dev (run: npx decap-server)
1318

19+
# media_folder = where uploads are committed in the repo.
20+
# public_folder = the path written into Markdown. Keep it RELATIVE (no leading
21+
# slash) so images resolve regardless of where the site is served — e.g. a
22+
# GitHub *project* page under /<repo>/ where an absolute "/docs/..." would 404.
23+
# MkDocs rewrites this relative path correctly for each page's output URL.
1424
media_folder: docs/assets/images
15-
public_folder: /docs/assets/images
25+
public_folder: assets/images
1626

1727
# ── Documentation pages ──────────────────────────────────────────────────────
1828
# Folder collection: editors can create, edit, and delete pages in docs/.

admin/index.html

Lines changed: 29 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,37 @@
88
<body>
99
<script src="https://unpkg.com/decap-cms@^3.0.0/dist/decap-cms.js"></script>
1010
<script>
11-
// ── Live preview for the showcase page ──────────────────────────────────
11+
// ── Preview styles ───────────────────────────────────────────────────────
12+
// Inject the built site CSS into the CMS preview iframe so the docs preview
13+
// matches the published theme. style.css is the theme's pre-built Tailwind
14+
// (incl. .prose typography); served at /docs/style.css on the built site.
15+
CMS.registerPreviewStyle('/docs/style.css');
1216
CMS.registerPreviewStyle('/showcase.css');
1317

18+
// ── Live preview for documentation pages ────────────────────────────────
19+
// The theme styles are scoped to "#docs-root" and ".prose" (see theme/
20+
// main.html), so mirror that wrapper here or the markdown renders unstyled.
21+
var DocsPreview = createClass({
22+
render: function () {
23+
return h(
24+
'div',
25+
{ id: 'docs-root' },
26+
h(
27+
'main',
28+
{ id: 'content' },
29+
h(
30+
'article',
31+
{ className: 'prose prose-gray max-w-none', style: { padding: '1.5rem' } },
32+
this.props.widgetFor('body')
33+
)
34+
)
35+
);
36+
}
37+
});
38+
39+
CMS.registerPreviewTemplate('docs', DocsPreview);
40+
41+
// ── Live preview for the showcase page ──────────────────────────────────
1442
var ShowcasePreview = createClass({
1543
render: function () {
1644
var content = this.props.entry.getIn(['data', 'content']) || '';

docs/adding-pages.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,39 @@ From the showcase HTML, link into the docs:
7575
<a href="docs/index.html">Read the docs</a>
7676
```
7777

78+
## Embedding SharePoint videos
79+
80+
MkDocs renders Markdown, not MDX — but reusable components are available through
81+
[mkdocs-macros](https://mkdocs-macros-plugin.readthedocs.io/). Call the
82+
`sharepoint_video` macro from any `.md` page to drop in a responsive video:
83+
84+
```markdown
85+
{% raw %}{{ sharepoint_video('https://absa.sharepoint.com/.../video.mp4', title='Onboarding demo') }}{% endraw %}
86+
```
87+
88+
This renders a responsive 16:9 iframe that scales with the page. Arguments:
89+
90+
| Argument | Required | Notes |
91+
|---|---|---|
92+
| `url` | Yes | SharePoint share/embed URL. In SharePoint use **Share → Embed** and copy the `src`; a plain share link usually works too (append `&embed=true` if the player does not load). |
93+
| `title` | No | Accessible label for screen readers. |
94+
| `ratio` | No | Aspect-ratio padding. `"56.25%"` = 16:9 (default), `"75%"` = 4:3. |
95+
96+
Macros are defined in `main.py` at the repo root — add your own there to build
97+
more reusable components.
98+
99+
## Adding images
100+
101+
Upload images through the CMS at `/admin/`, or drop files into
102+
`docs/assets/images/` and reference them with a path relative to your page:
103+
104+
```markdown
105+
![Architecture diagram](assets/images/diagram.png)
106+
```
107+
108+
MkDocs rewrites the path to the correct URL at build time, so it works both
109+
locally and when the site is served under a subpath.
110+
78111
## Cross-linking between Markdown pages
79112

80113
Use relative paths without the `.md` extension in links:

main.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
#
2+
# Copyright 2026 ABSA Group Limited
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
#
16+
17+
"""mkdocs-macros module — reusable components for documentation pages.
18+
19+
Macros are usable directly inside any docs/*.md file, e.g.:
20+
21+
{{ sharepoint_video('https://absa.sharepoint.com/.../video.mp4', title='Demo') }}
22+
23+
Docs: https://mkdocs-macros-plugin.readthedocs.io/
24+
"""
25+
26+
import html
27+
28+
29+
def define_env(env):
30+
"""Register macros with the mkdocs-macros plugin."""
31+
32+
@env.macro
33+
def sharepoint_video(url, title="SharePoint video", ratio="56.25%"):
34+
"""Embed a SharePoint / Microsoft Stream video as a responsive iframe.
35+
36+
Args:
37+
url: The SharePoint share/embed URL of the video.
38+
title: Accessible iframe title (shown to screen readers).
39+
ratio: Aspect-ratio padding. "56.25%" = 16:9, "75%" = 4:3.
40+
41+
Usage in a .md page:
42+
{{ sharepoint_video('https://absa.sharepoint.com/.../video.mp4') }}
43+
44+
Tip: in SharePoint use "Share" → "Embed" and paste the src URL. A plain
45+
share link usually works too; append "&embed=true" if the player does
46+
not load.
47+
"""
48+
safe_url = html.escape(str(url), quote=True)
49+
safe_title = html.escape(str(title), quote=True)
50+
return (
51+
f'<div class="sp-embed" style="position:relative;width:100%;'
52+
f'padding-bottom:{ratio};height:0;overflow:hidden;margin:1.5rem 0;'
53+
f'border-radius:8px;background:#000;">'
54+
f'<iframe src="{safe_url}" title="{safe_title}" '
55+
f'style="position:absolute;top:0;left:0;width:100%;height:100%;border:0;" '
56+
f'allowfullscreen '
57+
f'allow="autoplay; fullscreen; encrypted-media; picture-in-picture" '
58+
f'loading="lazy"></iframe></div>'
59+
)

mkdocs.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,3 +24,8 @@ theme:
2424
custom_dir: theme
2525

2626
# nav is auto-generated from frontmatter (title, order, section) by pack.py
27+
28+
plugins:
29+
- search
30+
- macros: # reusable components for docs pages — see main.py
31+
module_name: main

requirements.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
11
mkdocs==1.6.1
22
jinja2>=3.1
3+
mkdocs-macros-plugin>=1.0

run.js

Lines changed: 81 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,8 +15,9 @@
1515
* limitations under the License.
1616
*/
1717

18-
const { execFileSync, execSync } = require("child_process");
18+
const { execFileSync, execSync, spawn } = require("child_process");
1919
const { resolve } = require("path");
20+
const fs = require("fs");
2021

2122
const ROOT = resolve(__dirname);
2223
const env = { ...process.env, PYTHONIOENCODING: "utf-8" };
@@ -48,13 +49,91 @@ function build(extraArgs) {
4849
}
4950

5051
if (args.includes("--preview")) {
52+
// Full build once, then serve dist/ AND watch source files so edits made by
53+
// hand or through the CMS at /admin/ (incl. uploaded media) are picked up and
54+
// dist/ is rebuilt automatically — no manual rebuild needed.
5155
build([]);
56+
5257
console.log("\nServing dist/ at http://localhost:8000 …");
53-
execFileSync(py, ["-m", "http.server", "8000", "-d", "dist"], {
58+
const server = spawn(py, ["-m", "http.server", "8000", "-d", "dist"], {
5459
cwd: ROOT,
5560
stdio: "inherit",
5661
env,
5762
});
63+
64+
// Source paths that feed the build. dist/ is deliberately NOT watched (would
65+
// loop). mkdocs-build*.yml are transient and live at the repo root, unwatched.
66+
const watchTargets = [
67+
"docs",
68+
"data",
69+
"theme",
70+
"admin",
71+
"mkdocs.yml",
72+
"mkdocs-headless.yml",
73+
"showcase.html",
74+
"showcase.css",
75+
"main.py",
76+
"marketplace.json",
77+
];
78+
79+
let rebuilding = false;
80+
let pending = false;
81+
let timer = null;
82+
83+
function rebuild() {
84+
if (rebuilding) {
85+
pending = true;
86+
return;
87+
}
88+
rebuilding = true;
89+
console.log("\n↻ Change detected — rebuilding dist/ …");
90+
try {
91+
execFileSync(py, ["scripts/pack.py", "--no-package"], {
92+
cwd: ROOT,
93+
stdio: "inherit",
94+
env: { ...env, SKIP_PIP_INSTALL: "1" },
95+
});
96+
console.log("✅ Rebuild complete — refresh your browser.\n");
97+
} catch (e) {
98+
console.error("⚠ Rebuild failed:", e.status || e.message);
99+
}
100+
rebuilding = false;
101+
if (pending) {
102+
pending = false;
103+
schedule();
104+
}
105+
}
106+
107+
function schedule() {
108+
clearTimeout(timer);
109+
timer = setTimeout(rebuild, 300); // debounce bursty CMS/file writes
110+
}
111+
112+
const watchers = [];
113+
for (const target of watchTargets) {
114+
const p = resolve(ROOT, target);
115+
if (!fs.existsSync(p)) continue;
116+
try {
117+
watchers.push(fs.watch(p, { recursive: true }, schedule));
118+
} catch {
119+
// recursive watch unsupported (older Node on Linux) — watch shallowly
120+
watchers.push(fs.watch(p, {}, schedule));
121+
}
122+
}
123+
console.log(`Watching ${watchers.length} source path(s) — Ctrl+C to stop.`);
124+
125+
function shutdown() {
126+
for (const w of watchers) {
127+
try {
128+
w.close();
129+
} catch {}
130+
}
131+
if (server && !server.killed) server.kill();
132+
process.exit(0);
133+
}
134+
process.on("SIGINT", shutdown);
135+
process.on("SIGTERM", shutdown);
136+
server.on("exit", (code) => process.exit(code || 0));
58137
} else if (args.includes("--dev")) {
59138
try {
60139
execFileSync(py, ["scripts/pack.py", "--serve"], {

scripts/pack.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,11 +242,14 @@ def add_entries(items, section=None):
242242
def main():
243243
headless = False
244244
serve = False
245+
no_package = False
245246
for arg in sys.argv[1:]:
246247
if arg == "--headless":
247248
headless = True
248249
elif arg == "--serve":
249250
serve = True
251+
elif arg == "--no-package":
252+
no_package = True
250253
else:
251254
print(f"Unknown argument: {arg}")
252255
sys.exit(1)
@@ -282,6 +285,10 @@ def main():
282285
print("▶ Generating dist/marketplace.json with pages manifest...")
283286
generate_marketplace_json()
284287

288+
if no_package:
289+
print("✅ dist/ rebuilt (headless, packaging skipped)")
290+
return
291+
285292
print("▶ Packaging (headless)...")
286293
with tarfile.open("dist.tar.gz", "w:gz") as tar:
287294
tar.add("dist")
@@ -306,6 +313,10 @@ def main():
306313
print("▶ Generating dist/marketplace.json with pages manifest...")
307314
generate_marketplace_json()
308315

316+
if no_package:
317+
print("✅ dist/ rebuilt (packaging skipped)")
318+
return
319+
309320
print("▶ Packaging...")
310321
with tarfile.open("dist.tar.gz", "w:gz") as tar:
311322
tar.add("dist")

0 commit comments

Comments
 (0)