diff --git a/create-proposal.sh b/create-proposal.sh
new file mode 100755
index 0000000000000000000000000000000000000000..f05814644a74ef0543f452368e76c4a45c229f77
--- /dev/null
+++ b/create-proposal.sh
@@ -0,0 +1,48 @@
+#!/bin/sh
+PROPOSAL_DIR="i2p2www/spec/proposals"
+
+if [ $# -lt 4 ]
+then
+    echo "Usage: ./create-proposal.sh name-in-url \"Title of proposal\" author forum-url [file]"
+    exit
+fi
+
+name=$1
+title=$2
+author=$3
+thread=$4
+file=$5
+
+date=`date +%Y-%m-%d`
+num=`expr $(expr substr $(ls -r "$PROPOSAL_DIR" | head -n1) 1 3) + 1`
+titleline=`printf '%*s' "$(expr length "$title")" | tr ' ' =`
+
+proposal="$PROPOSAL_DIR/$num-$name.rst"
+
+cat >"$proposal" <<EOF
+$titleline
+$title
+$titleline
+.. meta::
+    :author: $author
+    :created: $date
+    :thread: $thread
+    :lastupdated: $date
+    :status: Draft
+
+.. contents::
+
+
+Introduction
+============
+
+EOF
+
+if [ -f "$file" ]
+then
+    cat "$file" >>"$proposal"
+else
+    echo >>"$proposal"
+fi
+
+echo "Proposal created: $proposal"
diff --git a/i2p2www/__init__.py b/i2p2www/__init__.py
index 5018c59b43eeec9220f0fa8ccc3de7aa0e8c3744..34c5ff9fe4fa902e0afd703339dc9ada794ef22b 100644
--- a/i2p2www/__init__.py
+++ b/i2p2www/__init__.py
@@ -110,6 +110,7 @@ GETTEXT_DOMAIN_MAPPING = {
 TEMPLATE_DIR = os.path.join(os.path.dirname(__file__), 'pages')
 STATIC_DIR = os.path.join(os.path.dirname(__file__), 'static')
 SPEC_DIR = os.path.join(os.path.dirname(__file__), 'spec')
+PROPOSAL_DIR = os.path.join(SPEC_DIR, 'proposals')
 BLOG_DIR = os.path.join(os.path.dirname(__file__), 'blog')
 MEETINGS_DIR = os.path.join(os.path.dirname(__file__), 'meetings/logs')
 SITE_DIR = os.path.join(TEMPLATE_DIR, 'site')
diff --git a/i2p2www/pages/global/macros b/i2p2www/pages/global/macros
index b50e5541054610b63608608f69612ad7b74f7caa..7876f3323fdf40a790e03ce28579f88f07e409d8 100644
--- a/i2p2www/pages/global/macros
+++ b/i2p2www/pages/global/macros
@@ -1,6 +1,7 @@
 {%- macro change_lang(lang) -%}
 {%- if request.endpoint == 'site_show' -%}{{ url_for('site_show', lang=lang, page=page) }}
 {%- elif request.endpoint == 'spec_show' -%}{{ url_for('spec_show', name=name) }}
+{%- elif request.endpoint == 'proposal_show' -%}{{ url_for('proposal_show', name=name) }}
 {%- elif request.endpoint == 'blog_index' -%}
   {%- if category -%}{{ url_for('blog_index', lang=lang, category=category) }}
   {%- else -%}{{ url_for('blog_index', lang=lang) }}
diff --git a/i2p2www/pages/global/nav.html b/i2p2www/pages/global/nav.html
index 14d3f0cee98e24cbeed1572794d5872d4be11f15..974be8de89ede6832a64b0e479c050e3da3bfc1d 100644
--- a/i2p2www/pages/global/nav.html
+++ b/i2p2www/pages/global/nav.html
@@ -30,6 +30,7 @@
         </ul>
       </li>
       <li><a href="{{ url_for('spec_index') }}"><div class="menuitem"><span>{{ _('Specifications') }}</span></div></a></li>
+      <li><a href="{{ url_for('proposal_index') }}"><div class="menuitem"><span>{{ _('Proposals') }}</span></div></a></li>
       <li class="has-sub"><div class="menuitem"><span>{{ _('API') }}</span></div>
         <ul>
           <li><a href="{{ site_url('docs/api/i2ptunnel') }}"><div class="menuitem"><span>I2PTunnel</span></div></a></li>
diff --git a/i2p2www/pages/spec/index.html b/i2p2www/pages/spec/index.html
index db397f3415ffd4ef27a0210723125731868b2633..f56ce0945446399dd8121a68b4234687f50c905d 100644
--- a/i2p2www/pages/spec/index.html
+++ b/i2p2www/pages/spec/index.html
@@ -1,9 +1,13 @@
 {% extends "global/layout.html" %}
 {% block title %}I2P Specification Documents{% endblock %}
 {% block content %}
+<p>
 This page provides the specifications for various components of the I2P network
 and router software. These are living documents, and the specifications are
-updated as modifications are made to the network and software.
+updated as modifications are made to the network and software. The proposal
+documents that track changes to these specifications can be viewed
+<a href="{{ url_for('proposal_index') }}">here</a>.
+</p>
 
 <ul><li>
 "Last updated" is the last date when the specification given within a document
diff --git a/i2p2www/pages/spec/proposal-index.html b/i2p2www/pages/spec/proposal-index.html
new file mode 100644
index 0000000000000000000000000000000000000000..b701d42b0cf2f596b498ec667b6951805bbbb8d1
--- /dev/null
+++ b/i2p2www/pages/spec/proposal-index.html
@@ -0,0 +1,36 @@
+{% extends "global/layout.html" %}
+{% block title %}I2P Proposal Documents{% endblock %}
+{% block content %}
+<p>
+This page is the central index of proposed changes to the
+<a href="{{ url_for('spec_index') }}">I2P specifications</a>.
+</p>
+
+<p>{% trans dev='http://'+i2pconv('zzz.i2p'),
+trac='https://trac.i2p2.de/report/1' -%}
+To submit a proposal, post it on the <a href="{{ dev }}">development forum</a>
+or <a href="{{ trac }}">enter a ticket with the proposal attached</a>.
+{%- endtrans %}</p>
+
+<table>
+  <tr>
+    <th>Number</th>
+    <th>Title</th>
+    <th>Last updated</th>
+    <th>Status</th>
+    <th>Link</th>
+  </tr>
+  {% for proposal in proposals %}
+  <tr>
+    <td>{{ proposal.num }}</td>
+    <td>{{ proposal.title }}</td>
+    <td><time>{{ proposal.lastupdated }}</time></td>
+    <td>{{ proposal.status }}</td>
+    <td>
+      <a href="{{ url_for('proposal_show', name=proposal.name) }}">HTML</a> |
+      <a href="{{ url_for('proposal_show_txt', name=proposal.name) }}">TXT</a>
+    </td>
+  </tr>
+{% endfor %}
+</table>
+{% endblock %}
diff --git a/i2p2www/pages/spec/proposal-show.html b/i2p2www/pages/spec/proposal-show.html
new file mode 100644
index 0000000000000000000000000000000000000000..4b3d8c9c9ef1d1899cc09b515b57b703c621d469
--- /dev/null
+++ b/i2p2www/pages/spec/proposal-show.html
@@ -0,0 +1,24 @@
+{% extends "global/layout.html" %}
+{%- from "global/macros" import render_categories with context -%}
+{% block title %}{{ title }}{% endblock %}
+{% block content_nav %}
+{% autoescape false %}
+{{ toc }}
+{% endautoescape %}
+{% endblock %}
+{% block content %}
+<dl class="meta">
+  <dt>Author</dt>
+  <dd>{{ meta.author }}</dd>
+  <dt>Created</dt>
+  <dd><time datetime="{{ meta.created }}">{{ meta.created }}</time></dd>
+  <dt>Thread</dt>
+  <dd><a href="{{ meta.thread }}">{{ meta.thread }}</a></dd>
+  <dt>Last updated</dt>
+  <dd><time datetime="{{ meta.lastupdated }}">{{ meta.lastupdated }}</time></dd>
+  <dt>Status</dt><dd>{{ meta.status }}</dd>
+</dl>
+{% autoescape false %}
+{{ body }}
+{% endautoescape %}
+{% endblock %}
diff --git a/i2p2www/spec/views.py b/i2p2www/spec/views.py
index c43f4ddce18a1c70a214558286d6f5ab5bbae9f3..318638553450d156e5a3620a6eeb798fcc6645ad 100644
--- a/i2p2www/spec/views.py
+++ b/i2p2www/spec/views.py
@@ -1,9 +1,12 @@
 import codecs
+from docutils import io
 from docutils.core import (
+    Publisher,
     publish_doctree,
     publish_from_doctree,
     publish_parts,
 )
+from docutils.readers.doctree import Reader
 from flask import (
     abort,
     g,
@@ -17,7 +20,7 @@ from flask import (
 )
 import os.path
 
-from i2p2www import SPEC_DIR
+from i2p2www import PROPOSAL_DIR, SPEC_DIR
 from i2p2www import helpers
 
 
@@ -26,7 +29,6 @@ SPEC_METATAGS = {
     'category': '',
     'lastupdated': None,
     }
-
 SPEC_LIST_METATAGS = [
     ]
 SPEC_CATEGORY_SORT = {
@@ -36,12 +38,36 @@ SPEC_CATEGORY_SORT = {
     '': 999,
     }
 
+PROPOSAL_METATAGS = {
+    'author': u'I2P devs',
+    'created': None,
+    'lastupdated': None,
+    'status': u'Draft',
+    'thread': None,
+    }
+PROPOSAL_LIST_METATAGS = [
+    ]
+PROPOSAL_STATUS_SORT = {
+    'Draft': 1,
+    '': 999,
+    }
 
-def spec_index():
-    specs = []
-    for f in os.listdir(SPEC_DIR):
+METATAG_LABELS = {
+    'accuratefor': u'Accurate for',
+    'author': u'Author',
+    'category': u'Category',
+    'created': u'Created',
+    'lastupdated': u'Last updated',
+    'status': u'Status',
+    'thread': u'Thread',
+    }
+
+
+def get_rsts(directory, meta_parser):
+    rsts = []
+    for f in os.listdir(directory):
         if f.endswith('.rst'):
-            path = safe_join(SPEC_DIR, f)
+            path = safe_join(directory, f)
             # read file header
             header = ''
             with codecs.open(path, encoding='utf-8') as fd:
@@ -49,22 +75,32 @@ def spec_index():
                     header += line
                     if not line.strip():
                         break
-            parts = publish_parts(source=header, source_path=SPEC_DIR, writer_name="html")
-            meta = get_metadata_from_meta(parts['meta'])
+            parts = publish_parts(source=header, source_path=directory, writer_name="html")
+            meta = meta_parser(parts['meta'])
 
-            spec = {
+            rst = {
                 'name': f[:-4],
                 'title': parts['title'],
             }
-            spec.update(meta)
-            specs.append(spec)
+            rst.update(meta)
+            rsts.append(rst)
+    return rsts
 
+def spec_index():
+    specs = get_rsts(SPEC_DIR, spec_meta)
     specs.sort(key=lambda s: (SPEC_CATEGORY_SORT[s['category']], s['title']))
     return render_template('spec/index.html', specs=specs)
 
-def spec_show(name, txt=False):
+def proposal_index():
+    proposals = get_rsts(PROPOSAL_DIR, proposal_meta)
+    for i in range(0, len(proposals)):
+        proposals[i]['num'] = int(proposals[i]['name'][:3])
+    proposals.sort(key=lambda s: (PROPOSAL_STATUS_SORT[s['status']], s['num']))
+    return render_template('spec/proposal-index.html', proposals=proposals)
+
+def render_rst(directory, name, meta_parser, template):
     # check if that file actually exists
-    path = safe_join(SPEC_DIR, name + '.rst')
+    path = safe_join(directory, name + '.rst')
     if not os.path.exists(path):
         abort(404)
 
@@ -72,7 +108,7 @@ def spec_show(name, txt=False):
     with codecs.open(path, encoding='utf-8') as fd:
         content = fd.read()
 
-    if txt:
+    if not template:
         # Strip out RST
         content = content.replace('.. meta::\n', '')
         content = content.replace('.. contents::\n\n', '')
@@ -82,16 +118,15 @@ def spec_show(name, txt=False):
         content = content.replace(']_', '] ')
         # Change highlight formatter
         content = content.replace('{% highlight', "{% highlight formatter='textspec'")
-        # Other string changes
-        content = content.replace('    :accuratefor', '- Accurate for')
-        content = content.replace('    :category', '- Category')
-        content = content.replace('    :lastupdated', '- Last updated')
+        # Metatags
+        for (metatag, label) in METATAG_LABELS.items():
+            content = content.replace('    :%s' % metatag, label)
 
     # render the post with Jinja2 to handle URLs etc.
     rendered_content = render_template_string(content)
     rendered_content = rendered_content.replace('</pre></div>', '  </pre></div>')
 
-    if txt:
+    if not template:
         # Send response
         r = make_response(rendered_content)
         r.mimetype = 'text/plain'
@@ -102,19 +137,37 @@ def spec_show(name, txt=False):
     bullet_list = doctree[1][1]
     doctree.clear()
     doctree.append(bullet_list)
-    toc = publish_from_doctree(doctree, writer_name='html')
+    reader = Reader(parser_name='null')
+    pub = Publisher(reader, None, None,
+                    source=io.DocTreeInput(doctree),
+                    destination_class=io.StringOutput)
+    pub.set_writer('html')
+    pub.publish()
+    toc = pub.writer.parts['fragment']
 
     # Remove the ToC from the main document
     rendered_content = rendered_content.replace('.. contents::\n', '')
 
     # publish the spec with docutils
-    parts = publish_parts(source=rendered_content, source_path=SPEC_DIR, writer_name="html")
-    meta = get_metadata_from_meta(parts['meta'])
+    parts = publish_parts(source=rendered_content, source_path=directory, writer_name="html")
+    meta = meta_parser(parts['meta'])
 
-    return render_template('spec/show.html', title=parts['title'], toc=toc, body=parts['fragment'], name=name, meta=meta)
+    return render_template(template, title=parts['title'], toc=toc, body=parts['fragment'], name=name, meta=meta)
+
+def spec_show(name):
+    return render_rst(SPEC_DIR, name, spec_meta, 'spec/show.html')
 
 def spec_show_txt(name):
-    return spec_show(name, True)
+    return render_rst(SPEC_DIR, name, spec_meta, None)
+
+def proposal_show(name):
+    return render_rst(PROPOSAL_DIR, name, proposal_meta, 'spec/proposal-show.html')
 
-def get_metadata_from_meta(meta):
+def proposal_show_txt(name):
+    return render_rst(PROPOSAL_DIR, name, proposal_meta, None)
+
+def spec_meta(meta):
     return helpers.get_metadata_from_meta(meta, SPEC_METATAGS, SPEC_LIST_METATAGS)
+
+def proposal_meta(meta):
+    return helpers.get_metadata_from_meta(meta, PROPOSAL_METATAGS, PROPOSAL_LIST_METATAGS)
diff --git a/i2p2www/urls.py b/i2p2www/urls.py
index 63faa463b6ed7b08fe240f65ba2b4ad00b791f91..41d6b58287324bcc12d8e7946552d823d0b2a162 100644
--- a/i2p2www/urls.py
+++ b/i2p2www/urls.py
@@ -47,6 +47,9 @@ url('/<lang:lang>/<path:page>', 'views.site_show')
 url('/spec', 'spec.views.spec_index')
 url('/spec/<string:name>', 'spec.views.spec_show')
 url('/spec/<string:name>.txt', 'spec.views.spec_show_txt')
+url('/spec/proposals', 'spec.views.proposal_index')
+url('/spec/proposals/<string:name>', 'spec.views.proposal_show')
+url('/spec/proposals/<string:name>.txt', 'spec.views.proposal_show_txt')
 
 url('/<lang:lang>/papers/', 'anonbib.views.papers_list')
 url('/<lang:lang>/papers/bibtex', 'anonbib.views.papers_bibtex')