Write all page content as Markdown files and use a build script to generate the same HTML+CSS output that currently exists, without changing the visual appearance.
The following table covers all HTML constructs found across the 125 article pages.
| HTML construct | Count of pages | Markdown equivalent |
|---|---|---|
<h1> … <h4> |
all pages | # … #### |
<p> |
~70 pages | blank line between paragraphs |
<br> (line break) |
very common | two trailing spaces or \ at end of line (CommonMark) |
<ul> / <ol> / <li> |
~110 pages | - item / 1. item |
<b> / <strong> |
~90 pages | **text** |
<i> / <em> |
~20 pages | *text* |
<s> strikethrough |
2 pages | ~~text~~ (GFM extension) |
<a href="…"> plain links |
all pages | [label](href) |
<code> inline |
~15 pages | `code` |
<pre><code> block |
14 pages | ```lang … ``` fenced block |
<blockquote> |
5 pages | > text |
<hr> |
4 pages | --- |
<img src alt width> |
~20 pages |  |
| HTML construct | Pages | Problem | Solution |
|---|---|---|---|
<table class="infobox"> |
14 | Floating right, complex colspan, links inside cells | Raw HTML block in Markdown; or YAML front-matter shorthand |
<table> with rowspan/colspan |
~12 | Markdown tables do not support merged cells | Keep as raw HTML block in Markdown |
<table> simple (no merge) |
~8 | Supported | Markdown pipe table syntax |
<a class="ref"> |
6 pages | Custom CSS class on link | [text](url){.ref} using attr_list extension, or raw HTML |
<ul id="tags"> at page bottom |
11 | Semantic tag list | YAML front-matter tags: list; template renders it |
<figure> + <figcaption> |
3 | No native Markdown equivalent | Raw HTML block, or  + custom CSS |
<div style="background: orange;"> |
1 | Warning/callout box | > [!WARNING] callout syntax (GitHub-flavored) or custom class via attr_list |
style="background:#bbbbbb" on <b> |
4 occurrences | Inline highlighted term | Raw HTML <b style="…"> inside Markdown |
style="max-width:300px;" on <img> |
17 occurrences | Image size constraint | {style="max-width:300px;"} with attr_list, or raw <img> |
style="display:block" on <a> |
51 occurrences | All in one large table page (Blupi_websites) |
Keep that page as raw HTML; or add .block CSS class |
<main style="background: #f5b7b1;"> |
1 (error page) | Special background on error page | Front-matter body_class: error and template adds inline style |
| HTML construct | Reason disappears |
|---|---|
<header> + <nav> |
Already injected by buildPage() |
<footer> |
Already injected by buildPage() |
<div id="tocButton"> + <div id="toc"> |
Already generated by loadContent() — the template adds them |
<body onload=…> |
Template uses DOMContentLoaded |
<script>PAGE_CONFIG={…}</script> |
Template generates this from front-matter |
<meta description/keywords/author> |
Already injected by buildPage() |
<base href="…"> |
Template calculates depth from file path |
<section> wrapper |
Template wraps content in <section> automatically |
Every article becomes one .md file with a YAML front-matter header. The front-matter replaces PAGE_CONFIG and provides metadata. The rest is standard Markdown.
---
title: "Speedy Blupi (Windows)"
breadcrumb:
- {label: "Blupi", href: "Blupi/index.html"}
- {label: "Games", href: "Blupi/Games/index.html"}
- {label: "Speedy Blupi (Windows)", href: "Blupi/Games/Speedy_Blupi_(Windows)/index.html"}
subpages:
- {label: "Go Up", href: "Blupi/Games/index.html"}
- {label: "Blocks", href: "Blupi/Games/Speedy_Blupi_(Windows)/Blocks/index.html"}
- {label: "Levels", href: "Blupi/Games/Speedy_Blupi_(Windows)/Levels/index.html"}
tags:
- "Games created by Daniel Roux"
---
<!-- Raw HTML infobox (kept as-is) -->
<table class="infobox"> … </table>
## Introduction
Speedy Blupi is a 2D [platformer](Article_does_not_yet_exist_or_link_is_broken/index.html) game
originally developed by Swiss company [Epsitec](Blupi/Epsitec/index.html).
## Minimum system requirements
- PC Windows 95/98/98SE/Me/2000/XP/Vista/7/8/10/11
- [Pentium 100](Technologies/Programming_languages/Assembly_Language/I586/Pentium_100/index.html) MHz CPU
- 16 MB RAM
## Code example
```java
float velocityY = 0;
final float gravity = -0.5f;| Header A | Header B |
|---|---|
| value 1 | value 2 |
| Name | Effects | |
|---|---|---|
External source{.ref}
---
### 2.3 Recommended Build Tool: Python + `python-markdown`
**Why Python:**
- Already used in this project (the transformation script is Python)
- No new runtime to install
- `python-markdown` library with extensions handles 90 % of constructs natively
**Extensions needed from `python-markdown`:**
| Extension | Purpose |
|---|---|
| `tables` | Basic pipe-table syntax |
| `fenced_code` | ```` ```lang … ``` ```` code blocks |
| `attr_list` | Add `{.classname}` / `{style="…"}` to any element |
| `toc` | Auto-generate heading ids (already done by JS, but useful for anchors) |
| `nl2br` | Convert single newlines to `<br>` (matches current heavy `<br>` usage) |
| `sane_lists` | Correct list nesting |
| `meta` | Read YAML-style front-matter (or use PyYAML separately) |
| `md_in_html` | Allow Markdown inside raw HTML blocks |
**Alternative: Pandoc (single binary)**
Pandoc can convert Markdown → HTML with a custom `--template` flag. It supports all the above natively plus finer control over raw HTML passthrough. The downside is it requires installing the Pandoc binary (~30 MB), which is non-trivial in CI without caching.
---
### 2.4 Build Script Architecture
src/ About/ index.md ← Markdown content + front-matter Abbreviations/ index.md Blupi/ index.md Games/ Speedy_Blupi_(Windows)/ index.md Levels/ I/ 060/ index.md … _template.html ← HTML shell (head + script refs, buildPage calls)
build.py ← reads src//*.md, writes root//*.html styles.css ← unchanged script.js ← unchanged (buildPage + loadContent)
**`build.py` logic (per file):**
1. Read `src/.../index.md`
2. Parse YAML front-matter (title, breadcrumb, subpages, tags, optional depth)
3. Convert Markdown body → HTML fragment using `python-markdown`
4. Calculate `<base href>` from file depth
5. Generate `<script>PAGE_CONFIG={…}</script>` from front-matter
6. Generate `<ul id="tags">` from `tags:` list (if present)
7. Write the full HTML to the matching path under the root (e.g. `About/index.html`)
The output HTML files are byte-for-byte compatible with the current format — the same `script.js` and `styles.css` work unchanged.
---
### 2.5 What Stays as Raw HTML in Markdown
These constructs are kept as raw HTML blocks directly inside the `.md` files and passed through unchanged by the Markdown processor:
1. **`<table class="infobox">`** — all 14 occurrences; too complex and unique to convert
2. **Tables with `rowspan`/`colspan`** — ~12 pages (Cheats table, Level tables, Blupi_websites)
3. **`<figure><img …><figcaption>`** — 3 pages
4. **`<div style="background: orange;">` callout boxes** — 1 page
5. **Bold text with `style="background:#bbbbbb"`** — 4 occurrences (could add a `.highlight` CSS class instead)
Estimated **raw HTML percentage: ~10–15 %** of content volume across the site. The remaining 85–90 % converts cleanly to readable Markdown.
---
### 2.6 Migration Strategy (no changes to existing pages during migration)
1. **Phase 1 (setup):** Create `src/` directory and `build.py`. Write the HTML template. Set up GitHub Actions to run `build.py` and commit the generated HTML back to the repo on every push to `src/`.
2. **Phase 2 (pilot):** Convert 5–10 small pages from HTML → Markdown, verify output is visually identical.
3. **Phase 3 (bulk conversion):** Use a Python script to auto-convert the existing `index.html` files to `index.md` (reverse of `build.py`). Auto-conversion handles ~85 % of pages. Manually fix the remaining 15 % with complex tables/infoboxes.
4. **Phase 4 (switch):** Once all Markdown sources are verified, the generated HTML files are the only files committed to the root. Source `.md` files live in `src/`.
**CI/CD pipeline (GitHub Actions):**
```yaml
name: Build site
on:
push:
paths: ['src/**', 'build.py', '_template.html']
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- run: pip install markdown PyYAML
- run: python build.py
- uses: actions/upload-pages-artifact@v3
with: {path: '.'}
Or simpler: commit generated HTML directly to the repo (no separate artifact step), which keeps GitHub Pages working without changing the deployment method.
| Aspect | Current (raw HTML) | After Markdown migration |
|---|---|---|
| Writing a new article | ~15-line HTML shell + content HTML | Pure Markdown text, no HTML knowledge needed |
| Infoboxes | Write table HTML by hand | Still write table HTML (raw block) |
| Simple tables | HTML table markup | Markdown pipe syntax |
| Links | <a href="…">label</a> |
[label](href) |
| Build step required | No | Yes (Python script + GitHub Actions) |
| Previewing in editor | Browser only | Any Markdown editor (VS Code, Obsidian, etc.) |
| Raw HTML still needed | Everywhere | ~10–15 % of content (complex tables, infoboxes) |
| Risk | Low (already done) | Medium (new build pipeline to maintain) |
A static HTML fan-community website for the Speedy Blupi / Open Eggbert game series, hosted (likely via GitHub Pages) without any build toolchain. There are no frameworks, no bundlers — just raw HTML files, one shared styles.css, and one shared script.js, all residing at the repository root.
- 127 HTML files spread across approximately 129 directories
- Maximum directory depth: 6 levels (e.g.
Blupi/Games/Speedy_Blupi_(Windows)/Levels/I/060/index.html) - One
Template/index.htmlused as a copy-paste starting point for new pages - One special
tree.htmlat root (full article tree, no shared boilerplate — standalone page)
Every article lives at <Category>/<Subcategory>/.../index.html. All asset references (CSS, JS, favicon) are resolved via <base href="…"> pointing back to the site root, so all links inside a page are written as absolute-from-root paths regardless of how deep the file sits.
Every single index.html (except tree.html) contains the following identical blocks:
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta name="description" content="Open Eggbert, a free and open-source game…"/>
<meta name="keywords" content="Blupi, Speedy Blupi, Speedy Eggbert, …"/>
<meta name="author" content="Robert Vokáč">
<link rel="stylesheet" href="styles.css">
<link rel="icon" href="favicon.ico" type="image/x-icon" sizes="32x32">
<script type="text/javascript" src="script.js"></script>Only two things differ: <title> text and <base href="…"> depth.
<header>
<div id="main_banner"><a href="index.html">Open Eggbert</a></div>
<nav>
<ul>
<li><a href="index.html">Home</a></li>
<li><a href="About/index.html">About</a></li>
<li><a href="Blupi/index.html">Blupi</a></li>
<li><a href="Projects/index.html">Projects</a></li>
<li><a href="Technologies/index.html">Technologies</a></li>
</ul>
</nav>
</header>Repeated in all 126 article pages.
<footer>
<p>Content is available under <a href="https://creativecommons.org/licenses/by-sa/4.0/" …>
Creative Commons Attribution-ShareAlike 4.0 International License</a> unless otherwise noted.</p>
</footer>Repeated in all 126 article pages.
<body onload="loadContent()">
…
<div id="tocButton"></div>
<div id="toc"></div>The TOC div pair appears inside <main> on every article page.
The outer HTML structure is always the same:
<div id="breadcrumb_hierarchy_panel">
<div id="breadcrumb"> … </div>
<div id="hierarchy_panel"> … </div>
</div>Only the inner links differ (breadcrumb chain and list of child pages).
| Element | Unique per page? |
|---|---|
<title> |
Yes |
<base href> |
Yes (depth varies) |
<header> / <nav> |
No — identical everywhere |
<footer> |
No — identical everywhere |
| Breadcrumb links | Yes |
| Hierarchy panel links | Yes |
<main> content |
Yes — all article text |
No build step is introduced. All pages remain plain .html files. The shared boilerplate (header, nav, footer) is removed from every page and instead injected by script.js at page-load time.
Each article page is reduced to a minimal shell:
DOCTYPE + <html>
<head> ← only title, base href, CSS/JS refs
<body> ← NO header/footer markup
<script> ← small inline config object (breadcrumb + subpages)
PAGE_CONFIG = { … };
</script>
<main>
<section>
<h1>…</h1>
<!-- article content only -->
</section>
</main>
script.js is extended to:
- Read
PAGE_CONFIGbefore injecting. document.body.insertAdjacentHTML('afterbegin', headerHTML)— inject<header>+<nav>at top.- Build
#breadcrumb_hierarchy_panelfromPAGE_CONFIG.breadcrumbandPAGE_CONFIG.subpages. document.body.insertAdjacentHTML('beforeend', footerHTML)— inject<footer>.- Run the existing TOC logic (already in
loadContent()).
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Open Eggbert - ARTICLE_TITLE</title>
<base href="RELATIVE_PATH_TO_ROOT" target="_self">
<link rel="stylesheet" href="styles.css">
<link rel="icon" href="favicon.ico" type="image/x-icon" sizes="32x32">
<script src="script.js"></script>
</head>
<body>
<script>
PAGE_CONFIG = {
title: "ARTICLE_TITLE",
breadcrumb: [
{ label: "ParentA", href: "ParentA/index.html" },
{ label: "ParentB", href: "ParentA/ParentB/index.html" }
],
subpages: [
{ label: "Go Up", href: "ParentA/ParentB/index.html" },
{ label: "Child 1", href: "ParentA/ParentB/ARTICLE_TITLE/Child1/index.html" },
{ label: "Child 2", href: "ParentA/ParentB/ARTICLE_TITLE/Child2/index.html" }
]
};
</script>
<main>
<section>
<h1>ARTICLE_TITLE</h1>
<div id="tocButton"></div>
<div id="toc"></div>
<!-- article content here -->
</section>
</main>
</body>
</html>Lines saved per page: approximately 35–40 lines of boilerplate → down to ~10 lines of shell.
Add a buildPage() function that runs before loadContent():
const NAV_LINKS = [
{ label: "Home", href: "index.html" },
{ label: "About", href: "About/index.html" },
{ label: "Blupi", href: "Blupi/index.html" },
{ label: "Projects", href: "Projects/index.html" },
{ label: "Technologies", href: "Technologies/index.html" }
];
const FOOTER_HTML =
`<footer><p>Content is available under ` +
`<a href="https://creativecommons.org/licenses/by-sa/4.0/" target="_blank" rel="noopener noreferrer">` +
`Creative Commons Attribution-ShareAlike 4.0 International License</a> unless otherwise noted.</p></footer>`;
function buildPage() {
// Build <header>
const navItems = NAV_LINKS.map(n => `<li><a href="${n.href}">${n.label}</a></li>`).join('');
const headerHTML =
`<header>` +
`<div id="main_banner"><a href="index.html">Open Eggbert</a></div>` +
`<nav><ul>${navItems}</ul></nav>` +
`</header>`;
document.body.insertAdjacentHTML('afterbegin', headerHTML);
// Build breadcrumb + hierarchy panel from PAGE_CONFIG
if (typeof PAGE_CONFIG !== 'undefined') {
const breadcrumbLinks = (PAGE_CONFIG.breadcrumb || [])
.map(b => `<a href="${b.href}">${b.label}</a>`)
.join(' / ');
const subpageLinks = (PAGE_CONFIG.subpages || [])
.map(s => `<a href="${s.href}">${s.label}</a>`)
.join('');
const panelHTML =
`<div id="breadcrumb_hierarchy_panel">` +
`<div id="breadcrumb">${breadcrumbLinks}</div>` +
`<div id="hierarchy_panel">${subpageLinks}</div>` +
`</div>`;
document.querySelector('main').insertAdjacentHTML('beforebegin', panelHTML);
}
// Inject <footer>
document.body.insertAdjacentHTML('beforeend', FOOTER_HTML);
}<body> changes from <body onload="loadContent()"> to just <body>, and both buildPage() and loadContent() are called via:
document.addEventListener('DOMContentLoaded', () => { buildPage(); loadContent(); });The current <meta name="description"> and <meta name="keywords"> tags are identical on every page (they always describe the site, not the article). Two options:
- Option A (simplest): Move these into
script.js/buildPage()and inject them into<head>dynamically — they disappear from every page file. - Option B (per-page SEO): Keep them in the page
<head>so each page can have a unique description in the future.
Recommendation: Option A for now (they are already identical everywhere, so no per-page value is lost), but make PAGE_CONFIG.description an optional override that, if present, replaces the default.
| Benefit | Detail |
|---|---|
| Adding a new page | Copy minimal template (~25 lines), fill PAGE_CONFIG + <main> content. No boilerplate copying. |
| Updating nav | Change NAV_LINKS in script.js — propagates to all 127 pages instantly. |
| Updating footer license | Change FOOTER_HTML in script.js — done. Currently requires editing 127 files. |
| Updating meta description | Same — one place instead of 127. |
| Fewer merge conflicts | Articles only contain content, not structural HTML. |
| No build toolchain needed | Pure browser-side JS; works with GitHub Pages as-is. |
- The URL structure (
Category/Subcategory/index.html) stays identical. - The
styles.cssis untouched. - The
<base href>trick for depth-relative asset loading is kept. - The visual appearance is pixel-identical to the current site (same CSS, same HTML output after JS runs).
- The
tree.htmlpage is standalone and needs no changes (it already has its own inline styles). - SEO: Google and most modern crawlers execute JavaScript, so injected content is indexed. For the rare crawler that does not execute JS, the page title and article
<h1>content are still in the static HTML.
Migration can be done in small, safe batches:
- Update
script.js— addbuildPage()and change the init call. No page breaks yet (existing pages still have their own header/footer). - Convert pages one section at a time — start with a low-traffic section (e.g.
About/), verify visually, then proceed. - Remove old boilerplate from converted pages only after verifying the injected version looks correct.
- Update
Template/index.htmlto the new minimal format so all future pages start slim. - Remove
<meta name="description/keywords/author">from all pages oncebuildPage()handles them.
A simple shell script can verify each converted page still contains the expected PAGE_CONFIG variable and <main> tag.
Once the JS injection is in place, a further step (not required) would be to write article content in Markdown and convert it with a minimal build script (e.g. pandoc or a Node.js script) that:
- Reads each
content.mdfile - Wraps it in the minimal HTML shell with appropriate
PAGE_CONFIG - Outputs
index.html
This would allow article authors to write plain Markdown without touching any HTML at all. This step is entirely optional and should only be considered after the JS injection refactor is complete and working.