July 12, 2025
I have recently redesigned my website to take a more modern look, and thought I would share how I ended up with this result. I'm sure many aspects could be improved, as I have limited experience building websites. My requirements were the following:
To keep things easy to write and maintain, I chose to use Markdown, which is simple and powerful enough for my needs. Furthermore it is easy to convert Markdown to HTML with the python package markdown. Due to my very limited knowledge in CSS styling, I have heavily used chatGPT and I must say I was happy with the result. Below, I describe the structure of the code in more details.
The main page is created with the following script:
#!/usr/bin/env python
import argparse
import markdown
import os
from utils import embed_image_float
def main():
parser = argparse.ArgumentParser()
parser.add_argument('md_path', type=str, help="Directory containing md files.")
parser.add_argument('--out-html', type=str, help="Path to output html file.")
parser.add_argument('--blog', action='store_true', default=False, help="Enable link to blog.")
args = parser.parse_args()
md_path = args.md_path
out_html = args.out_html
blog = args.blog
sections = {
"about": "about.md",
"research": "research.md",
"publications": "publications.md"
}
html_sections = ""
for section_id, filename in sections.items():
with open(os.path.join(md_path, filename)) as f:
raw_md = f.read()
processed_md = embed_image_float(raw_md)
html_content = markdown.markdown(processed_md)
html_sections += f'''<section id="{section_id}">
<div class="markdown-body">
{html_content}
</div>
</section>
'''
nav_bar = ""
for key in sections.keys():
name = key.title()
nav_bar += f' <a href="#{key}">{name}</a>\n'
if blog:
nav_bar += ' <a href="./blog.html">Blog</a>\n'
with open(out_html, "w") as f:
f.write(f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Lucas Amoudruz</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<nav class="top-nav">
{nav_bar}
</nav>
{html_sections}
</body>
</html>
""")
if __name__ == '__main__':
main()
It converts Markdown files, parses custom directives, and combines everything into a single HTML file.
The pre-processing stage helps with placing floating images more easily, and it uses the function embed_custom_images
.
This function handles parsing custom image blocks and turns them into proper HTML:
def embed_image_float(md_text):
pattern = r'\{\{\s*image\("([^"]+)",\s*"([^"]*)",\s*"([^"]+)",\s*(\d+)\)\s*\}\}'
def replacer(match):
src = match.group(1)
caption = match.group(2).strip()
float_dir = match.group(3)
width = match.group(4)
float_class = f"float-img-{float_dir}" if float_dir in ["left", "center", "right"] else ""
style = f'style="width: {width}%;"'
caption_html = f'<p class="image-caption">{caption}</p>' if caption else ''
return f'''
<div class="image-wrap {float_class}" {style}>
<img src="{src}" alt="{caption}">
{caption_html}
</div>
'''
return re.sub(pattern, replacer, md_text)
It replaces patterns of the form {{image, "path/to/image.png", "caption", "left/right/center", width-in-percent}}
with the appropriate HTML content.
After being processed, each md file is placed in section
, and a link is added to a navigation bar.
The main blog page is similar to the main page, except that there is a single section containing links to individual blog posts:
#!/usr/bin/env python
import argparse
import markdown
import os
def main():
parser = argparse.ArgumentParser()
parser.add_argument('md_path', type=str, help="path to blog.md")
parser.add_argument('--out-html', type=str, help="Path to output html file.")
args = parser.parse_args()
md_path = args.md_path
out_html = args.out_html
with open(md_path) as f:
raw_md = f.read()
processed_md = raw_md
html_content = markdown.markdown(processed_md)
html_section = f"""
<section class="markdown-body">
{html_content}
</section>
"""
with open(out_html, "w") as f:
f.write(f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Blog – Lucas Amoudruz</title>
<link rel="stylesheet" href="css/main.css" />
</head>
<body>
<nav class="top-nav">
<a href="./index.html">Home</a>
<a href="./blog.html">Blog</a>
</nav>
{html_section}
</body>
</html>
""")
if __name__ == '__main__':
main()
Individual blog pages follow the same pattern: a single md file is preprocessed and then embedded into a common html file format.
#!/usr/bin/env python
import argparse
import markdown
import os
from utils import embed_image_float, embed_image_row, embed_code_from_files
def md_path_to_title(md_path):
return "blog - " + md_path.split("/")[-1].split(".md")[0]
def main():
parser = argparse.ArgumentParser()
parser.add_argument('md_path', type=str, help="path to md file")
parser.add_argument('--out-html', type=str, help="Path to output html file.")
args = parser.parse_args()
md_path = args.md_path
out_html = args.out_html
with open(md_path) as f:
raw_md = f.read()
processed_md = embed_image_float(raw_md)
processed_md = embed_image_row(processed_md)
processed_md = embed_code_from_files(processed_md)
html_content = markdown.markdown(processed_md,
extensions=['mdx_math',
'fenced_code',
'codehilite',
'tables'])
html_section = f"""
<section class="markdown-body">
{html_content}
</section>
"""
title = md_path_to_title(md_path)
with open(out_html, "w") as f:
f.write(f"""
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>{title}</title>
<link rel="stylesheet" href="../css/main.css" />
<link rel="stylesheet" href="../css/codehilite.css" />
<script type="text/javascript" async
src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.7/MathJax.js?config=TeX-MML-AM_CHTML">
</script>
</head>
<body>
<nav class="top-nav">
<a href="../index.html">Home</a>
<a href="../blog.html">Blog</a>
</nav>
{html_section}
</body>
</html>
""")
if __name__ == '__main__':
main()
Note that there is an additional stylesheet for code, generated as follows:
pygmentize -S trac -f html -a .codehilite > css/codehilite.css
This, together with the extensions
provided to the Markdown converter, allows code highlighting in the final rendering.
Similarly, the blog pages include a mathjax snippet to allow equations rendering.
Code and equations are not needed in the main page.
The pre-processing stage also contains an additional step, to include code from files:
def embed_code_from_files(md_text):
full_pattern = r'\{\{\s*file_full,\s*"([^"]+)",\s*"([^"]+)"(?:,\s*"([^"]+)")?\s*\}\}'
partial_pattern = r'\{\{\s*file_partial,\s*"([^"]+)",\s*"([^"]+)",\s*"([^"]+)"(?:,\s*"([^"]+)")?\s*\}\}'
def full_replacer(match):
path, lang = match.group(1), match.group(2)
spoiler = match.group(3) == "spoiler"
try:
with open(path, "r") as f:
code = f.read().strip()
code_block = f"\n```{lang}\n{code}\n```\n"
if spoiler:
return f'<details class="code-block">\n<summary>Show code</summary>\n\n{code_block}</details>\n'
else:
return code_block
except Exception as e:
return f"**Error including file: {path} ({e})**"
def partial_replacer(match):
path, lang, marker = match.group(1), match.group(2), match.group(3)
spoiler = match.group(4) == "spoiler"
start_tag = f"# START_{marker}"
end_tag = f"# END_{marker}"
try:
with open(path, "r") as f:
lines = f.readlines()
inside = False
extracted = []
for line in lines:
if start_tag in line:
inside = True
continue
if end_tag in line:
inside = False
continue
if inside:
extracted.append(line)
code = ''.join(extracted).strip()
code_block = f"\n```{lang}\n{code}\n```\n"
if spoiler:
return f"""<details class="code-block">\n<summary>Show code</summary>\n\n{code_block}</details>\n"""
else:
return code_block
except Exception as e:
return f"**Error including partial code: {path} ({e})**"
md_text = re.sub(full_pattern, full_replacer, md_text)
md_text = re.sub(partial_pattern, partial_replacer, md_text)
return md_text
This function uses a format similar to the one used by the image
pattern.
Here I have two options:
{{file_full, "path/to/code", "language", "spoiler"} }
(spoiler is optional) to include the full file;{{file_partial, "path/to/code", "language", "marker", "spoiler"} }
(again, spoiler is optional) to include code between markers # START_marker
and # END_marker
;Here is my full CSS configuration.
body {
font-family: "Georgia", "Times New Roman", serif;
background-color: white;
color: #222;
line-height: 1.6;
margin: 0;
scroll-behavior: smooth;
}
h1, h2, h3 {
font-family: "Helvetica Neue", "Segoe UI", sans-serif;
font-weight: 600;
color: #3e6075;
}
a {
color: #3e6075;
text-decoration: none;
}
a:hover {
text-decoration: underline;
}
html {
scroll-behavior: smooth;
}
.top-nav {
position: sticky;
top: 0;
width: 100%;
height: auto;
background: white;
border-bottom: 1px solid #ddd;
padding: 1rem;
display: flex;
align-items: center;
justify-content: center;
gap: 1.5rem;
z-index: 1000;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.5);
font-family: "Helvetica Neue", "Segoe UI", sans-serif;
}
.top-nav a {
text-decoration: none;
color: #3e6075;
font-weight: 500;
font-size: 1rem;
padding: 0.3rem 0.6rem;
border-radius: 4px;
transition: background 0.2s ease;
}
.top-nav a:hover {
background-color: rgba(62, 96, 117, 0.1); /* subtle hover */
}
section {
scroll-margin-top: 80px;
max-width: 800px;
margin-left: auto;
margin-right: auto;
padding-bottom: 2rem;
}
.markdown-body img:not(.float-img-left):not(.float-img-right) {
max-width: 100%;
width: 100%;
height: auto;
display: block;
margin: 1rem auto; /* optional: center the image */
}
@media (max-width: 700px) {
.markdown-body {
padding: 0 1rem;
}
}
.markdown-body p {
text-align: justify
}
.markdown-body h1 {
font-size: 2rem;
font-weight: 700;
margin-top: 2rem;
margin-bottom: 1rem;
border-bottom: 2px solid #eee;
padding-bottom: 0.3rem;
}
.markdown-body h2 {
font-size: 1.5rem;
font-weight: 600;
margin-top: 1.5rem;
margin-bottom: 0.75rem;
color: #5a738b;
}
.markdown-body h3 {
font-size: 1.25rem;
font-weight: 500;
margin-top: 1rem;
margin-bottom: 0.5rem;
color: #5a738b;
}
.float-img-right {
float: right;
width: 40%;
margin: 0 0 1em 1em;
}
.float-img-left {
float: left;
width: 40%;
margin: 0 1em 1em 0;
}
.image-row {
display: flex;
gap: 2%;
justify-content: center;
flex-wrap: wrap;
margin: 1.5rem 0;
}
.image-row .image-wrap {
margin: 0;
}
.image-row img {
width: 100%;
height: auto;
}
@media (max-width: 700px) {
.image-row {
flex-direction: column;
}
.image-row .image-wrap {
width: 100% !important;
}
}
.image-caption {
font-style: italic;
font-size: 0.9rem;
color: #555;
margin-top: 0.25rem;
text-align: center;
line-height: 1.3;
}
details {
margin-bottom: 1rem;
}
.code-block summary {
cursor: pointer;
font-weight: bold;
margin: 0.5rem 0;
color: #3e6075;
}
.pub-entry summary {
cursor: pointer;
font-weight: normal;
color: inherit;
margin: 0;
padding: 0;
}
.pub-entry pre {
background: #f9f9f9;
border: 1px solid #ddd;
padding: 0.75rem;
border-radius: 4px;
overflow-x: auto;
font-size: 0.9rem;
}
.codehilite {
background-color: #f9f9f9;
border: 1px solid #ddd;
border-radius: 4px;
padding: 1rem;
overflow-x: auto;
font-size: 0.95rem;
line-height: 1.5;
}
.codehilite pre {
margin: 0;
font-family: Consolas, Menlo, Monaco, "Courier New", monospace;
background: none;
border: none;
}
table {
border-collapse: collapse;
width: 100%;
margin: 1rem 0;
}
th, td {
border: 1px solid #ccc;
padding: 0.4rem 0.75rem;
text-align: left;
}
th {
background-color: #f5f5f5;
font-weight: 600;
}
I could quite easily get everything working with this approach. Markdown is a great format to write content without needing to write any HTML, and the python code is useful to process these Markdown files automatically and convert them into HTML. Here is the github repository of the full source code, hopefully this is useful to somebody.