diff --git a/.gitignore b/.gitignore
index f8d95c9..e65f3d5 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,3 +1,4 @@
 /\.kdev4/
 /log/
 /earthworm.kdev4
+__pycache__
diff --git a/data/common.css b/data/common.css
new file mode 100644
index 0000000..8b13789
--- /dev/null
+++ b/data/common.css
@@ -0,0 +1 @@
+
diff --git a/data/logo.png b/data/logo.png
new file mode 100644
index 0000000..52ff5ee
Binary files /dev/null and b/data/logo.png differ
diff --git a/main.py b/main.py
index c124f91..930eccd 100644
--- a/main.py
+++ b/main.py
@@ -5,8 +5,14 @@ from config import config
 from server import Server
 from request import Request
 
+import template.common
+
 server = Server(config)
 
 def application(env, start_response):
   request = Request(server, env, start_response)
-  return request.complete("Env:\n" + str(env))
+  request.template = template.common.instance
+  
+  content = '<p>' + request.t("Hello World!") + '</p>' \
+          + '<p>' + "Env:\n" + str(env) + '</p>'
+  return request.complete_content(content)
diff --git a/request.py b/request.py
index e124284..66d86be 100644
--- a/request.py
+++ b/request.py
@@ -9,21 +9,56 @@ class Request:
     self.server = server
     self.environ = environ
     self.start_response = start_response
+    self.session = None
+    self.template = None
+    
+    self.status = "200 OK"
+    self.headers = []
+    self.title = ''
 
-  def complete(self, result = None, headers = []):
-    content_headers = [('Content-Type','text/html')]
+  def translate(self, text):
+    return text
+  
+  def t(self, text):
+    return self.translate(text)
 
-    if not result is None:
-      if not type(result) is bytes:
-        result = bytes(str(result), "utf8")
+  def chain_template(subtemplate):
+    subtemplate.parent = self.template
+    self.template = subtemplate
 
+  def chain_title(subtitle):
+    if subtitle:
+      if self.title:
+        self.title = subtitle + ' | ' + self.title
+      else:
+        self.title = subtitle
+    
+  def complete_data(self, data):
+    if data is None:
+      data = bytes()
+    if not type(data) is bytes:
+      data = bytes(str(data), "utf8")
+    if not data and self.status == "200 OK":
+      self.status = "204 No Content"
+    
+    size = 65536
     result_list = []
-    if result:
-      status = "200 OK"
-      size = 65536
-      for i in range(0, len(result), size):
-        result_list.append(result[i:i + size])
-    else:
-      status = "204 No Content"
-    self.start_response(status, headers + content_headers)
+    for i in range(0, len(data), size):
+      result_list.append(data[i:i + size])
+    data = None
+
+    self.start_response(self.status, self.headers)
     return result_list
+
+  def complete_text(self, text):
+    if text is None: text = ""
+    return self.complete_data(text)
+
+  def complete_html(self, html):
+    self.headers += [('Content-Type','text/html')];
+    return self.complete_text(html)
+
+  def complete_content(self, content):
+    if self.template:
+      content = self.template.wrap(self, content)
+    return self.complete_html(content)
diff --git a/server.py b/server.py
index 599f279..2e0a6a6 100644
--- a/server.py
+++ b/server.py
@@ -2,3 +2,5 @@
 class Server:
   def __init__(self, config):
     self.config = config
+    self.urlprefix = config.get('urlprefix', '')
+    self.urlprefix_data = self.urlprefix + '/data'
diff --git a/template/common.py b/template/common.py
new file mode 100644
index 0000000..63768f1
--- /dev/null
+++ b/template/common.py
@@ -0,0 +1,36 @@
+
+from template.template import Template
+import template.login as login
+import template.usermenu as usermenu
+
+
+class Common(Template):
+  def wrapfunc(self, request, content):
+    return '''
+<html>
+  <head>
+    <title>%(title)s</title>
+    <link rel="stylesheet" href="%(prefix_data)s/common.css" />
+  </head>
+  <body>
+    <div class="header">
+      <div id="logo"><img src="%(prefix_data)s/logo.png" /></div>
+      %(usermenu)s
+    </div>
+    <div class="content">
+      %(content)s
+    </div>
+    <div class="footer">
+      powered by magic
+    </div>
+  </body>
+</html>
+''' % {
+  'title'       : request.title,
+  'prefix_data' : request.server.urlprefix_data,
+  'usermenu'    : (usermenu.instance if request.session else login.instance).wrap(request, content),
+  'content'     : content,
+}
+
+
+instance = Common()
diff --git a/template/login.py b/template/login.py
new file mode 100644
index 0000000..0aab657
--- /dev/null
+++ b/template/login.py
@@ -0,0 +1,23 @@
+
+from template.template import Template
+
+class LoginTemplate(Template):
+  def wrapfunc(self, request, content):
+    return '''
+<div class="login">
+  <form method="POST">
+    <input type="hidden" name="action" value="login" />
+    <span class="username">
+      ''' + request.t('Username:') + '''
+      <input type="text" name="username" />
+    </span>
+    <span class="password">
+      ''' + request.t('Password:') + '''
+      <input type="password" name="password" />
+    </span>
+    <input type="submit" value="''' + request.t('Login') + '''" />
+  </form>
+</div>
+'''
+
+instance = LoginTemplate()
diff --git a/template/template.py b/template/template.py
new file mode 100644
index 0000000..220e169
--- /dev/null
+++ b/template/template.py
@@ -0,0 +1,15 @@
+
+class Template:
+  def __init__(self):
+    self.parent = None
+  
+  def wrap(self, request, content):
+    content = self.wrapfunc(request, content)
+    if not self.parent:
+      return content
+    return self.parent(request, content)
+
+  def wrapfunc(self, request, content):
+    return content
+
+instance = Template()
diff --git a/template/usermenu.py b/template/usermenu.py
new file mode 100644
index 0000000..4bcf9aa
--- /dev/null
+++ b/template/usermenu.py
@@ -0,0 +1,11 @@
+
+from template.template import Template
+
+class UsermenuTemplate(Template):
+  def wrapfunc(self, request, content):
+    return '''
+<div class="usermenu">
+</div>
+'''
+
+instance = UsermenuTemplate()