diff --git a/pygitweb/main.py b/pygitweb/main.py
index 6752c55..8396ac9 100644
--- a/pygitweb/main.py
+++ b/pygitweb/main.py
@@ -13,6 +13,8 @@ from fastapi import FastAPI, HTTPException, Query, Request
 from fastapi.responses import HTMLResponse, PlainTextResponse, Response
 from fastapi.staticfiles import StaticFiles
 
+from jinja2 import Template, Environment, PackageLoader
+
 from pygitweb import config
 from pygitweb.config import (
     EXPORT_OK,
@@ -86,9 +88,16 @@ ACTIONS = {
 
 app = FastAPI(title="pygitweb", description="FastAPI + Pygit2 Repo Browser")
 
-# Theme: default is dark; set <html class="theme-light"> or class="theme-solarized" to switch
-HTML_HEAD = '<meta charset="utf-8"><link rel="stylesheet" href="/static/main.css">'
+env = Environment(
+    loader=PackageLoader("pygitweb", "templates"),
+    # autoescape=True,
+    trim_blocks=True,
+    lstrip_blocks=True,
+)
+PREAMBLE = env.get_template("preamble.html")
+POSTAMBLE = "</body></html>"
 
+# Todo handle with nginx route
 _static_dir = Path(__file__).parent / "static"
 if _static_dir.is_dir():
     app.mount("/static", StaticFiles(directory=str(_static_dir)), name="static")
@@ -170,19 +179,17 @@ def git_project_list(
     
     # todo
     # list_ = filter_forks_from_projects_list(list_)
-    # Simple HTML response (full template can be added later)
-    rows = []
-    for pr in list_[:50]:
-        path = pr.get("path", "")
-        descr = pr.get("descr", "") or path
-        rows.append(f"<tr><td><a href='/{path}'>{esc_html(path)}</a></td><td>{esc_html(descr)}</td></tr>")
-    body = "\n".join(rows)
-    return HTMLResponse(
-        f"""<!DOCTYPE html><html class="theme-dark"><head><title>{config.SITE_NAME} - project list</title>{HTML_HEAD}</head>
-<body><h1>Projects</h1><table><tr><th>Project</th><th>Description</th></tr>
-{body}
-</table></body></html>"""
+
+    # We need better escaping logic here but can wait until we harden the templates
+    table = env.get_template("table.html").render(
+        cols=["Project", "Description"],
+        rows=[
+            [f"<a href='/{pr.get('path', "")}'>{pr.get('path', "")}</a>",
+            pr.get('descr') or pr.get('path', "")]
+             for pr in list_[:50]]
     )
+    pre = PREAMBLE.render(title=f'{esc_html(config.SITE_NAME)} - Projects')
+    return HTMLResponse(f"{pre}<h1>Project List</h1>{table}{POSTAMBLE}")
 
 
 @app.get("/index", response_class=PlainTextResponse)
@@ -280,9 +287,9 @@ def dispatch(
     if action == "history":
         return git_history(proj, hash_param, file_name)
     # Stub others with minimal response
+    pre = PREAMBLE.render(title=f"{esc_html(action)} - {esc_html(proj)}")
     return HTMLResponse(
-        f"""<!DOCTYPE html><html class="theme-dark"><head><title>{esc_html(action)} - {esc_html(proj)}</title>{HTML_HEAD}</head>
-<body><p>Action: {esc_html(action)}</p><p>Project: {esc_html(proj)}</p></body></html>"""
+        f"{pre}<p>Action: {esc_html(action)}</p><p>Project: {esc_html(proj)}</p>{POSTAMBLE}"
     )
 
 
@@ -298,15 +305,18 @@ def git_summary(project: str) -> HTMLResponse:
     head = git_get_head_hash(project)
     co = parse_commit(project, head) if head else {}
     head_short = head[:7] if head else ""
-    return HTMLResponse(
-        f"""<!DOCTYPE html><html class="theme-dark"><head><title>{config.SITE_NAME} - {project}</title>{HTML_HEAD}</head>
-<body><h1>{esc_html(project)}</h1>
-<table><tr><td>description</td><td>{esc_html(descr)}</td></tr>
-<tr><td>owner</td><td>{esc_html(owner)}</td></tr>
-<tr><td>HEAD</td><td><a href="/{project}/commit/{head or ''}">{head_short or 'N/A'}</a></td></tr>
-<tr><td>tree</td><td><a href="/{project}?a=tree&h={head or ''}">browse</a></td></tr>
-</table></body></html>"""
+
+    table = env.get_template("table.html").render(
+        cols=[],
+        rows=[
+            ["Description", esc_html(descr)],
+            ["Owner", esc_html(owner)],
+            [esc_html("HEAD"), f"<a href='/{project}/commit/{head or ''}'>{head_short or 'N/A'}</a>"],
+            [esc_html("tree"), f"<a href='/{project}?a=tree&h={head or ''}'>browse</a>"],
+        ]
     )
+    pre = PREAMBLE.render(title=f"{esc_html(config.SITE_NAME)} - {project}")
+    return HTMLResponse(f"{pre}<h1>{esc_html(project)}</h1>{table}{POSTAMBLE}")
 
 
 def _tree_url(project: str, h: str, f: str | None, a: str = "tree") -> str:
@@ -339,11 +349,10 @@ def git_blob(project: str, h: str | None, f: str | None, *, raw: bool = False) -
         return Response(content=data, media_type=media_type)
     # HTML view: raw content, escaped for safe display
     text = to_utf8(data) or ""
-    body_escaped = sanitize(text) or ""
+    body = sanitize(text) or ""
+    pre = PREAMBLE.render(title=f"{esc_html(f)} - {esc_html(project)}")
     return HTMLResponse(
-        f"""<!DOCTYPE html><html class="theme-dark"><head><title>{esc_html(f)} - {esc_html(project)}</title>{HTML_HEAD}</head>
-<body><pre class="blob-content">{body_escaped}</pre></body></html>"""
-    )
+        f"{pre}<pre class='blob-content'>{body}</pre>{POSTAMBLE}")
 
 
 def git_tree(project: str, h: str | None, f: str | None) -> HTMLResponse:
@@ -374,24 +383,22 @@ def git_tree(project: str, h: str | None, f: str | None) -> HTMLResponse:
         sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
         link = _tree_url(project, ref_oid, sub_path)
         rows.append(
-            f'<tr><td><a href="{link}">{esc_html(name)}/</a></td><td>tree</td></tr>'
+            [f'<a href="{link}">{esc_html(name)}/</a>', "tree"]
         )
     for name, typ, _ in blobs:
         sub_path = f"{f.strip('/')}/{name}".strip("/") if f else name
         link = _tree_url(project, ref_oid, sub_path, a="blob")
         rows.append(
-            f'<tr><td><a href="{link}">{esc_html(name)}</a></td><td>blob</td></tr>'
+            [f'<a href="{link}">{esc_html(name)}</a>', "blob"]
         )
-    table_body = "\n".join(rows)
     title_path = f" / {f}" if f else ""
+    pre = PREAMBLE.render(title=f"{esc_html(project)}{esc_html(title_path)} - Tree")
+    table = env.get_template("table.html").render(
+        cols=["Name", "Type"],
+        rows=rows
+    )
     return HTMLResponse(
-        f"""<!DOCTYPE html><html class="theme-dark"><head><title>{esc_html(project)}{esc_html(title_path)} - tree</title>{HTML_HEAD}</head>
-<body><p class="breadcrumb">{breadcrumb_html}</p>
-<h1>Tree{esc_html(title_path)}</h1>
-<table><thead><tr><th>Name</th><th>Type</th></tr></thead>
-<tbody>
-{table_body}
-</tbody></table></body></html>"""
+        f"{pre}<p class='breadcrumb'>{breadcrumb_html}</p><h1>Tree{esc_html(title_path)}</h1>{table}{POSTAMBLE}"
     )
 
 
@@ -406,9 +413,9 @@ def git_forks(project: str, request: Request) -> HTMLResponse:
     for pr in list_:
         path = pr.get("path", "")
         rows.append(f"<tr><td><a href='/{path}'>{esc_html(path)}</a></td></tr>")
+    pre = PREAMBLE.render(title=f"{esc_html(project)} - Forks")
     return HTMLResponse(
-        f"""<!DOCTYPE html><html class="theme-dark"><head><title>Forks - {esc_html(project)}</title>{HTML_HEAD}</head>
-<body><h1>Forks of {esc_html(project)}</h1><table>{"".join(rows)}</table></body></html>"""
+        f"{pre}<h1>Forks of {esc_html(project)}</h1><table>{"".join(rows)}</table>{POSTAMBLE}"
     )
 
 
@@ -455,25 +462,21 @@ def _format_commit_table_row(project: str, commit: dict[str, Any], short: bool =
     
     if short:
         # Shortlog: simpler format
-        return (
-            f'<tr>'
-            f'<td><a href="{commit_link}">{esc_html(oid_short)}</a></td>'
-            f'<td>{esc_html(subject)}</td>'
-            f'<td>{author_display}</td>'
-            f'<td>{esc_html(age_str)}</td>'
-            f'</tr>'
-        )
+        return [
+            f'<a href="{commit_link}">{esc_html(oid_short)}</a>',
+            esc_html(subject),
+            author_display,
+            esc_html(age_str)
+        ]
     else:
         # Full log: more details
-        return (
-            f'<tr>'
-            f'<td><a href="{commit_link}">{esc_html(oid_short)}</a></td>'
-            f'<td>{esc_html(subject)}</td>'
-            f'<td>{author_display} &lt;{esc_html(author_email)}&gt;</td>'
-            f'<td>{esc_html(date_str)}</td>'
-            f'<td>{esc_html(age_str)}</td>'
-            f'</tr>'
-        )
+        return [
+            f'<a href="{commit_link}">{esc_html(oid_short)}</a>',
+            esc_html(subject),
+            author_display,
+            esc_html(date_str),
+            esc_html(age_str)
+        ]
 
 
 def git_log(project: str, h: str | None) -> HTMLResponse:
@@ -486,23 +489,16 @@ def git_log(project: str, h: str | None) -> HTMLResponse:
     for commit in commits:
         rows.append(_format_commit_table_row(project, commit, short=False))
     
-    table_body = "\n".join(rows)
     ref_display = h[:7] if h else "HEAD"
-    title = f"Log - {esc_html(project)}"
-    if h:
-        title += f" @ {esc_html(ref_display)}"
-    
+    title = f"Log - {esc_html(project)} @ {esc_html(ref_display)}"
+
+    pre = PREAMBLE.render(title=title)
+    table = env.get_template("table.html").render(
+        cols=["Commit", "Subject", "Author", "Date", "Age"],
+        rows=rows
+    )
     return HTMLResponse(
-        f"""<!DOCTYPE html><html class="theme-dark"><head><title>{title}</title>{HTML_HEAD}</head>
-<body><h1>Commit log - {esc_html(project)}</h1>
-<table>
-<thead>
-<tr><th>Commit</th><th>Subject</th><th>Author</th><th>Date</th><th>Age</th></tr>
-</thead>
-<tbody>
-{table_body}
-</tbody>
-</table></body></html>"""
+        f"{pre}<h1>{title}</h1>{table}{POSTAMBLE}"
     )
 
 
@@ -516,23 +512,16 @@ def git_shortlog(project: str, h: str | None) -> HTMLResponse:
     for commit in commits:
         rows.append(_format_commit_table_row(project, commit, short=True))
     
-    table_body = "\n".join(rows)
     ref_display = h[:7] if h else "HEAD"
-    title = f"Shortlog - {esc_html(project)}"
-    if h:
-        title += f" @ {esc_html(ref_display)}"
+    title = f"Shortlog - {esc_html(project)} @ {esc_html(ref_display)}"
     
+    pre = PREAMBLE.render(title=title)
+    table = env.get_template("table.html").render(
+        cols=["Commit", "Subject", "Author", "Age"],
+        rows=rows
+    )
     return HTMLResponse(
-        f"""<!DOCTYPE html><html class="theme-dark"><head><title>{title}</title>{HTML_HEAD}</head>
-<body><h1>Shortlog - {esc_html(project)}</h1>
-<table>
-<thead>
-<tr><th>Commit</th><th>Subject</th><th>Author</th><th>Age</th></tr>
-</thead>
-<tbody>
-{table_body}
-</tbody>
-</table></body></html>"""
+        f"{pre}<h1>{title}</h1>{table}{POSTAMBLE}"
     )
 
 
@@ -550,22 +539,16 @@ def git_history(project: str, h: str | None, f: str | None) -> HTMLResponse:
     rows = []
     for commit in commits:
         rows.append(_format_commit_table_row(project, commit, short=False))
-    
-    table_body = "\n".join(rows)
-    ref_display = h[:7] if h else "HEAD"
-    
+
+    ref_display = h[:7] if h else "HEAD"    
+    title=f"History - {esc_html(f)} - {esc_html(project)} @ {esc_html(ref_display)}"
+    pre = PREAMBLE.render(title=title)
+    table = env.get_template("table.html").render(
+        cols=["Commit", "Subject", "Author", "Date", "Age"],
+        rows=rows
+    )
     return HTMLResponse(
-        f"""<!DOCTYPE html><html class="theme-dark"><head><title>History - {esc_html(f)} - {esc_html(project)}</title>{HTML_HEAD}</head>
-<body><h1>History of {esc_html(f)}</h1>
-<p>Project: <a href="/{project}">{esc_html(project)}</a></p>
-<table>
-<thead>
-<tr><th>Commit</th><th>Subject</th><th>Author</th><th>Date</th><th>Age</th></tr>
-</thead>
-<tbody>
-{table_body}
-</tbody>
-</table></body></html>"""
+        f"{pre}<h1>{title}</h1><p>Project: <a href='/{project}'>{esc_html(project)}</a></p>{table}{POSTAMBLE}"
     )
 
 
diff --git a/pygitweb/templates/preamble.html b/pygitweb/templates/preamble.html
new file mode 100644
index 0000000..7574ae3
--- /dev/null
+++ b/pygitweb/templates/preamble.html
@@ -0,0 +1,9 @@
+<!DOCTYPE html><html class="theme-dark"><head><title>{{title}}</title>
+<script id="Highlight-script" async src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.9.0/highlight.min.js"></script>
+<script id="MathJax-script" async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/3.2.2/es5/tex-chtml.min.js"></script>
+<script id="GraphViz-script" async src="https://cdnjs.cloudflare.com/ajax/libs/viz.js/1.8.2/viz.js"></script>
+<link rel="preconnect" href="https://fonts.googleapis.com">
+<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
+<link href="https://fonts.googleapis.com/css2?family=Fira+Code:wght@300..700&family=Fira+Mono:wght@400;500;700&family=Fira+Sans:ital,wght@0,100;0,200;0,300;0,400;0,500;0,600;0,700;0,800;0,900;1,100;1,200;1,300;1,400;1,500;1,600;1,700;1,800;1,900&display=swap" rel="stylesheet">
+<link rel="stylesheet" href="static/main.css"/>
+</head><body>
\ No newline at end of file
diff --git a/pygitweb/templates/table.html b/pygitweb/templates/table.html
new file mode 100644
index 0000000..6f451ec
--- /dev/null
+++ b/pygitweb/templates/table.html
@@ -0,0 +1,13 @@
+<table>
+<tr>{% for col in cols %}
+<th>{{col}}</th>
+{% endfor %}
+</tr>
+<tbody>{% for row in rows %}
+<tr>{% for cell in row %}   
+<td>{{cell}}</td>
+{% endfor %}
+</tr>
+{% endfor %}
+</tbody>
+</table>
