diff --git a/.gitmodules b/.gitmodules
new file mode 100644
index 0000000000000000000000000000000000000000..522acb3108446c625e338adfc5bc78b397d2d1fe
--- /dev/null
+++ b/.gitmodules
@@ -0,0 +1,3 @@
+[submodule "mathjax"]
+	path = mathjax
+	url = https://github.com/mathjax/MathJax.git
diff --git a/Makefile b/Makefile
index bdfdb460dca80e7e2f615ea6373000b0fdd122e3..71ecb485ad059da37b4e4c5730e5e7cc58f78d8d 100644
--- a/Makefile
+++ b/Makefile
@@ -1,9 +1,18 @@
 .PHONY: html
-html: run
+html: build
 
 .PHONY: publish
-publish: run
+publish: build
+
+.PHONY: build
+build: mathjax
+	cd fgs && python3 __main__.py
+
+.PHONY: mathjax
+mathjax:
+	[ ! -d output/mathjax ] && mkdir -p output && cp -vr mathjax/es5 output/mathjax || true
+
+.PHONY: devserver
+devserver:
+	cd output && python -m http.server 8000
 
-.PHONY: run
-run:
-	make -C fgs
diff --git a/docs/Makefile b/docs/Makefile
new file mode 100644
index 0000000000000000000000000000000000000000..86d1c18cbc716e8fc5ce61a8e4e1aabaafbaa075
--- /dev/null
+++ b/docs/Makefile
@@ -0,0 +1,4 @@
+
+content.md: content.json gen_content.py
+	./gen_content.py > content.md
+
diff --git a/docs/content.json b/docs/content.json
new file mode 100644
index 0000000000000000000000000000000000000000..9335e0983c30aca80dbbdaaef38ed01e36db47c2
--- /dev/null
+++ b/docs/content.json
@@ -0,0 +1,156 @@
+{
+  "header": {
+    "eclass": "block",
+    "level": "int",
+    "attr": "attr",
+    "content": "inlines"
+  },
+  "rawblock": {
+    "eclass": "block",
+    "format": "text",
+    "raw": "text"
+  },
+  "bulletlist": {
+    "eclass": "block",
+    "items": "[blocks]",
+    "count": "int"
+  },
+  "orderedlist": {
+    "eclass": "block",
+    "items": "[blocks]",
+    "count": "int",
+    "start": "int",
+    "style": [
+      "default",
+      "example",
+      "decimal",
+      "lower_roman",
+      "upper_roman",
+      "lower_alpha",
+      "upper_alpha"
+    ],
+    "delim": [
+      "default",
+      "period",
+      "one_parenthesis",
+      "two_parentheses"
+    ]
+  },
+  "blockquote": {
+    "eclass": "block",
+    "content": "blocks"
+  },
+  "plain": {
+    "eclass": "block",
+    "content": "inlines"
+  },
+  "paragraph": {
+    "eclass": "block",
+    "content": "inlines"
+  },
+  "codeblock": {
+    "eclass": "block",
+    "attr": "attr",
+    "code": "text",
+    "code_lines": "[text]"
+  },
+  "horizontalrule": {
+    "eclass": "block"
+  },
+  "blockcontainer": {
+    "eclass": "block",
+    "attr": "attr",
+    "content": "blocks"
+  },
+  "space": {
+    "eclass": "inline"
+},
+  "linebreak": {
+    "eclass": "inline"
+},
+  "softbreak": {
+    "eclass": "inline"
+},
+  "string": {
+    "eclass": "inline",
+    "text": "text"
+  },
+  "strong": {
+    "eclass": "inline",
+    "content": "inlines"
+  },
+  "emph": {
+    "eclass": "inline",
+    "content": "inlines"
+  },
+  "underline": {
+    "eclass": "inline",
+    "content": "inlines"
+  },
+  "strikeout": {
+    "eclass": "inline",
+    "content": "inlines"
+  },
+  "superscript": {
+    "eclass": "inline",
+    "content": "inlines"
+  },
+  "subscript": {
+    "eclass": "inline",
+    "content": "inlines"
+  },
+  "smallcaps": {
+    "eclass": "inline",
+    "content": "inlines"
+  },
+  "link": {
+    "eclass": "inline",
+    "attr": "attr",
+    "content": "inlines",
+    "url": "text",
+    "title": "text"
+  },
+  "image": {
+    "eclass": "inline",
+    "attr": "attr",
+    "alt": "inlines",
+    "url": "text",
+    "title": "text"
+  },
+  "quoted": {
+    "eclass": "inline",
+    "quotetype": [
+      "single",
+      "double"
+    ],
+    "content": "inlines"
+  },
+  "math": {
+    "eclass": "inline",
+    "mathtype": [
+      "display",
+      "inline"
+    ],
+    "math": "text"
+  },
+  "code": {
+    "eclass": "inline",
+    "attr": "attr",
+    "code": "text",
+    "code_lines": "[text]"
+  },
+  "inlinecontainer": {
+    "eclass": "inline",
+    "attr": "attr",
+    "content": "inlines"
+  },
+  "rawinline": {
+    "eclass": "inline",
+    "format": "text",
+    "raw": "text"
+  },
+  "footnote": {
+    "eclass": "inline",
+    "content": "blocks"
+  }
+}
diff --git a/docs/content.md b/docs/content.md
new file mode 100644
index 0000000000000000000000000000000000000000..f417821d35c3e0a1a16290bb7f328880a3be92fd
--- /dev/null
+++ b/docs/content.md
@@ -0,0 +1,302 @@
+
+# Content Format
+
+
+
+
+
+
+## Attr
+
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| id | String |  |
+| classes | \[String\] |  |
+| extra | {foo: bar, alice: bob, ...} |  |
+
+
+## Blocks
+
+
+### header
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `header` |  |
+| eclass | `block` |  |
+| level | Integer |  |
+| attr | [Attr](#attr) |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### rawblock
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `rawblock` |  |
+| eclass | `block` |  |
+| format | String |  |
+| raw | String |  |
+
+
+### bulletlist
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `bulletlist` |  |
+| eclass | `block` |  |
+| items | \[\[[Block](#blocks)\]\] |  |
+| count | Integer |  |
+
+
+### orderedlist
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `orderedlist` |  |
+| eclass | `block` |  |
+| items | \[\[[Block](#blocks)\]\] |  |
+| count | Integer |  |
+| start | Integer |  |
+| style | `default` \| `example` \| `decimal` \| `lower_roman` \| `upper_roman` \| `lower_alpha` \| `upper_alpha` |  |
+| delim | `default` \| `period` \| `one_parenthesis` \| `two_parentheses` |  |
+
+
+### blockquote
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `blockquote` |  |
+| eclass | `block` |  |
+| content | \[[Block](#blocks)\] |  |
+
+
+### plain
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `plain` |  |
+| eclass | `block` |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### paragraph
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `paragraph` |  |
+| eclass | `block` |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### codeblock
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `codeblock` |  |
+| eclass | `block` |  |
+| attr | [Attr](#attr) |  |
+| code | String |  |
+| code_lines | \[String\] |  |
+
+
+### horizontalrule
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `horizontalrule` |  |
+| eclass | `block` |  |
+
+
+### blockcontainer
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `blockcontainer` |  |
+| eclass | `block` |  |
+| attr | [Attr](#attr) |  |
+| content | \[[Block](#blocks)\] |  |
+
+
+## Inlines
+
+
+### space
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `space` |  |
+| eclass | `inline` |  |
+
+
+### linebreak
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `linebreak` |  |
+| eclass | `inline` |  |
+
+
+### softbreak
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `softbreak` |  |
+| eclass | `inline` |  |
+
+
+### string
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `string` |  |
+| eclass | `inline` |  |
+| text | String |  |
+
+
+### strong
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `strong` |  |
+| eclass | `inline` |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### emph
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `emph` |  |
+| eclass | `inline` |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### underline
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `underline` |  |
+| eclass | `inline` |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### strikeout
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `strikeout` |  |
+| eclass | `inline` |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### superscript
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `superscript` |  |
+| eclass | `inline` |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### subscript
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `subscript` |  |
+| eclass | `inline` |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### smallcaps
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `smallcaps` |  |
+| eclass | `inline` |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### link
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `link` |  |
+| eclass | `inline` |  |
+| attr | [Attr](#attr) |  |
+| content | \[[Inline](#inlines)\] |  |
+| url | String |  |
+| title | String |  |
+
+
+### image
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `image` |  |
+| eclass | `inline` |  |
+| attr | [Attr](#attr) |  |
+| alt | \[[Inline](#inlines)\] |  |
+| url | String |  |
+| title | String |  |
+
+
+### quoted
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `quoted` |  |
+| eclass | `inline` |  |
+| quotetype | `single` \| `double` |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### math
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `math` |  |
+| eclass | `inline` |  |
+| mathtype | `display` \| `inline` |  |
+| math | String |  |
+
+
+### code
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `code` |  |
+| eclass | `inline` |  |
+| attr | [Attr](#attr) |  |
+| code | String |  |
+| code_lines | \[String\] |  |
+
+
+### inlinecontainer
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `inlinecontainer` |  |
+| eclass | `inline` |  |
+| attr | [Attr](#attr) |  |
+| content | \[[Inline](#inlines)\] |  |
+
+
+### rawinline
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `rawinline` |  |
+| eclass | `inline` |  |
+| format | String |  |
+| raw | String |  |
+
+
+### footnote
+
+| Key | Parser | Comment |
+|-----|--------|---------|
+| etype | `footnote` |  |
+| eclass | `inline` |  |
+| content | \[[Block](#blocks)\] |  |
diff --git a/docs/gen_content.py b/docs/gen_content.py
new file mode 100755
index 0000000000000000000000000000000000000000..6cb27e3262cdbb7d12b5342a5b445e6f87d4e5b1
--- /dev/null
+++ b/docs/gen_content.py
@@ -0,0 +1,83 @@
+#!/usr/bin/env python3
+
+import json
+import os
+
+header = """
+# Content Format
+
+
+
+"""
+
+def print_entry(key, parser_raw):
+    # | key | parser | comment |
+    parser = '`' + str(parser_raw) + '`'
+
+    if parser_raw == "text":
+        parser = "String"
+    elif parser_raw == "int":
+        parser = "Integer"
+    elif parser_raw == "[text]":
+        parser = "\[String\]"
+    elif parser_raw == "attr":
+        parser = "[Attr](#attr)"
+    elif parser_raw == "blocks":
+        parser = "\[[Block](#blocks)\]"
+    elif parser_raw == "[blocks]":
+        parser = "\[\[[Block](#blocks)\]\]"
+    elif parser_raw == "inlines":
+        parser = "\[[Inline](#inlines)\]"
+    elif parser_raw == "kvps":
+        parser = "{foo: bar, alice: bob, ...}"
+    elif isinstance(parser_raw,list):
+        parser = '`' + '` \| `'.join(parser_raw) + '`'
+
+    print("| {key} | {parser} | {comment} |".format(key=key, parser=parser, comment=""))
+
+def print_table_header():
+    print("| Key | Parser | Comment |")
+    print("|-----|--------|---------|")
+
+
+def print_etype(etype, c):
+    print("")
+    print("")
+    print("### {etype}".format(etype=etype))
+    print("")
+    print_table_header()
+    print_entry("etype", etype)
+    for key, value in c.items():
+        print_entry(key, value)
+
+
+if __name__ == '__main__':
+    j = {}
+    with open('content.json') as f:
+        j = json.loads(f.read())
+
+    print(header)
+    print("")
+    print("")
+    print("## Attr")
+    print("")
+    print("")
+    print_table_header()
+    print_entry("id", "text")
+    print_entry("classes", "[text]")
+    print_entry("extra", "kvps")
+
+
+    print("")
+    print("")
+    print("## Blocks")
+    is_blocks = True
+    for etype, c in j.items():
+        if c['eclass'] != 'block' and is_blocks:
+            is_blocks = False
+            print("")
+            print("")
+            print("## Inlines")
+        print_etype(etype, c)
+
+
diff --git a/fgs/Makefile b/fgs/Makefile
index 6cfa260bbf2297c11be043f09ddb62a9ada3e30e..61357d60c72710f07dfe8358631094058ec73efe 100644
--- a/fgs/Makefile
+++ b/fgs/Makefile
@@ -1,4 +1,4 @@
 
 .PHONY: run
 run:
-	python3 __main__.py
+	make -C ..
diff --git a/fgs/page.py b/fgs/page.py
index 801b5959f0795e08ed237509f302746723fe7f96..cc2c7a308a0626e421084cc96b79fd1c4afdf3b9 100644
--- a/fgs/page.py
+++ b/fgs/page.py
@@ -5,7 +5,6 @@ class Page:
     #raw = None
     #metadata = None
     #content = None
-    #toc = None
     #title = None
     #category = None
     #slug = None
@@ -18,13 +17,12 @@ class Page:
     #template = None
     
 
-    def __init__(self, filename, subpath, raw, metadata, content, toc, title, category, slug, lang, date_created, date_modified, authors, last_modification_author, status, config):
+    def __init__(self, filename, subpath, raw, metadata, content, title, category, slug, lang, date_created, date_modified, authors, last_modification_author, status, config):
         self.filename =  filename
         self.subpath =  subpath
         self.raw =  raw
         self.metadata =  metadata
         self.content =  content
-        self.toc =  toc
         self.title =  title
         self.category =  category
         self.slug =  slug
diff --git a/fgs/pandoc.py b/fgs/pandoc.py
new file mode 100644
index 0000000000000000000000000000000000000000..d3ebc20b9518093968246e0a2c2be0c73d53036e
--- /dev/null
+++ b/fgs/pandoc.py
@@ -0,0 +1,436 @@
+#!/usr/bin/env python3
+
+import subprocess
+import json
+import sys
+
+def run_pandoc(source, base="markdown", extensions=[], extra_args=[]):
+    to = "json"
+    ext_str = ""
+    if isinstance(extensions, list):
+        for ext in extensions:
+            if ext.startswith('#'):
+                continue
+            if ext.startswith('+') or ext.startswith('-'):
+                ext_str = ext_str + ext
+            elif len(ext) > 0:
+                ext_str = ext_str + '+' + ext
+    elif isinstance(extensions, dict):
+        for ext_key in extensions:
+            # TODO catch 'illegal' ext_keys (containing spaces for example)
+            ext = extensions[ext_key]
+            if "ignore" in ext and ext["ignore"]:
+                continue
+            flag='+'
+            if "enabled" in ext and not ext["enabled"]:
+                flag='-'
+            ext_str = ext_str + flag + ext_key
+
+    #print(ext_str)
+    pandoc_bin = "pandoc"
+    args = [pandoc_bin, "-f", base + ext_str, "-t", to] + extra_args
+    p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
+    out, _ = p.communicate(source.encode('utf-8', errors='strict'))
+    out_str = out.decode('utf-8')
+    out_dict = json.loads(out.decode('utf-8'))
+
+    if json.dumps(out_dict["pandoc-api-version"]) != "[1, 22, 2]":
+        raise Exception("Unsupported pandoc-api version", out_dict["pandoc-api-version"])
+
+    # parse blocks
+    raw_blocks = out_dict['blocks']
+    blocks = []
+    for raw_block in raw_blocks:
+        #print('raw_block: ', type(raw_block), raw_block)
+        block = parse_from_register(block_parsing_register, raw_block)
+        if block != None:
+            blocks.append(block)
+
+    contentmetadata = {}
+    contentmetadata["toc_list"] = []
+    contentmetadata["toc_count"] = 0
+
+    # TODO TOC
+    #contentmetadata["toc"] = build_toc(n["toc_list"].copy())
+
+    blocks = json.loads(json.dumps(blocks, cls=ElementEncoder)) # Reduce to 'simple' dict, which can be converted to JSON in Jinja2.
+
+    return (blocks, contentmetadata)
+
+def parse_from_register(reg: dict, h: dict):
+    t = h['t'] # pandoc type
+    if t not in reg:
+        raise Exception("pandoc type not in register", t, h)
+    entry = reg[t] # registry entry
+
+    c = None
+    if 'c' in h:
+        c = h['c']
+
+    res = None
+
+    if isinstance(entry, dict):
+        if "TODO" in entry and entry["TODO"]:
+            print("Warning: entry is marked as TODO: ",t, entry,file=sys.stderr)
+            return None
+
+        handler = entry['handler']
+        res = handler(entry['etype'])
+    else:
+        handler = entry
+        res = handler()
+
+    res.parse(c)
+
+    return res
+
+
+class ElementEncoder(json.JSONEncoder):
+    def default(self, obj):
+        if isinstance(obj, Element):
+            res = {}
+            for key, name in obj.export.items():
+                res[name] = getattr(obj, key)
+            return res
+        return super().default(obj)
+
+class Element():
+    def __init__(self, etype = None):
+        if etype != None:
+            self.etype = etype
+        self.children = []
+        self.export = {}
+
+        self.export_key('etype', 'type')
+        self.export_key('eclass', 'class')
+        self.export_key('children')
+
+    def addChild(self, child):
+        if child != None:
+            self.children.append(child)
+
+    def parse_internal(self, pandocraw):
+        raise Exception("parse_internal not overridden: ", self)
+
+    def parse(self, pandocraw):
+        prevkeys = dir(self)
+        self.parse_internal(pandocraw)
+        afterkeys = dir(self)
+        for key in afterkeys:
+            if key not in prevkeys:
+                self.export_key(key)
+
+
+    def export_key(self, key, name=None):
+        if name == None:
+            name = key
+        self.export[key] = name
+
+
+
+    def parse_blocks(self, raw_blocks):
+        if not isinstance(raw_blocks, list):
+            raise Exception("raw_blocks is not a list: ", raw_blocks)
+        res = []
+        for raw_block in raw_blocks:
+            block = self.parse_block(raw_block)
+            if block != None:
+                res.append(block)
+        return res
+    def parse_inlines(self, raw_inlines):
+        if not isinstance(raw_inlines, list):
+            raise Exception("raw_inlines is not a list: ", raw_inlines)
+        res = []
+        for raw_inline in raw_inlines:
+            inline = self.parse_inline(raw_inline)
+            if inline != None:
+                res.append(inline)
+        return res
+
+    def parse_block(self, raw_block):
+        res = parse_from_register(block_parsing_register, raw_block)
+        self.addChild(res)
+        return res
+    def parse_inline(self, raw_inline):
+        res = parse_from_register(inline_parsing_register, raw_inline)
+        self.addChild(res)
+        return res
+
+    def parse_attr(self, raw_attr):
+        #print("called parse_attr: ", raw_attr)
+        res = {}
+        res['id'] = self.parse_text(raw_attr[0])
+        classes = []
+        for c in raw_attr[1]:
+            classes.append(self.parse_text(c))
+        res['classes'] = classes
+        # convert [ "key1", "value1", "key2", "value2" ] to {"key1":"value1", "key2", "value2"}
+        it = iter(raw_attr[2])
+        kvp = dict(zip(it, it)) # key-value pairs
+        extra = {}
+        for key, value in kvp.items():
+            res[key] = self.parse_text(value)
+        res['extra'] = extra
+
+        return res
+
+    def parse_text(self, raw_text):
+        if len(raw_text) > 0:
+            return raw_text
+        else:
+            return None
+
+    def parse_int(self, raw_num):
+        return raw_num # TODO
+
+    def parse_target(self, raw_target): # For URLs
+        res = {}
+        res['url'] = self.parse_text(raw_target[0])
+        res['title'] = self.parse_text(raw_target[1])
+        return res
+
+    def parse_code(self, code):
+        res = {}
+        res["code"] = self.parse_text(code)
+        res["code_lines"] = code.splitlines()
+        return res
+
+    def parse_enum(self, mapping, enum):
+        if len(enum.keys()) != 1 or "t" not in enum:
+            raise Exception("enum is not a valid enum", enum, mapping)
+        enum = enum["t"]
+        if enum not in mapping:
+            raise Exception("enum not found in mapping")
+        return mapping[enum]
+
+    def update(self, d): # Like dict.update()
+        for key, value in d.items():
+            setattr(self, key, value)
+
+
+
+################################ BLOCK #########################################
+
+class Block(Element):
+    eclass = "block"
+
+
+class BlockHeader(Block): # Int Attr [Inline]
+    etype = "header"
+    def parse_internal(self, pandocraw):
+        self.level = self.parse_int(pandocraw[0])
+        self.attr = self.parse_attr(pandocraw[1])
+        self.content = self.parse_inlines(pandocraw[2])
+
+class BlockRaw(Block): # Format Text
+    etype = "rawblock"
+    def parse_internal(self, pandocraw):
+        self.format = self.parse_text(pandocraw[0])
+        self.raw = self.parse_text(pandocraw[1])
+
+class BlockList(Block): # [[Block]]
+    def parse_listitems(self, rawitems):
+        res = {}
+        res['items'] = []
+        for itemrawblocks in rawitems:
+            item = self.parse_blocks(itemrawblocks)
+            res['items'].append(item)
+        res['count'] = len(res['items'])
+        return res
+
+class BlockBulletList(BlockList): # [[Block]]
+    etype = "bulletlist"
+    def parse_internal(self, pandocraw):
+        self.update(self.parse_listitems(pandocraw))
+
+class BlockOrderedList(BlockList): # ListAttributes [[Block]]
+    etype = "orderedlist"
+    def parse_internal(self, pandocraw):
+        self.update(self.parse_orderedlist_attr(pandocraw[0]))
+        self.update(self.parse_listitems(pandocraw[1]))
+
+    def parse_orderedlist_attr(self, attrs: list):
+        res = {}
+        res["start"] = self.parse_int(attrs[0])
+
+        styles = {
+            "DefaultStyle": "default",
+            "Example"     : "example"     ,
+            "Decimal"     : "decimal"     ,
+            "LowerRoman"  : "lower_roman"  ,
+            "UpperRoman"  : "upper_roman"  ,
+            "LowerAlpha"  : "lower_alpha"  ,
+            "UpperAlpha"  : "upper_alpha"  ,
+        }
+        res["style"] = self.parse_enum(styles, attrs[1])
+
+        delims = {
+            "DefaultDelim" : "default"         ,
+            "Period"       : "period"          ,
+            "OneParen"     : "one_parenthesis" ,
+            "TwoParens"    : "two_parentheses" ,
+        }
+        res["delim"] = self.parse_enum(delims, attrs[2])
+
+        return res
+
+class BlockQuote(Block): # [Block]
+    etype = "blockquote"
+    def parse_internal(self, pandocraw):
+        self.content = self.parse_blocks(pandocraw)
+        # TODO add name, color, time
+
+class BlockPlain(Block): # [Inline]
+    etype = "plain"
+    def parse_internal(self, pandocraw):
+        self.content = self.parse_inlines(pandocraw)
+
+class BlockParagraph(Block): # [Inline]
+    etype = "paragraph"
+    def parse_internal(self, pandocraw):
+        self.content = self.parse_inlines(pandocraw)
+
+class BlockCode(Block): # Attr Text
+    etype = "codeblock"
+    def parse_internal(self, pandocraw):
+        self.attr = self.parse_attr(pandocraw[0])
+        self.update(self.parse_code(pandocraw[1]))
+
+class BlockHorizontalRule(Block): #
+    etype = "horizontalrule"
+    def parse_internal(self, pandocraw):
+        pass
+
+class BlockContainer(Block): # Attr [Block]
+    # a div
+    etype = "blockcontainer"
+    def parse_internal(self, pandocraw):
+        self.attr = self.parse_attr(pandocraw[0])
+        self.content = self.parse_blocks(pandocraw[1])
+        # TODO handle alerts
+
+############################## INLINE #########################################
+
+class Inline(Element):
+    eclass = "inline"
+
+class InlineSpace(Inline): #
+    etype = "space"
+    def parse_internal(self, pandocraw):
+        pass
+
+class InlineLineBreak(Inline): #
+    etype = "linebreak"
+    def parse_internal(self, pandocraw):
+        pass
+
+class InlineSoftBreak(Inline): #
+    etype = "softbreak"
+    def parse_internal(self, pandocraw):
+        pass
+
+class InlineString(Inline): # Text
+    etype = "string"
+    def parse_internal(self, pandocraw):
+        self.text = self.parse_text(pandocraw)
+        # TODO handle abbreviations
+
+class InlineSimple(Inline): # [Inline]
+    def __init__(self, etype):
+        self.etype = etype
+        super().__init__()
+
+    def parse_internal(self, pandocraw):
+        self.content = self.parse_inlines(pandocraw)
+
+class InlineLink(Inline): # Attr [Inline] Target
+    etype = "link"
+    def parse_internal(self, pandocraw):
+        self.attr = self.parse_attr(pandocraw[0])
+        self.content = self.parse_inlines(pandocraw[1])
+        self.update(self.parse_target(pandocraw[2]))
+
+class InlineImage(Inline): # Attr [Inline] Target
+    etype = "image"
+    def parse_internal(self, pandocraw):
+        self.attr = self.parse_attr(pandocraw[0])
+        self.alt = self.parse_inlines(pandocraw[1])
+        self.update(self.parse_target(pandocraw[2]))
+
+class InlineQuoted(Inline): # QuoteType Text
+    etype = "quoted"
+    def parse_internal(self, pandocraw):
+        self.quotetype = self.parse_enum({"SingleQuote": "single", "DoubleQuote": "double"}, pandocraw[0])
+        self.content = self.parse_inlines(pandocraw[1])
+
+class InlineMath(Inline): # MathType Text
+    etype = "math"
+    def parse_internal(self, pandocraw):
+        self.mathtype = self.parse_enum({"DisplayMath": "display", "InlineMath": "inline"}, pandocraw[0])
+        self.math = self.parse_text(pandocraw[1])
+
+class InlineCode(Inline): # Attr Text
+    etype = "code"
+    def parse_internal(self, pandocraw):
+        self.attr = self.parse_attr(pandocraw[0])
+        self.update(self.parse_code(pandocraw[1]))
+
+class InlineContainer(Inline): # Attr [Inline]
+    etype = "inlinecontainer"
+    def parse_internal(self, pandocraw):
+        self.attr = self.parse_attr(pandocraw[0])
+        self.content = self.parse_inlines(pandocraw[1])
+        # TODO handle emojis
+
+class InlineFootnote(Inline): # [Block]
+    etype = "footnote"
+    def parse_internal(self, pandocraw):
+        self.content = self.parse_blocks(pandocraw)
+        # TODO add footnote ids # TODO add back references # TODO handle duplicates
+
+class InlineRaw(Inline): # Format Text
+    etype = "rawinline"
+    def parse_internal(self, pandocraw):
+        self.format = self.parse_text(pandocraw[0])
+        self.raw = self.parse_text(pandocraw[1])
+
+
+inline_parsing_register = {
+    "Space"      : InlineSpace,
+    "Str"        : InlineString,
+    "Strong"     :{"handler" : InlineSimple, "etype":"strong"     },
+    "Emph"       :{"handler" : InlineSimple, "etype":"emph"       },
+    "Underline"  :{"handler" : InlineSimple, "etype":"underline"  },
+    "Strikeout"  :{"handler" : InlineSimple, "etype":"strikeout"  },
+    "Superscript":{"handler" : InlineSimple, "etype":"superscript"},
+    "Subscript"  :{"handler" : InlineSimple, "etype":"subscript"  },
+    "SmallCaps"  :{"handler" : InlineSimple, "etype":"smallcaps"  },
+    "Link"       : InlineLink,
+    "Image"      : InlineImage,
+    "Quoted"     : InlineQuoted,
+    "Math"       : InlineMath,
+    "Code"       : InlineCode,
+    "Span"       : InlineContainer,
+    "RawInline"  : InlineRaw,
+    "Note"       : InlineFootnote,
+    #"Cite"       :{"type":"citation","TODO": True, "c" : [] }, # [Citation] [Inline] # TODO find file that triggers Cite
+    "SoftBreak"  : InlineSoftBreak,
+    "LineBreak"  : InlineLineBreak,
+}
+block_parsing_register = {
+    "Plain"         : BlockPlain,
+    "Para"          : BlockParagraph,
+    "BlockQuote"    : BlockQuote,
+    "BulletList"    : BlockBulletList,
+    "RawBlock"      : BlockRaw,
+    "Header"        : BlockHeader,
+    "CodeBlock"     : BlockCode,
+    "Div"           : BlockContainer,
+    "OrderedList"   : BlockOrderedList,
+    "HorizontalRule": BlockHorizontalRule,
+    "Table"         : {"TODO": True,}, # Attr Caption [ColSpec] TableHead [TableBody] TableFoot
+    "DefinitionList": {"TODO": True,}, # [([Inline], [[Block]])]
+    #"LineBlock"     :{"type":"lineblock",     "TODO": True, "c" : [] }, # [[Inline]] # TODO find file that triggers LineBlock
+    #"Null"          :{"type":"nothing" }, # TODO find file that triggers Null
+}
+
diff --git a/fgs/pandoc_toc.html b/fgs/pandoc_toc.html
deleted file mode 100644
index 2fd5a7bd68b1d9474ccc9e42a8dc058a8801e06b..0000000000000000000000000000000000000000
--- a/fgs/pandoc_toc.html
+++ /dev/null
@@ -1 +0,0 @@
-$table-of-contents$
diff --git a/fgs/reader.py b/fgs/reader.py
index 9cc14065d9781c638546842e82415482dac9354a..1e8058f379df96aefb334802b74bec0aadc1f4bc 100644
--- a/fgs/reader.py
+++ b/fgs/reader.py
@@ -1,4 +1,5 @@
 import page
+import pandoc
 
 import frontmatter
 
@@ -24,22 +25,19 @@ class MarkdownReader:
         print("parsing file: ", path, subpath)
 
         f = open(path)
-        rawcontent = f.read()
+        rawfile = f.read()
         f.close()
-        metadata, _ = frontmatter.parse(rawcontent)
+        metadata, rawcontent = frontmatter.parse(rawfile)
 
         #print(metadata)
 
         category_name = self.get_category_name(metadata, subpath)
 
         # content
-        content = self.run_pandoc(rawcontent, self.config['pandoc']['base'], self.config['pandoc']['extensions'], "html5")
+        content, contentmetadata = pandoc.run_pandoc(source=rawcontent, base=self.config['pandoc']['base'], extensions=self.config['pandoc']['extensions'])
+        metadata.update(contentmetadata) # merge content specific metadata into metadata
         #print(content)
 
-        # TOC
-        toc = self.run_pandoc(rawcontent, self.config['pandoc']['base'], self.config['pandoc']['extensions'], "html5", ["--template", "./pandoc_toc.html", "--toc", "--toc-depth", str(self.config['toc_depth'])])
-        #print((toc))
-    
         # title
         if 'title' not in metadata:
             raise Exception("File is missing title in metadata: ", path, subpath)
@@ -117,10 +115,9 @@ class MarkdownReader:
         p = page.Page(
                 filename,
                 subpath,
-                rawcontent,
+                rawfile,
                 metadata,
                 content,
-                toc,
                 title,
                 category_name,
                 slug,
@@ -182,37 +179,3 @@ class MarkdownReader:
         return out_str
 
 
-    def run_pandoc(self, source, base="markdown", extensions=[], to="json", extra_args=[]):
-        ext_str = ""
-        if isinstance(extensions, list):
-            for ext in extensions:
-                if ext.startswith('#'):
-                    continue
-                if ext.startswith('+') or ext.startswith('-'):
-                    ext_str = ext_str + ext
-                elif len(ext) > 0:
-                    ext_str = ext_str + '+' + ext
-        elif isinstance(extensions, dict):
-            for ext_key in extensions:
-                # TODO catch 'illegal' ext_keys (containing spaces for example)
-                ext = extensions[ext_key]
-                if "ignore" in ext and ext["ignore"]:
-                    continue
-                flag='+'
-                if "enabled" in ext and not ext["enabled"]:
-                    flag='-'
-                ext_str = ext_str + flag + ext_key
-    
-        #print(ext_str)
-        pandoc_bin = "pandoc"
-        args = [pandoc_bin, "-f", base + ext_str, "-t", to] + extra_args
-        p = subprocess.Popen(args, stdin=subprocess.PIPE, stdout=subprocess.PIPE)
-        out, _ = p.communicate(source.encode('utf-8', errors='strict'))
-        out_str = out.decode('utf-8')
-        #print("----------------------")
-        #print(out_str)
-        #print("----------------------")
-        #json_dict = json.loads(out.decode('utf-8'))
-        return out_str
-
-
diff --git a/lang.json b/lang.json
index 56055dc73d694c7ec93b3c53a18cc55145f2511f..5382dd7e64cbbbb75c24d5b34bd2ab7ece7ca586 100644
--- a/lang.json
+++ b/lang.json
@@ -48,6 +48,16 @@
 		"only_pages": {
 			"title": "Seiten"
 		},
-		"no_content": "Diese Seite hat noch keine Inhalte."
+		"no_content": "Diese Seite hat noch keine Inhalte.",
+		"quotations": {
+			"double": {
+				"left": "„",
+				"right": "“"
+			},
+			"single": {
+				"left": "‚",
+				"right": "‘"
+			}
+		}
 	}
 }
diff --git a/mathjax b/mathjax
new file mode 160000
index 0000000000000000000000000000000000000000..600692ad9d3552cc25f85510d5797bc942ecc9f7
--- /dev/null
+++ b/mathjax
@@ -0,0 +1 @@
+Subproject commit 600692ad9d3552cc25f85510d5797bc942ecc9f7
diff --git a/theme/old-templates/article.html b/theme/old-templates/article.html
new file mode 100644
index 0000000000000000000000000000000000000000..1a2c28ab4adf8e48fd37a66eaa57e7d741091353
--- /dev/null
+++ b/theme/old-templates/article.html
@@ -0,0 +1,69 @@
+{% extends "base.html" %}
+
+{%- macro article_translation_link(translation, is_current=False) -%}
+<a href="{{ SITEURL }}/{{ translation.url }}" hreflang="{{ translation.lang }}" title="{{ l[translation.lang].langname|e }}">
+	<img alt="{{ l[translation.lang].langname|e }}" src="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/images/flags/{{ translation.lang }}.svg">
+</a>
+{%- endmacro -%}
+
+{% block html_lang %}{{ article.lang }}{% endblock %}
+
+{% block title %}{{ article.title|striptags }}{% endblock %}
+
+{% block extra_head %}
+	{%- for translation in article.translations -%}
+		<link rel="alternate" hreflang="{{ translation.lang }}" href="{{ SITEURL }}/{{ translation.url }}">
+	{%- endfor -%}
+
+	{% if article.summary %}
+		<meta name="description" content="{{ article.summary | striptags | safe | truncate(150) }}" />
+	{% endif %}
+{% endblock %}
+
+{% block content %}
+	<article>
+		<header>
+			<h1>{{ article.title }}</h1>
+		</header>
+		{{ article.content }}
+		<footer>
+			{%- if article.authors -%}
+			<address>
+				{{ l[lang].article.authors_prefix }}
+				{% for author in article.authors %}
+					<a href="{{ SITEURL }}/{{ author.url }}">{{ author }}</a>
+				{% endfor %}
+				{{ l[lang].article.authors_suffix }}
+			</address>
+			{%- endif -%}
+				
+			<span>{{ l[lang].article.published_prefix }}
+			<abbr title="{{ article.date.isoformat() }}">
+				{{ article.locale_date }}
+			</abbr>{{ l[lang].article.published_suffix }}</span>
+
+			{%- if article.modified -%}
+			<br />
+			<span>{{ l[lang].article.modified_prefix }}
+			<abbr title="{{ article.modified.isoformat() }}">{{ article.locale_modified }}</abbr>
+			{{ l[lang].article.modified_suffix }}</span>
+			{%- endif -%}
+
+			<br />
+			<span>{{ l[lang].article.category_prefix }}<a href="{{ SITEURL }}/{{ article.category.url }}">{{ article.category }}</a>{{ l[lang].article.category_suffix }}</span>
+			<br />
+			<span>{{ l[lang].article.languages_prefix }}</span>
+			<ul class="languages">
+			{%- for translation in article.translations -%}
+				<li>
+					{{ article_translation_link(translation) }}
+				</li>
+			{%- endfor -%}
+				<li>
+					{{ article_translation_link(article, True) }}
+				</li>
+			</ul>
+			{{ l[lang].article.languages_suffix }}
+		</footer>
+	</article>
+{% endblock %}
diff --git a/theme/old-templates/base.html b/theme/old-templates/base.html
new file mode 100644
index 0000000000000000000000000000000000000000..6466a8f7566c435c8e4cba8909ab67d03bbc5a38
--- /dev/null
+++ b/theme/old-templates/base.html
@@ -0,0 +1,92 @@
+<!DOCTYPE html>
+{#- TODO lang irgendwie vernünftig setzen/erkennen -#}
+{%- if lang is not defined -%}
+	{%- set lang = DEFAULT_LANG -%}
+{%- else -%}
+Hurra!!! lang ist definiert als {{ lang }}.
+{{ diese_variable_existiert_nicht_werfe_fehler }}
+{%- endif -%}
+
+{%- import 'macros/getters.html' as get with context -%}
+{%- import 'macros/cards.html' as cards with context -%}
+{%- import 'macros/renderers.html' as render with context -%}
+
+<html lang="{%- block html_lang -%}{{ DEFAULT_LANG }}{%- endblock html_lang -%}">
+<head>
+	{% block head %}
+	<meta charset="utf-8" />
+	<meta name="viewport" content="width=device-width, initial-scale=1.0">
+	<title>{% block title %}{{ l[lang].title_prefix }}{{ l[lang].sitename }}{{ l[lang].title_suffix }}{%endblock%}</title>
+	<!-- <base target="_blank"> -->
+	<!-- <meta HTTP-EQUIV="REFRESH" content="500; url=#"> -->
+	<link rel="preload" href="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/css/{{ CSS_FILE }}" as="style" />
+	<link rel="stylesheet" type="text/css" href="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/css/{{ CSS_FILE }}" />
+	{#- TODO load javascript? -#}
+	{#- TODO og: meta tags -#}
+	{#- TODO favicon -#}
+	<!-- <link rel="icon" type="image/png" sizes="192x192" href="static/img/favicon-192x192.png"/>
+	<link rel="icon" type="image/png" sizes="32x32" href="static/img/favicon-32x32.png"/>-->
+	<!-- <link rel="icon" type="image/x-icon" sizes="16x16" href="static/img/favicon.ico"/> -->
+	{%- if FEED_ALL_ATOM %}
+	<link href="{{ FEED_DOMAIN }}/{% if FEED_ALL_ATOM_URL %}{{ FEED_ALL_ATOM_URL }}{% else %}{{ FEED_ALL_ATOM }}{% endif %}" type="application/atom+xml" rel="alternate" title="{{ l[lang].atom.title|e }}" />
+	{% endif -%}
+	{% block extra_head %}{% endblock extra_head %}
+	{% endblock head %}
+</head>
+<body>
+	<header>
+		{% block header %}
+		<h1><a href="{{ SITEURL }}/" title="{{ l[lang].banner.title|e }}" ><img alt="{{ l[lang].banner.alt|e }}" src="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/images/banner-logo.png"><span>{{ l[lang].banner.prefix|e }}{{ l[lang].sitename|e }}{{ l[lang].banner.suffix|e }}</span></a></h1>
+		{% block extra_header %}{% endblock extra_header %}
+		{% endblock header %}
+	</header>
+	<div class="nav-container">
+		<label for="show-header-menu" class="show-header-menu">&#9776;</label>
+		<input type="checkbox" id="show-header-menu" role="button">
+		<label for="show-header-menu" class="show-header-menu-bg"> </label>
+		<nav>
+			<ul>
+			{% for item in sc.menuitems -%}
+				{%- if item.category is defined -%}
+					{%- call(nativecat, sccat, _) get.category_by_name(item.category) -%}
+						<li style="--category-color: {{ sccat.color }}"><a href="{{ SITEURL }}/{{ nativecat.url }}">{{ sccat[lang]|e }}</a></li>
+					{%- endcall -%}
+				{%- elif item.tag is defined -%}
+					{%- call(nativetag, sccat, _) get.tag_by_name(item.tag) -%}
+						<li style="--category-color: {{ sccat.color }}"><a href="{{ SITEURL }}/{{ nativetag.url }}">{{ sccat[lang]|e }}</a></li>
+					{%- endcall -%}
+				{%- elif item.slug is defined -%}
+					{%- call(aop, _) get.article_or_page_by_slug(item.slug, lang) -%}
+					{%- call(__, sccat, ___) get.category_by_name(aop.category.name, True) -%}
+						<li style="--category-color: {{ sccat.color }}"><a href="{{ SITEURL }}/{{ aop.url }}">{{ aop.title }}</a></li>
+					{%- endcall -%}
+					{%- endcall -%}
+				{%- else -%}
+					<br />
+					<strong>ERROR: menuitems: Cannot parse item: {{ item|string|e }}</strong><br />
+					<br />
+				{%- endif -%}
+				{#- TODO add active class if this is the current site -#}
+			{%- endfor %}
+			</ul>
+		</nav>
+	</div>
+	<main>
+	{% block content %}
+	{% endblock content %}
+	</main>
+	<footer>
+		{% block footer %}
+		{#- TODO besserer footer -#}
+		<div>
+			<a href="https://asta.uni-goettingen.de/impressum/datenschutz/">Datenschutz</a>
+			<a href="/fg-website/Impressum.md">Impressum</a>
+		<div>
+		<div>Fachgruppe Informatik Göttingen, 2022.</div>
+		<!-- blablabla datenschutz,impressum etc... -->
+		{% block extra_footer %}{% endblock extra_footer %}
+		{% endblock footer %}
+	</footer>
+</body>
+<!-- The Cake Is A Lie! -->
+</html>
diff --git a/theme/old-templates/category.html b/theme/old-templates/category.html
new file mode 100644
index 0000000000000000000000000000000000000000..0c2b6e0ff9f6a25c9462dc5910bfcccf62a7aaf7
--- /dev/null
+++ b/theme/old-templates/category.html
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+{% block title %}{{ l[lang].title_prefix }}{{ l[lang].sitename }}{{ l[lang].title_suffix }} - {{ category }}{%endblock%}
+{% block content %}
+	<section>
+		{{ render.section({"type": "category", "category": category.slug, "num": None}) }}
+	</section>
+{% endblock content %}
diff --git a/theme/old-templates/index.html b/theme/old-templates/index.html
new file mode 100644
index 0000000000000000000000000000000000000000..d4f2b290de95d8c0f5e42938e0348405c51773ec
--- /dev/null
+++ b/theme/old-templates/index.html
@@ -0,0 +1,11 @@
+{% extends "base.html" %}
+{% block content_title %}{% endblock %}
+
+
+{% block content %}
+{%- for s in sc.startpage -%}
+	<section>
+		{{ render.section(s) }}
+	</section>
+{%- endfor -%}
+{% endblock content %}
diff --git a/theme/old-templates/macros/README.md b/theme/old-templates/macros/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..75ed3122989219278ca7cac37db4a07f01cc2e7a
--- /dev/null
+++ b/theme/old-templates/macros/README.md
@@ -0,0 +1,184 @@
+# Macros
+
+[TOC]
+
+## [getters.html](getters.html)
+
+```
+{%- import 'macros/getters.html' as get with context -%}
+```
+
+Mit diesen Macros können Informationen abgefragt werden.
+
+### `category_by_name(catname, ignore_native = False)`
+
+**Argumente:**
+
+- `catname` (String): Der Name der Kategorie.
+- `ignore_native` (Boolean): Setzt den ersten und dritten Rückgabewert zu `None`. (Verkürzt die Laufzeit dieser Funktion.)
+
+**Rückgabewerte:**
+
+1. ([Category](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#object-author-cat-tag)): Die native Kategorie.
+2. (Dict): Der Eintrag zu der Kategorie aus der config.json-Datei aus dem Inhaltsrepo.
+3. (List\<[Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article)\>): Die Artikel in der Kategorie.
+
+**Beispiel Aufruf:**
+
+```
+{%- call(nativecat, sccat, catarticles) get.category_by_name(article.category.name) -%}
+	<ul>
+		<li>Kategorie: {{ nativecat.name }}</li>
+		<li>Farbe: {{ sccat.color }}</li>
+		<li>Anzahl Artikel: {{ catarticles|length }}</li>
+	</ul>
+{%- endcall -%}
+```
+
+### `tag_by_name(tagname)`
+
+**Argumente:**
+
+- `catname` (String): Der Name des Tags.
+
+**Rückgabewerte:**
+
+1. ([Tag](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#object-author-cat-tag)): Der native Tag.
+2. (Dict): Falls der Tag gleichnamig zu einer Kategorie ist, dann der Eintrag zu der Kategorie aus der config.json-Datei aus dem Inhaltsrepo, sonst `None`.
+3. (List\<[Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article)\>): Die Artikel die diesen Tag haben.
+
+**Beispiel Aufruf:**
+
+```
+{%- call(nativetag, sccat, tagarticles) get.tag_by_name("event") -%}
+	<ul>
+		<li>Tag: {{ nativetag.name }}</li>
+		<li>Farbe: {% if sccat %}{{ sccat.color }}{% else %}Dieser Tag ist nicht gleichnamig zu einer Kategorie und hat deshalb keine Farbe.{% endif %}</li>
+		<li>Anzahl Artikel mit diesem Tag: {{ tagarticles|length }}</li>
+	</ul>
+{%- endcall -%}
+```
+
+
+### `article_by_slug(slug, lang)`
+
+**Argumente:**
+
+- `slug` (String): Der Slug von dem Artikel.
+- `lang` (String): Die Sprache von dem Artikel.
+
+**Rückgabewerte:**
+
+1. ([Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article)): Der Artikel.
+
+### `page_by_slug(slug, lang)`
+
+**Argumente:**
+
+- `slug` (String): Der Slug von der Seite.
+- `lang` (String): Die Sprache von der Seite.
+
+**Rückgabewerte:**
+
+1. ([Page](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#page)): Die Seite.
+
+### `article_or_page_by_slug(slug, lang)`
+
+**Argumente:**
+
+- `slug` (String): Der Slug von dem Artikel oder der Seite.
+- `lang` (String): Die Sprache von dem Artikel oder der Seite.
+
+**Rückgabewerte:**
+
+1. ([Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article) | [Page](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#page)): Der Artikel oder die Seite.
+2. (String): "article" falls es ein Artikel ist. "page" falls es eine Seite ist.
+
+## [cards.html](cards.html)
+
+```
+{%- import 'macros/cards.html' as cards with context -%}
+```
+
+Mit diesen Macros können Kachelblöcke erstellt werden.
+
+In einem Kachelblock dürfen nur Kacheln sein und sonst nichts!
+
+**Beispiel:**
+
+```
+{{ cards.open() }}
+
+{%- for article in all_articles -%}
+	{{ cards.card_from_article_or_page(article) }}
+{%- endfor -%}
+
+{{ cards.card(title="Dies ist auch eine Kachel", url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", catcolor="green") }}
+
+{{ cards.close() }}
+```
+
+### `open(id = None, classes = None)`
+
+Beginnt einen Kachelblock.
+
+**Argumente:**
+
+- `id` (String): Die ID von dem Kachelblock.
+- `classes` (List\<String\>): Zusätzliche Klassen für den Kachelblock.
+
+### `close()`
+
+Schließt einen Kachelblock.
+
+### `card(title, url, catcolor, escape_title = True, is_url_external = True)`
+
+Erstellt eine Kachel.
+
+**Argumente:**
+
+- `title` (String): Der Titel.
+- `url` (String): Der URL.
+- `catcolor` (String): Die Farbe.
+- `escape_title` (Boolean): Ob `title` escaped werden soll.
+- `is_url_external` (Boolean): Ob `url` auf eine externe Resource zeigt, oder als relativen Link interpretiert werden soll.
+
+### `card_from_article_or_page(aop)`
+
+Erstellt eine Kachel von einem Artikel oder einer Seite.
+
+**Argumente:**
+
+1. `aop` ([Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article) | [Page](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#page)): Der Artikel oder die Seite.
+
+
+### `cards_from_articles_or_pages(asops, max = None, standalone = True)`
+
+Erstellt mehrere Kacheln oder einen vollständigen Kachelblock von mehreren Artikeln oder Seiten.
+
+**Argumente:**
+
+1. `asops` (List\<[Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article) | [Page](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#page)\>): Die Artikel oder Seiten. Kann auch gemischt sein.
+2. `max` (Int | None): Falls vorhanden werden maximal `max` Kacheln gerendert.
+3. `standalone` (Boolean): Ob `open()` und `close()` automatisch mit ausgeführt werden soll.
+
+**Beispiel Aufruf:**
+
+```
+{{ cards_from_articles_or_pages(all_articles, max=5) }}
+```
+
+## [renderers.html](renderers.html)
+
+```
+{%- import 'macros/renderers.html' as render with context -%}
+```
+
+**TODO: ** Die Dokumentation für diese Macros ist abhängig von der Dokumentation von der config.json-Datei im Inhaltsrepo.
+
+### `section_news(s)`
+### `section_custom(s)`
+### `section_iframe(s)`
+### `section_category(s)`
+### `section_tag(s)`
+### `section(s)`
diff --git a/theme/old-templates/macros/cards.html b/theme/old-templates/macros/cards.html
new file mode 100644
index 0000000000000000000000000000000000000000..bea528c91af935481ca7b08df074b8c83a0c8876
--- /dev/null
+++ b/theme/old-templates/macros/cards.html
@@ -0,0 +1,35 @@
+{%- import 'macros/getters.html' as get with context -%}
+
+
+{%- macro open(id = None, classes = None) -%}
+	<ul {%- if id %} id="{{ id|e }}"{% endif %} class="cards {%- if classes %}{% for class in classes %} {{ class|e }}{% endfor %}{% endif %}">
+{%- endmacro -%}
+
+{%- macro close() -%}
+	</ul>
+{%- endmacro -%}
+
+{%- macro card(title, url, catcolor, escape_title = True, is_url_external = True) -%}
+	<li style="--category-color: {{ catcolor }}"><a href="{% if not is_url_external %}{{ SITEURL }}/{% endif %}{{ url }}">{% if escape_title %}{{ title|e }}{% else %}{{ title }}{% endif %}</a></li>
+{%- endmacro -%}
+
+{%- macro card_from_article_or_page(aop) -%}
+	{%- call(_, sccat, __) get.category_by_name(aop.category.name, True) -%}
+		{{ card(title=aop.title, url=aop.url, catcolor=sccat.color, escape_title=False, is_url_external = False) }}
+	{%- endcall -%}
+{%- endmacro -%}
+
+{%- macro cards_from_articles_or_pages(asops, max = None, standalone = True) -%}
+	{%- if standalone -%}
+		{{ open() }}
+	{%- endif -%}
+	{%- for aop in asops -%}
+		{%- if max == None or loop.index < max -%}
+			{{ cards.card_from_article_or_page(aop) }}
+		{%- endif -%}
+	{%- endfor -%}
+	{%- if standalone -%}
+		{{ close() }}
+	{%- endif -%}
+{%- endmacro -%}
+
diff --git a/theme/old-templates/macros/getters.html b/theme/old-templates/macros/getters.html
new file mode 100644
index 0000000000000000000000000000000000000000..1c1088d4e26ed72f34c271193e9883acf3d3a986
--- /dev/null
+++ b/theme/old-templates/macros/getters.html
@@ -0,0 +1,53 @@
+{%- macro category_by_name(catname, ignore_native = False) -%}
+	{%- if ignore_native -%}
+		{{- caller(None, sc.categories[catname], None) -}}
+	{%- else -%}
+		{%- for nativecat, catarticles in categories -%}
+			{%- if nativecat.name == catname -%}
+				{{- caller(nativecat, sc.categories[catname], catarticles) -}}
+			{%- endif -%}
+		{%- endfor -%}
+	{%- endif -%}
+{%- endmacro -%}
+
+{%- macro tag_by_name(tagname) -%}
+	{%- for nativetag, tagarticles in tags -%}
+		{%- if nativetag.name == tagname -%}
+			{%- if tagname in sc.categories -%}
+				{{- caller(nativetag, sc.categories[tagname], tagarticles) -}}
+			{%- else -%}
+				{{- caller(nativetag, None, tagarticles) -}}
+			{%- endif -%}
+		{%- endif -%}
+	{%- endfor -%}
+{%- endmacro -%}
+
+{%- macro article_by_slug(slug, lang) -%}
+	{%- for a in all_articles -%}
+		{%- if a.slug == slug and a.lang == lang -%}
+			{{- caller(a) -}}
+		{%- endif -%}
+	{%- endfor -%}
+{%- endmacro -%}
+
+{%- macro page_by_slug(slug, lang) -%}
+	{%- for p in pages -%}
+		{%- if p.slug == slug and p.lang == lang -%}
+			{{- caller(p) -}}
+		{%- endif -%}
+	{%- endfor -%}
+{%- endmacro -%}
+
+{%- macro article_or_page_by_slug(slug, lang) -%}
+	{%- for a in all_articles -%}
+		{%- if a.slug == slug and a.lang == lang -%}
+			{{- caller(a, "article") -}}
+		{%- endif -%}
+	{%- endfor -%}
+	{%- for p in pages -%}
+		{%- if p.slug == slug and p.lang == lang -%}
+			{{- caller(p, "page") -}}
+		{%- endif -%}
+	{%- endfor -%}
+{%- endmacro -%}
+
diff --git a/theme/old-templates/macros/renderers.html b/theme/old-templates/macros/renderers.html
new file mode 100644
index 0000000000000000000000000000000000000000..698dbcccbd9ad24f5a77b13e2687ebff69bce05d
--- /dev/null
+++ b/theme/old-templates/macros/renderers.html
@@ -0,0 +1,71 @@
+{%- import 'macros/getters.html' as get with context -%}
+{%- import 'macros/cards.html' as cards with context -%}
+
+{%- macro section_news(s) -%}
+	{{ cards.cards_from_articles_or_pages(all_articles, max=s.num) }}
+{%- endmacro -%}
+
+{%- macro section_custom(s) -%}
+	{{ cards.open() }}
+	{%- for c in s.content -%}
+		{{ cards.card(title=c[lang], url=c.url, catcolor=c.color) }}
+	{%- endfor -%}
+	{{ cards.close() }}
+{%- endmacro -%}
+
+{%- macro section_iframe(s) -%}
+	<iframe src="{{ s.url }}"></iframe>
+{%- endmacro -%}
+
+{%- macro section_category(s) -%}
+	{%- call(nativecat, sccat, catarticles) get.category_by_name(s.category) -%}
+	{%- if s.title is not defined -%}
+		<header>
+			<h2 {% if s.id is defined -%}id="{{ s.id }}"{%- endif %}>{{ sccat[lang]|e }}</h2>
+		</header>
+	{%- endif -%}
+	{{ cards.cards_from_articles_or_pages(catarticles, max=s.num) }}
+	{%- endcall -%}
+{%- endmacro -%}
+
+{%- macro section_tag(s) -%}
+	{%- call(nativetag, sccat, tagarticles) get.tag_by_name(s.tag) -%}
+	{%- if s.title is not defined -%}
+		<header>
+			<h2 {% if s.id is defined -%}id="{{ s.id }}"{%- endif %}>
+				{%- if sccat -%}
+					{{- sccat[lang]|e -}}
+				{%- else -%}
+					{{- s.tag|e -}}
+				{%- endif -%}
+			</h2>
+		</header>
+	{%- endif -%}
+	{{ cards.cards_from_articles_or_pages(tagarticles, max=s.num) }}
+
+
+	{%- endcall -%}
+{%- endmacro -%}
+
+{%- macro section(s) -%}
+	{%- if s.title is defined -%}
+		<header>
+			<h2 {% if s.id is defined -%}id="{{ s.id }}"{%- endif %}>{{ s.title[lang]|e }}</h2>
+		</header>
+	{%- endif -%}
+	{%- if s.type == "news" -%}
+		{{ section_news(s) }}
+	{%- elif s.type == "iframe" -%}
+		{{ section_iframe(s) }}
+	{%- elif s.type == "custom" -%}
+		{{ section_custom(s) }}
+	{%- elif s.type == "category" -%}
+		{{ section_category(s) }}
+	{%- elif s.type == "tag" -%}
+		{{ section_tag(s) }}
+	{%- else -%}
+	<br />
+	<strong>ERROR: render.section: Unknown section type: {{ s.type|e }}</strong><br />
+	<br />
+	{%- endif -%}
+{%- endmacro -%}
diff --git a/theme/old-templates/page.html b/theme/old-templates/page.html
new file mode 100644
index 0000000000000000000000000000000000000000..af4e389ee2929cd5d3155aac3a7f8cd914899e2f
--- /dev/null
+++ b/theme/old-templates/page.html
@@ -0,0 +1,69 @@
+{% extends "base.html" %}
+
+{%- macro page_translation_link(translation, is_current=False) -%}
+<a href="{{ SITEURL }}/{{ translation.url }}" hreflang="{{ translation.lang }}" title="{{ l[translation.lang].langname|e }}">
+	<img alt="{{ l[translation.lang].langname|e }}" src="{{ SITEURL }}/{{ THEME_STATIC_DIR }}/images/flags/{{ translation.lang }}.svg">
+</a>
+{%- endmacro -%}
+
+{% block html_lang %}{{ page.lang }}{% endblock %}
+
+{% block title %}{{ page.title|striptags }}{% endblock %}
+
+{% block extra_head %}
+	{%- for translation in page.translations -%}
+		<link rel="alternate" hreflang="{{ translation.lang }}" href="{{ SITEURL }}/{{ translation.url }}">
+	{%- endfor -%}
+
+	{% if page.summary %}
+		<meta name="description" content="{{ page.summary | striptags | safe | truncate(150) }}" />
+	{% endif %}
+{% endblock %}
+
+{% block content %}
+	<article>
+		<header>
+			<h1>{{ page.title }}</h1>
+		</header>
+		{{ page.content }}
+		<footer>
+			{%- if page.authors -%}
+			<address>
+				{{ l[lang].page.authors_prefix }}
+				{% for author in page.authors %}
+					<a href="{{ SITEURL }}/{{ author.url }}">{{ author }}</a>
+				{% endfor %}
+				{{ l[lang].page.authors_suffix }}
+			</address>
+			{%- endif -%}
+				
+			<span>{{ l[lang].page.published_prefix }}
+			<abbr title="{{ page.date.isoformat() }}">
+				{{ page.locale_date }}
+			</abbr>{{ l[lang].page.published_suffix }}</span>
+
+			{%- if page.modified -%}
+			<br />
+			<span>{{ l[lang].page.modified_prefix }}
+			<abbr title="{{ page.modified.isoformat() }}">{{ page.locale_modified }}</abbr>
+			{{ l[lang].page.modified_suffix }}</span>
+			{%- endif -%}
+
+			<br />
+			<span>{{ l[lang].page.category_prefix }}<a href="{{ SITEURL }}/{{ page.category.url }}">{{ page.category }}</a>{{ l[lang].page.category_suffix }}</span>
+			<br />
+			<span>{{ l[lang].page.languages_prefix }}</span>
+			<ul class="languages">
+			{%- for translation in page.translations -%}
+				<li>
+					{{ page_translation_link(translation) }}
+				</li>
+			{%- endfor -%}
+				<li>
+					{{ page_translation_link(page, True) }}
+				</li>
+			</ul>
+			{{ l[lang].page.languages_suffix }}
+		</footer>
+	</article>
+{% endblock %}
diff --git a/theme/old-templates/tag.html b/theme/old-templates/tag.html
new file mode 100644
index 0000000000000000000000000000000000000000..6a636023257c22e38fb88b74bb4d631af4718362
--- /dev/null
+++ b/theme/old-templates/tag.html
@@ -0,0 +1,7 @@
+{% extends "base.html" %}
+{% block title %}{{ l[lang].title_prefix }}{{ l[lang].sitename }}{{ l[lang].title_suffix }} - {{ tag }}{%endblock%}
+{% block content %}
+	<section>
+		{{ render.section({"type": "tag", "tag": tag.slug, "num": None}) }}
+	</section>
+{% endblock content %}
diff --git a/theme/templates/base.html b/theme/templates/base.html
index 8a346355e90b1416b3231d6471c7890cf684ffc1..b798077b55fe0fcfad98567359fb547ebcef5f63 100644
--- a/theme/templates/base.html
+++ b/theme/templates/base.html
@@ -3,6 +3,7 @@
 {%- import 'macros/getters.html' as get with context -%}
 {%- import 'macros/cards.html' as cards with context -%}
 {%- import 'macros/renderers.html' as render with context -%}
+{%- import 'macros/content_renderer.html' as content_renderer with context -%}
 
 <html lang="{%- block html_lang -%}{{ l }}{%- endblock html_lang -%}">
 <head>
@@ -14,6 +15,7 @@
 	<!-- <meta HTTP-EQUIV="REFRESH" content="500; url=#"> -->
 	<link rel="preload" href="{{ siteurl }}/{{ theme.static_dir }}/css/{{ theme.css_file }}" as="style" />
 	<link rel="stylesheet" type="text/css" href="{{ siteurl }}/{{ theme.static_dir }}/css/{{ theme.css_file }}" />
+	<script src="{{ siteurl }}/mathjax/tex-chtml.js" id="MathJax-script" async></script>
 	{#- TODO load javascript? -#}
 	{#- TODO og: meta tags -#}
 	{#- TODO favicon -#}
diff --git a/theme/templates/macros/README.md b/theme/templates/macros/README.md
new file mode 100644
index 0000000000000000000000000000000000000000..75ed3122989219278ca7cac37db4a07f01cc2e7a
--- /dev/null
+++ b/theme/templates/macros/README.md
@@ -0,0 +1,184 @@
+# Macros
+
+[TOC]
+
+## [getters.html](getters.html)
+
+```
+{%- import 'macros/getters.html' as get with context -%}
+```
+
+Mit diesen Macros können Informationen abgefragt werden.
+
+### `category_by_name(catname, ignore_native = False)`
+
+**Argumente:**
+
+- `catname` (String): Der Name der Kategorie.
+- `ignore_native` (Boolean): Setzt den ersten und dritten Rückgabewert zu `None`. (Verkürzt die Laufzeit dieser Funktion.)
+
+**Rückgabewerte:**
+
+1. ([Category](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#object-author-cat-tag)): Die native Kategorie.
+2. (Dict): Der Eintrag zu der Kategorie aus der config.json-Datei aus dem Inhaltsrepo.
+3. (List\<[Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article)\>): Die Artikel in der Kategorie.
+
+**Beispiel Aufruf:**
+
+```
+{%- call(nativecat, sccat, catarticles) get.category_by_name(article.category.name) -%}
+	<ul>
+		<li>Kategorie: {{ nativecat.name }}</li>
+		<li>Farbe: {{ sccat.color }}</li>
+		<li>Anzahl Artikel: {{ catarticles|length }}</li>
+	</ul>
+{%- endcall -%}
+```
+
+### `tag_by_name(tagname)`
+
+**Argumente:**
+
+- `catname` (String): Der Name des Tags.
+
+**Rückgabewerte:**
+
+1. ([Tag](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#object-author-cat-tag)): Der native Tag.
+2. (Dict): Falls der Tag gleichnamig zu einer Kategorie ist, dann der Eintrag zu der Kategorie aus der config.json-Datei aus dem Inhaltsrepo, sonst `None`.
+3. (List\<[Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article)\>): Die Artikel die diesen Tag haben.
+
+**Beispiel Aufruf:**
+
+```
+{%- call(nativetag, sccat, tagarticles) get.tag_by_name("event") -%}
+	<ul>
+		<li>Tag: {{ nativetag.name }}</li>
+		<li>Farbe: {% if sccat %}{{ sccat.color }}{% else %}Dieser Tag ist nicht gleichnamig zu einer Kategorie und hat deshalb keine Farbe.{% endif %}</li>
+		<li>Anzahl Artikel mit diesem Tag: {{ tagarticles|length }}</li>
+	</ul>
+{%- endcall -%}
+```
+
+
+### `article_by_slug(slug, lang)`
+
+**Argumente:**
+
+- `slug` (String): Der Slug von dem Artikel.
+- `lang` (String): Die Sprache von dem Artikel.
+
+**Rückgabewerte:**
+
+1. ([Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article)): Der Artikel.
+
+### `page_by_slug(slug, lang)`
+
+**Argumente:**
+
+- `slug` (String): Der Slug von der Seite.
+- `lang` (String): Die Sprache von der Seite.
+
+**Rückgabewerte:**
+
+1. ([Page](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#page)): Die Seite.
+
+### `article_or_page_by_slug(slug, lang)`
+
+**Argumente:**
+
+- `slug` (String): Der Slug von dem Artikel oder der Seite.
+- `lang` (String): Die Sprache von dem Artikel oder der Seite.
+
+**Rückgabewerte:**
+
+1. ([Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article) | [Page](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#page)): Der Artikel oder die Seite.
+2. (String): "article" falls es ein Artikel ist. "page" falls es eine Seite ist.
+
+## [cards.html](cards.html)
+
+```
+{%- import 'macros/cards.html' as cards with context -%}
+```
+
+Mit diesen Macros können Kachelblöcke erstellt werden.
+
+In einem Kachelblock dürfen nur Kacheln sein und sonst nichts!
+
+**Beispiel:**
+
+```
+{{ cards.open() }}
+
+{%- for article in all_articles -%}
+	{{ cards.card_from_article_or_page(article) }}
+{%- endfor -%}
+
+{{ cards.card(title="Dies ist auch eine Kachel", url="https://www.youtube.com/watch?v=dQw4w9WgXcQ", catcolor="green") }}
+
+{{ cards.close() }}
+```
+
+### `open(id = None, classes = None)`
+
+Beginnt einen Kachelblock.
+
+**Argumente:**
+
+- `id` (String): Die ID von dem Kachelblock.
+- `classes` (List\<String\>): Zusätzliche Klassen für den Kachelblock.
+
+### `close()`
+
+Schließt einen Kachelblock.
+
+### `card(title, url, catcolor, escape_title = True, is_url_external = True)`
+
+Erstellt eine Kachel.
+
+**Argumente:**
+
+- `title` (String): Der Titel.
+- `url` (String): Der URL.
+- `catcolor` (String): Die Farbe.
+- `escape_title` (Boolean): Ob `title` escaped werden soll.
+- `is_url_external` (Boolean): Ob `url` auf eine externe Resource zeigt, oder als relativen Link interpretiert werden soll.
+
+### `card_from_article_or_page(aop)`
+
+Erstellt eine Kachel von einem Artikel oder einer Seite.
+
+**Argumente:**
+
+1. `aop` ([Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article) | [Page](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#page)): Der Artikel oder die Seite.
+
+
+### `cards_from_articles_or_pages(asops, max = None, standalone = True)`
+
+Erstellt mehrere Kacheln oder einen vollständigen Kachelblock von mehreren Artikeln oder Seiten.
+
+**Argumente:**
+
+1. `asops` (List\<[Article](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#article) | [Page](https://gaumi-fginfo.pages.gwdg.de/pelican/_build/html/themes.html#page)\>): Die Artikel oder Seiten. Kann auch gemischt sein.
+2. `max` (Int | None): Falls vorhanden werden maximal `max` Kacheln gerendert.
+3. `standalone` (Boolean): Ob `open()` und `close()` automatisch mit ausgeführt werden soll.
+
+**Beispiel Aufruf:**
+
+```
+{{ cards_from_articles_or_pages(all_articles, max=5) }}
+```
+
+## [renderers.html](renderers.html)
+
+```
+{%- import 'macros/renderers.html' as render with context -%}
+```
+
+**TODO: ** Die Dokumentation für diese Macros ist abhängig von der Dokumentation von der config.json-Datei im Inhaltsrepo.
+
+### `section_news(s)`
+### `section_custom(s)`
+### `section_iframe(s)`
+### `section_category(s)`
+### `section_tag(s)`
+### `section(s)`
diff --git a/theme/templates/macros/cards.html b/theme/templates/macros/cards.html
index 2360ed07469635ba2ecd8c8afb39b029938caa0a..13f78d5c954d54c4a04c8847fb419da4fac7dfc1 100644
--- a/theme/templates/macros/cards.html
+++ b/theme/templates/macros/cards.html
@@ -10,7 +10,7 @@
 {%- endmacro -%}
 
 {%- macro card(title, url, catcolor, is_url_external = True) -%}
-	<li style="--category-color: {{ catcolor }}"><a href="{% if not is_url_external %}{{ SITEURL }}/{% endif %}{{ url }}">{{ title|e }}</a></li>
+	<li style="--category-color: {{ catcolor }}"><a href="{% if not is_url_external %}{{ siteurl }}/{% endif %}{{ url }}">{{ title|e }}</a></li>
 {%- endmacro -%}
 
 {%- macro card_from_page(page, lang) -%}
diff --git a/theme/templates/macros/content_renderer.html b/theme/templates/macros/content_renderer.html
new file mode 100644
index 0000000000000000000000000000000000000000..610e05f96bdbda1872189542e470edf2a7d15e48
--- /dev/null
+++ b/theme/templates/macros/content_renderer.html
@@ -0,0 +1,346 @@
+
+{#- Siehe https://gitlab.gwdg.de/GAUMI-fginfo/fg-website/-/blob/better-content-renderer/docs/content.md -#}
+
+{%- macro render_content(content, lang) -%}
+	{{ render_blocks(content, lang) }}
+{%- endmacro -%}
+
+{%- macro render_attr_extra(key, value, lang) %}
+	{%- if value != None -%}
+		{%- if value is mapping and value['value'] != None -%}
+			{%- if value['escape'] %} {{ key }}="{{ value['value']|e }}"
+			{%- else %} {{ key }}="{{ value['value'] }}"
+			{%- endif -%}
+		{%- elif value is string %} {{ key }}="{{ value }}"
+		{%- endif -%}
+	{%- endif -%}
+{%- endmacro -%}
+
+{%- macro render_attr(attr, lang, extra_classes = [], extra = {}) -%}
+	{%- set id = attr['id']|d("") -%}
+	{%- set classes = attr['classes']|d([]) + extra_classes -%}
+	{%- set attr_extra = attr['extra']|d({}) -%}
+
+	{%- if id %} id="{{ id }}"{%- endif -%}
+	{%- if classes|length > 0 %} class="{{ classes|join(' ') }}"{%- endif -%}
+	{%- for key, value in attr_extra.items() -%}
+		{%- if key not in extra -%}
+			{{ render_attr_extra(key, value, lang) }}
+		{%- endif -%}
+	{%- endfor -%}
+	{%- for key, value in extra.items() -%}
+		{{ render_attr_extra(key, value, lang) }}
+	{%- endfor -%}
+{%- endmacro -%}
+
+{%- macro render_orderedlist_type(style, delim) -%}
+	{#- TODO also use delim: default | period | one_parenthesis | two_parentheses -#}
+	{%-   if style == "default" -%}1
+	{%- elif style == "lower_alpha" -%}a
+	{%- elif style == "upper_alpha" -%}A
+	{%- elif style == "lower_roman" -%}i
+	{%- elif style == "upper_roman" -%}I
+	{%- elif style == "example" -%}1
+	{%- else -%}1
+	{%- endif -%}
+{%- endmacro -%}
+
+{%- macro render_link(url, content, lang, attr = None, title = None) -%}
+	<a {{ render_attr(attr, lang, extra={"href": url, "title": {"value":title, "escape": true}}) }}>
+		{%- if content is string -%}
+			{{ content|e }}
+		{%- else -%}
+			{{ render_blocks_or_inlines(content, lang) }}
+		{%- endif -%}
+	</a>
+{%- endmacro -%}
+
+{%- macro render_image(url, alt, lang, attr = None, title = None) -%}
+	<img {{ render_attr(attr, lang, extra={"src": url, "title": {"value":title, "escape": true}}) }} alt="
+		{%- if alt is string -%}
+			{{ alt|e }}
+		{%- else -%}
+			{{ render_inlines(alt, lang) }}
+		{%- endif -%}
+	">
+{%- endmacro -%}
+
+{%- macro render_blocks_or_inlines(bsois, lang) -%}
+	{%- for boi in bsois -%}
+		{{ render_block_or_inline(boi, lang) }}
+	{%- endfor -%}
+{%- endmacro -%}
+
+{%- macro render_block_or_inline(boi, lang) -%}
+	{%- set eclass = boi['class'] -%}
+	{%- if eclass == "block" -%}
+		{{ render_block(boi,lang) }}
+	{%- elif eclass == "inline" -%}
+		{{ render_inline(boi,lang) }}
+	{%- else -%}
+		<br><strong>ERROR: Cannot render block or inline: '{{ boi|e }}'</strong><br>
+	{%- endif -%}
+{%- endmacro -%}
+
+{#- ############################ BLOCKS ################################### -#}
+
+{%- macro render_blocks(blocks, lang) -%}
+	{%- for block in blocks -%}
+		{{ render_block(block,lang) }}
+	{%- endfor -%}
+{%- endmacro -%}
+
+{%- macro render_block(block, lang) -%}
+	{%- set etype = block['type'] -%}
+	{%- if etype == "header" -%}
+		{{ render_block_header(block, lang) }}
+	{%- elif etype == "paragraph" -%}
+		{{ render_block_paragraph(block, lang) }}
+	{%- elif etype == "bulletlist" -%}
+		{{ render_block_bulletlist(block, lang) }}
+	{%- elif etype == "plain" -%}
+		{{ render_block_plain(block, lang) }}
+	{%- elif etype == "codeblock" -%}
+		{{ render_block_codeblock(block, lang) }}
+	{%- elif etype == "blockquote" -%}
+		{{ render_block_blockquote(block, lang) }}
+	{%- elif etype == "blockcontainer" -%}
+		{{ render_block_blockcontainer(block, lang) }}
+	{%- elif etype == "horizontalrule" -%}
+		{{ render_block_horizontalrule(block, lang) }}
+	{%- elif etype == "rawblock" -%}
+		{{ render_block_rawblock(block, lang) }}
+	{%- elif etype == "orderedlist" -%}
+		{{ render_block_orderedlist(block, lang) }}
+	{%- else -%}
+		<br><strong>ERROR: Unhandled block type: '{{ etype|e }}'</strong><br>
+	{%- endif -%}
+{%- endmacro -%}
+
+{%- macro render_block_header(block, lang) -%}
+	{%- set level = block['level'] -%}
+	{%- set attr = block['attr'] -%}
+	{%- set content = block['content'] -%}
+	<header {{ render_attr(attr, lang) }}>
+		<h{{ level }}>
+			{{ render_inlines(content, lang) }}
+		</h{{ level }}>
+	</header>
+{%- endmacro -%}
+
+{%- macro render_block_paragraph(block, lang) -%}
+	{%- set content = block['content'] -%}
+	<p>
+		{{ render_inlines(content, lang) }}
+	</p>
+{%- endmacro -%}
+
+{%- macro render_block_bulletlist(block, lang) -%}
+	{%- set items = block['items'] -%}
+	{%- set count = block['count'] -%}
+	<ul>
+		{%- for item in items -%}
+			<li>{{ render_blocks(item, lang) }}</li>
+		{%- endfor -%}
+	</ul>
+{%- endmacro -%}
+
+{%- macro render_block_plain(block, lang) -%}
+	{%- set content = block['content'] -%}
+	{{ render_inlines(content, lang) }}
+{%- endmacro -%}
+
+{%- macro render_block_codeblock(block, lang) -%}
+	{%- set attr = block['attr'] -%}
+	{%- set code = block['code'] -%}
+	{%- set code_lines = block['code_lines'] -%}
+	<pre {{ render_attr(attr, lang, extra_classes=["codeblock"]) }}>{{ code|e }}</pre>
+{%- endmacro -%}
+
+{%- macro render_block_blockquote(block, lang) -%}
+	{%- set content = block['content'] -%}
+	<blockquote>
+		{{ render_blocks(content, lang) }}
+	</blockquote>
+{%- endmacro -%}
+
+{%- macro render_block_blockcontainer(block, lang) -%}
+	{%- set attr = block['attr'] -%}
+	{%- set content = block['content'] -%}
+	<div {{ render_attr(attr, lang) }}>
+		{{ render_blocks(content, lang) }}
+	</div>
+{%- endmacro -%}
+
+{%- macro render_block_horizontalrule(block, lang) -%}
+	<hr>
+{%- endmacro -%}
+
+{%- macro render_block_rawblock(block, lang) -%}
+	{%- set format = block['format'] -%}
+	{%- set raw = block['raw'] -%}
+	{%- if format == "html" -%}
+		<div class="rawblock {{ format|e }}">
+			{{ raw }}
+		</div>
+	{%- else -%}
+		<div class="rawblock {{ format|e }}">{{ raw | e }}</div>
+	{%- endif -%}
+{%- endmacro -%}
+
+{%- macro render_block_orderedlist(block, lang) -%}
+	{%- set items = block['items'] -%}
+	{%- set count = block['count'] -%}
+	{%- set start = block['start'] -%}
+	{%- set style = block['style'] -%}
+	{%- set delim = block['delim'] -%}
+	{#- ol start="{{ start|e }}" style="list-style-type: {{ render_orderedlist_type(style, delim) }};" -#}
+	<ol start="{{ start|e }}" type="{{ render_orderedlist_type(style, delim) }}">
+		{%- for item in items -%}
+			<li>{{ render_blocks(item, lang) }}</li>
+		{%- endfor -%}
+	</ol>
+{%- endmacro -%}
+
+
+{#- ############################ INLINES ################################## -#}
+
+{%- macro render_inlines(inlines, lang) -%}
+	{%- for inline in inlines -%}
+		{{ render_inline(inline,lang) }}
+	{%- endfor -%}
+{%- endmacro -%}
+
+{%- macro render_inline(inline, lang) -%}
+	{%- set etype = inline['type'] -%}
+	{%- if etype == "space" -%}
+		{{ render_inline_space(inline, lang) }}
+	{%- elif etype == "string" -%}
+		{{ render_inline_string(inline, lang) }}
+	{%- elif etype == "linebreak" -%}
+		{{ render_inline_linebreak(inline, lang) }}
+	{%- elif etype == "strong"      -%}{{ render_inline_simple(inline, lang, "b") }}
+	{%- elif etype == "emph"        -%}{{ render_inline_simple(inline, lang, "i") }}
+	{%- elif etype == "strikeout"   -%}{{ render_inline_simple(inline, lang, "s") }}
+	{%- elif etype == "superscript" -%}{{ render_inline_simple(inline, lang, "sup") }}
+	{%- elif etype == "subscript"   -%}{{ render_inline_simple(inline, lang, "sub") }}
+	{%- elif etype == "underline"   -%}{{ render_inline_underline(inline, lang) }}
+	{%- elif etype == "smallcaps"   -%}{{ render_inline_smallcaps(inline, lang) }}
+	{%- elif etype == "inlinecontainer" -%}
+		{{ render_inline_inlinecontainer(inline, lang) }}
+	{%- elif etype == "rawinline" -%}
+		{{ render_inline_rawinline(inline, lang) }}
+	{%- elif etype == "code" -%}
+		{{ render_inline_code(inline, lang) }}
+	{%- elif etype == "quoted" -%}
+		{{ render_inline_quoted(inline, lang) }}
+	{%- elif etype == "footnote" -%}
+		{{ render_inline_footnote(inline, lang) }}
+	{%- elif etype == "link" -%}
+		{{ render_inline_link(inline, lang) }}
+	{%- elif etype == "image" -%}
+		{{ render_inline_image(inline, lang) }}
+	{%- elif etype == "math" -%}
+		{{ render_inline_math(inline, lang) }}
+	{%- else -%}
+		<br><strong>ERROR: Unhandled inline type: '{{ etype|e }}'</strong><br>
+	{%- endif -%}
+{%- endmacro -%}
+
+{%- macro render_inline_space(inline, lang) %} {% endmacro -%}
+
+{%- macro render_inline_string(inline, lang) -%}
+	{%- set text = inline['text'] -%}
+	{{ text|e }}
+{%- endmacro -%}
+
+{%- macro render_inline_linebreak(inline, lang) -%}
+	<br>
+{%- endmacro -%}
+
+{%- macro render_inline_simple(inline, lang, tag) -%}
+	{%- set content = inline['content'] -%}
+	<{{ tag }}>{{ render_inlines(content, lang) }}</{{ tag }}>
+{%- endmacro -%}
+
+{%- macro render_inline_underline(inline, lang) -%}
+	{%- set content = inline['content'] -%}
+	<span class="underline">{{ render_inlines(content, lang) }}</span>
+{%- endmacro -%}
+
+{%- macro render_inline_smallcaps(inline, lang) -%}
+	{%- set content = inline['content'] -%}
+	<span class="smallcaps">{{ render_inlines(content, lang) }}</span>
+{%- endmacro -%}
+
+{%- macro render_inline_inlinecontainer(inline, lang) -%}
+	{%- set attr = inline['attr'] -%}
+	{%- set content = inline['content'] -%}
+	<span {{ render_attr(attr, lang) }}>
+		{{ render_inlines(content, lang) }}
+	</span>
+{%- endmacro -%}
+
+{%- macro render_inline_rawinline(inline, lang) -%}
+	{%- set format = inline['format'] -%}
+	{%- set raw = inline['raw'] -%}
+	{%- if format == "html" -%}
+		<span class="rawinline {{ format|e }}">
+			{{ raw }}
+		</span>
+	{%- else -%}
+		<span class="rawinline {{ format|e }}">{{ raw | e }}</span>
+	{%- endif -%}
+{%- endmacro -%}
+
+{%- macro render_inline_code(inline, lang) -%}
+	{%- set attr = inline['attr'] -%}
+	{%- set code = inline['code'] -%}
+	{%- set code_lines = inline['code_lines'] -%}
+	<code {{ render_attr(attr, lang, extra_classes=["codeinline"]) }}>{{ code|e }}</code>
+{%- endmacro -%}
+
+{%- macro render_inline_quoted(inline, lang) -%}
+	{%- set quotetype = inline['quotetype'] -%}
+	{%- set content = inline['content'] -%}
+	{%- if quotetype == "single" -%}
+		{{ t[lang].quotations.single.left }}{{ render_inlines(content, lang) }}{{ t[lang].quotations.single.right }}
+	{%- elif quotetype == "double" -%}
+		{{ t[lang].quotations.double.left }}{{ render_inlines(content, lang) }}{{ t[lang].quotations.double.right }}
+	{%- else -%}
+		&quot;{{ render_inlines(content, lang) }}&quot;
+	{%- endif -%}
+{%- endmacro -%}
+
+{%- macro render_inline_footnote(inline, lang) -%}
+	{%- set content = inline['content'] -%}
+	<sup>[TODO FOOTNOTES]</sup>
+{%- endmacro -%}
+
+{%- macro render_inline_link(inline, lang) -%}
+	{%- set attr = inline['attr'] -%}
+	{%- set content = inline['content'] -%}
+	{%- set url = inline['url'] -%}
+	{%- set title = inline['title'] -%}
+	{{ render_link(url, content, lang, attr, title) }}
+{%- endmacro -%}
+
+{%- macro render_inline_image(inline, lang) -%}
+	{%- set attr = inline['attr'] -%}
+	{%- set alt = inline['alt'] -%}
+	{%- set url = inline['url'] -%}
+	{%- set title = inline['title'] -%}
+	{{ render_image(url, alt, lang, attr, title) }}
+{%- endmacro -%}
+
+{%- macro render_inline_math(inline, lang) -%}
+	{%- set mathtype = inline['mathtype'] -%}
+	{%- set math = inline['math'] -%}
+	{%- if mathtype == "inline" -%}
+		<span class="math inline">\({{ math|e }}\)</span>
+	{%- elif mathtype == "display" -%}
+		<span class="math display">\[{{ math|e }}\]</span>
+	{%- else -%}
+		<br><strong>ERROR: Unhandled mathtype: '{{ mathtype|e }}'</strong><br>
+	{%- endif -%}
+{%- endmacro -%}
diff --git a/theme/templates/macros/renderers.html b/theme/templates/macros/renderers.html
index 56351467715cfd0882aceb9394b91c9f9936dc82..ea366c2b6072efb80d2262eeecd331ea3cf0d74b 100644
--- a/theme/templates/macros/renderers.html
+++ b/theme/templates/macros/renderers.html
@@ -1,5 +1,6 @@
 {%- import 'macros/getters.html' as get with context -%}
 {%- import 'macros/cards.html' as cards with context -%}
+{%- import 'macros/content_renderer.html' as content_renderer with context -%}
 
 {%- macro section_news(s, lang) -%}
 	{{ cards.cards_from_pages(pages_modified, max=s.num) }}
@@ -27,7 +28,7 @@
 		{{ cards.cards_from_pages(tagpages, lang, max=s.num) }}
 		
 		{%- if tagpage -%}
-			{{ tagpage.content }}
+			{{ content_renderer.render_content(tagpage.content, lang) }}
 		{%- endif -%}
 	{%- endcall -%}
 {%- endmacro -%}
diff --git a/theme/templates/page.html b/theme/templates/page.html
index fdbef287039e822a368a5f6e0702340bd424aaaa..1ddee6adc5b961adf0e97c276f0b6b18ac2e5978 100644
--- a/theme/templates/page.html
+++ b/theme/templates/page.html
@@ -28,7 +28,7 @@
 		{%- call(s) get.metadata_entry(page.slug, l, 'before') -%}
 			{{ render.sections(s, l) }}
 		{%- endcall -%}
-		{{ page.content }}
+		{{ content_renderer.render_content(page.content, l) }}
 		{%- call(s) get.metadata_entry(page.slug, l, 'after') -%}
 			{{ render.sections(s, l) }}
 		{%- endcall -%}