diff --git a/symposion_project/apps/cms/__init__.py b/symposion_project/apps/cms/__init__.py
new file mode 100644
index 00000000..e69de29b
diff --git a/symposion_project/apps/cms/admin.py b/symposion_project/apps/cms/admin.py
new file mode 100644
index 00000000..4246494c
--- /dev/null
+++ b/symposion_project/apps/cms/admin.py
@@ -0,0 +1,19 @@
+from django.contrib import admin
+
+from mptt.admin import MPTTModelAdmin
+
+from cms.models import MenuItem, Page
+
+
+class PageAdmin(MPTTModelAdmin):
+    prepopulated_fields = {"slug": ("title",)}
+    list_display = ("title", "published", "status")
+
+admin.site.register(Page, PageAdmin)
+
+
+class MenuItemAdmin(MPTTModelAdmin):
+    prepopulated_fields = {"slug": ("name",)}
+    list_display = ("name", "login_required", "published",)
+
+admin.site.register(MenuItem, MenuItemAdmin)
diff --git a/symposion_project/apps/cms/context_processors.py b/symposion_project/apps/cms/context_processors.py
new file mode 100644
index 00000000..207c1793
--- /dev/null
+++ b/symposion_project/apps/cms/context_processors.py
@@ -0,0 +1,10 @@
+from cms.models import MenuItem
+
+
+def menuitems(request):
+    qs = MenuItem.objects.filter(published=True)
+    if not request.user.is_authenticated():
+        qs = qs.filter(login_required=False)
+    return {
+        "menuitems": qs,
+    }
diff --git a/symposion_project/apps/cms/models.py b/symposion_project/apps/cms/models.py
new file mode 100644
index 00000000..7f3f775e
--- /dev/null
+++ b/symposion_project/apps/cms/models.py
@@ -0,0 +1,81 @@
+from datetime import datetime
+
+from django.db import models
+from django.utils.translation import ugettext_lazy as _
+
+from markitup.fields import MarkupField
+
+from taggit.managers import TaggableManager
+
+from mptt.models import MPTTModel, TreeForeignKey
+from mptt.utils import drilldown_tree_for_node
+
+
+class ContentBase(models.Model):
+    
+    STATUS_CHOICES = (
+        (1, _("Draft")),
+        (2, _("Public")),
+    )
+
+    title = models.CharField(max_length=100)
+    slug = models.CharField(max_length=100, blank=True, null=True)
+    body = MarkupField()
+
+    tags = TaggableManager(blank=True)
+
+    status = models.IntegerField(choices=STATUS_CHOICES, default=2)
+    published = models.DateTimeField(default=datetime.now)
+    created = models.DateTimeField(editable=False, default=datetime.now)
+    updated = models.DateTimeField(editable=False, default=datetime.now)
+
+    class Meta:
+        abstract = True
+
+
+class Page(MPTTModel, ContentBase):
+
+    parent = TreeForeignKey("self", null=True, blank=True, related_name="children")
+    ordering = models.PositiveIntegerField(default=1)
+    path = models.TextField(blank=True, editable=False)
+
+    def __unicode__(self):
+        return self.title
+
+    def save(self, calculate_path=True, *args, **kwargs):
+        super(Page, self).save(*args, **kwargs)
+        if calculate_path:
+            self.calculate_path()
+    
+    def calculate_path(self):
+        self.path = ""
+        for page in drilldown_tree_for_node(self):
+            if page == self:
+                self.path += page.slug
+                break
+            else:
+                self.path += "%s/" % page.slug
+        self.save(calculate_path=False)
+
+    class MPTTMeta:
+        order_insertion_by = ["title"]
+
+
+class MenuItem(MPTTModel):
+    
+    name = models.CharField(max_length=50, unique=True)
+    slug = models.SlugField()
+    parent = TreeForeignKey("self", null=True, blank=True, related_name="children")
+
+    url = models.CharField(max_length=200)
+    #FIELDNAME = models.ForeignKey()
+
+    published = models.BooleanField(default=True)
+    login_required = models.BooleanField(default=False)
+
+    def __unicode__(self):
+        return self.name
+
+
+    class MPTTMeta:
+        order_insertion_by = ["name"]
diff --git a/symposion_project/apps/cms/views.py b/symposion_project/apps/cms/views.py
new file mode 100644
index 00000000..e21c790f
--- /dev/null
+++ b/symposion_project/apps/cms/views.py
@@ -0,0 +1,13 @@
+from django.shortcuts import render_to_response
+from django.template import RequestContext
+
+from cms.models import Page
+
+
+def page(request, slug):
+    
+    page = Page.objects.get(path=slug)
+    
+    return render_to_response("cms/page_detail.html", {
+        "page": page,
+    }, context_instance=RequestContext(request))
diff --git a/symposion_project/requirements/base.txt b/symposion_project/requirements/base.txt
index ba75d619..6c85c658 100644
--- a/symposion_project/requirements/base.txt
+++ b/symposion_project/requirements/base.txt
@@ -23,3 +23,7 @@ python-openid==2.2.5
 metron==0.1
 
 pinax-theme-bootstrap==0.1.5
+django-taggit==0.9.3
+django-reversion==1.5.1
+django-markitup==1.0.0
+markdown==2.1.1
diff --git a/symposion_project/settings.py b/symposion_project/settings.py
index c84b830a..91a363de 100644
--- a/symposion_project/settings.py
+++ b/symposion_project/settings.py
@@ -157,6 +157,10 @@ INSTALLED_APPS = [
     "timezones",
     "emailconfirmation",
     "metron",
+    "markitup",
+    "taggit",
+    "cms",
+    "mptt",
     
     # Pinax
     "pinax.apps.account",
@@ -196,6 +200,8 @@ DEBUG_TOOLBAR_CONFIG = {
     "INTERCEPT_REDIRECTS": False,
 }
 
+MARKITUP_FILTER = ("markdown.markdown", {"safe_mode": True})
+
 # local_settings.py can be used to override environment-specific settings
 # like database and email that differ between development and production.
 try:
diff --git a/symposion_project/templates/cms/page_detail.html b/symposion_project/templates/cms/page_detail.html
new file mode 100644
index 00000000..c6ee9b14
--- /dev/null
+++ b/symposion_project/templates/cms/page_detail.html
@@ -0,0 +1,6 @@
+{% extends "site_base.html" %}
+
+{% block body %}
+    <h1>{{ page.title }}</h1>
+    {{ page.body }}
+{% endblock %}
\ No newline at end of file
diff --git a/symposion_project/urls.py b/symposion_project/urls.py
index e8feb4b1..c22b7126 100644
--- a/symposion_project/urls.py
+++ b/symposion_project/urls.py
@@ -10,6 +10,7 @@ from pinax.apps.account.openid_consumer import PinaxConsumer
 
 handler500 = "pinax.views.server_error"
 
+WIKI_SLUG = r"(([\w-]{2,})(/[\w-]{2,})*)"
 
 urlpatterns = patterns("",
     url(r"^$", direct_to_template, {
@@ -20,6 +21,8 @@ urlpatterns = patterns("",
     url(r"^about/", include("about.urls")),
     url(r"^account/", include("pinax.apps.account.urls")),
     url(r"^openid/", include(PinaxConsumer().urls)),
+    url(r"^markitup/", include("markitup.urls")),
+    url(r"^pages/(?P<slug>%s)/$" % WIKI_SLUG, "cms.views.page", name="cms_page"),
 )