diff --git a/test/unittest_views.py b/test/unittest_views.py
new file mode 100644
index 0000000000000000000000000000000000000000..e912f1af89b2f8818a112950d39f8f0e386c7489_dGVzdC91bml0dGVzdF92aWV3cy5weQ==
--- /dev/null
+++ b/test/unittest_views.py
@@ -0,0 +1,19 @@
+from logilab.common.testlib import unittest_main, tag
+
+from cubicweb.devtools.testlib import CubicWebTC
+from cubicweb.devtools.htmlparser import XMLValidator
+
+
+class BoostrapTheMainTemplateTC(CubicWebTC):
+
+    @tag('index')
+    def test_valid_xhtml_index(self):
+        self.view('index')
+
+    @tag('error')
+    def test_valid_xhtml_error(self):
+        valid = self.content_type_validators.get('text/html', XMLValidator)()
+        page = valid.parse_string(self.vreg['views'].main_template(self.request(), 'error-template'))
+
+if __name__ == '__main__':
+    unittest_main()
diff --git a/views/basetemplates.py b/views/basetemplates.py
index 292de533fa6918b70ed6639fef402cf1d92b740c_dmlld3MvYmFzZXRlbXBsYXRlcy5weQ==..e912f1af89b2f8818a112950d39f8f0e386c7489_dmlld3MvYmFzZXRlbXBsYXRlcy5weQ== 100644
--- a/views/basetemplates.py
+++ b/views/basetemplates.py
@@ -19,11 +19,5 @@
 
 basetemplates.TheMainTemplate.doctype = HTML5
 
-
-### XXX / TODOs
-###
-### <footer> is set by ContentFooter class. In orbui, it's set by
-###          the main template
-
 @monkeypatch(basetemplates.TheMainTemplate)
 def call(self, view):
@@ -28,4 +22,6 @@
 @monkeypatch(basetemplates.TheMainTemplate)
 def call(self, view):
+    print self
+    self.grid_nb_cols = 12
     self.set_request_content_type()
     self.template_header(self.content_type, view)
@@ -30,25 +26,5 @@
     self.set_request_content_type()
     self.template_header(self.content_type, view)
-    w = self.w
-    w(u'<div class="row">')
-    w(u'<div class="col-md-12" id="pageContent">')
-    vtitle = self._cw.form.get('vtitle')
-    if vtitle:
-        w(u'<div class="vtitle">%s</div>\n' % xml_escape(vtitle))
-    # display entity type restriction component
-    etypefilter = self._cw.vreg['components'].select_or_none(
-        'etypenavigation', self._cw, rset=self.cw_rset)
-    if etypefilter and etypefilter.cw_propval('visible'):
-        etypefilter.render(w=w)
-    nav_html = UStringIO()
-    if view and not view.handle_pagination:
-        view.paginate(w=nav_html.write)
-    w(nav_html.getvalue())
-    w(u'<div id="contentmain">\n')
-    view.render(w=w)
-    w(u'</div>\n') # closes id=contentmain
-    w(nav_html.getvalue())
-    w(u'</div>\n' # closes id=pageContent
-      u'</div>\n') # closes row
+    self.template_page_content(view)
     self.template_footer(view)
 
@@ -53,6 +29,5 @@
     self.template_footer(view)
 
-
 @monkeypatch(basetemplates.TheMainTemplate)
 def template_html_header(self, content_type, page_title,
                          additional_headers=()):
@@ -81,4 +56,9 @@
 
 @monkeypatch(basetemplates.TheMainTemplate)
 def template_body_header(self, view):
+    self.w(u'<body>\n')
+    self.wview('header', rset=self.cw_rset, view=view)
+
+@monkeypatch(basetemplates.TheMainTemplate)
+def template_page_content(self, view):
     w = self.w
@@ -84,5 +64,3 @@
     w = self.w
-    w(u'<body>\n')
-    self.wview('header', rset=self.cw_rset, view=view)
     w(u'<div id="page" class="container">\n'
       u'<div class="row">\n')
@@ -87,11 +65,33 @@
     w(u'<div id="page" class="container">\n'
       u'<div class="row">\n')
-    #w(u'<div class="col-md-3">')
-    nb_boxes = self.nav_column(view, 'left')
-    #w(u'</div>')
-    if nb_boxes is not None and nb_boxes:
-        content_span = 9
-    else:
-        content_span = 12
-    w(u'<div id="contentColumn" class="col-md-%s">' % content_span)
+    left_boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects(
+        self._cw, rset=self.cw_rset, view=view, context='left'))
+    right_boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects(
+        self._cw, rset=self.cw_rset, view=view, context='right'))
+    nb_boxes = int(bool(left_boxes)) + int(bool(right_boxes))
+    content_cols = 12
+    if nb_boxes:
+        content_cols = self.grid_nb_cols-(3*nb_boxes)
+    self.nav_column(view, left_boxes, 'left')
+    self.content_column(view, content_cols)
+    self.nav_column(view, right_boxes, 'right')
+    self.w(u'</div>\n') # closes class=row
+
+@monkeypatch(basetemplates.TheMainTemplate)
+def nav_column(self, view, boxes, context):
+    if boxes:
+        getlayout = self._cw.vreg['components'].select
+        self.w(u'<div id="aside-main-%s" class="col-md-3">\n' %
+               context)
+        self.w(u'<div class="navboxes" id="navColumn%s">\n' % context.capitalize())
+        for box in boxes:
+            box.render(w=self.w, view=view)
+        self.w(u'</div>\n')
+        self.w(u'</div>\n')
+    return len(boxes)
+
+@monkeypatch(basetemplates.TheMainTemplate)
+def content_column(self, view, content_cols):
+    w = self.w
+    w(u'<div id="contentColumn" class="col-md-%s">' % content_cols)
     components = self._cw.vreg['components']
@@ -97,7 +97,40 @@
     components = self._cw.vreg['components']
+    self.content_components(view, components)
+    self.content_header(view)
+    w(u'<div class="row">')
+    w(u'<div id="pageContent">')
+    vtitle = self._cw.form.get('vtitle')
+    if vtitle:
+        w(u'<div class="vtitle">%s</div>\n' % xml_escape(vtitle))
+    self.content_navrestriction_components(view, components)
+    nav_html = UStringIO()
+    if view and not view.handle_pagination:
+        view.paginate(w=nav_html.write)
+    w(nav_html.getvalue())
+    w(u'<div id="contentmain">\n')
+    view.render(w=w)
+    w(u'</div>\n') # closes id=contentmain
+    w(nav_html.getvalue())
+    w(u'</div>\n' # closes id=pageContent
+      u'</div>\n') # closes row
+    self.content_footer(view)
+    w(u'</div>\n') # closes div#contentColumn in template_body_header
+
+
+@monkeypatch(basetemplates.TheMainTemplate)
+def content_footer(self, view=None):
+    # TODO : do not add the wrapping div if no data
+    self.w(u'<div class="row">') # metadata and so
+    self.wview('contentfooter', rset=self.cw_rset, view=view)
+    self.w(u'</div>\n') # closes row
+
+
+@monkeypatch(basetemplates.TheMainTemplate)
+def content_components(self, view, components):
+    """TODO : should use context"""
     rqlcomp = components.select_or_none('rqlinput', self._cw, rset=self.cw_rset)
     if rqlcomp:
         rqlcomp.render(w=self.w, view=view)
     msgcomp = components.select_or_none('applmessages', self._cw, rset=self.cw_rset)
     if msgcomp:
         msgcomp.render(w=self.w)
@@ -98,10 +131,17 @@
     rqlcomp = components.select_or_none('rqlinput', self._cw, rset=self.cw_rset)
     if rqlcomp:
         rqlcomp.render(w=self.w, view=view)
     msgcomp = components.select_or_none('applmessages', self._cw, rset=self.cw_rset)
     if msgcomp:
         msgcomp.render(w=self.w)
-    self.content_header(view)
+
+@monkeypatch(basetemplates.TheMainTemplate)
+def content_navrestriction_components(self, view, components):
+    # display entity type restriction component
+    etypefilter = components.select_or_none(
+        'etypenavigation', self._cw, rset=self.cw_rset)
+    if etypefilter and etypefilter.cw_propval('visible'):
+        etypefilter.render(w=self.w)
 
 @monkeypatch(basetemplates.TheMainTemplate)
 def template_footer(self, view=None):
@@ -105,10 +145,4 @@
 
 @monkeypatch(basetemplates.TheMainTemplate)
 def template_footer(self, view=None):
-    self.w(u'<div class="row">')
-    self.content_footer(view)
-    self.w(u'</div>')
-    self.w(u'</div>\n') # XXX closes div#contentColumn col-md-9 in template_body_header
-    self.nav_column(view, 'right')
-    self.w(u'</div>\n') # XXX closes div#page in template_body_header
     self.wview('footer', rset=self.cw_rset)
@@ -114,23 +148,6 @@
     self.wview('footer', rset=self.cw_rset)
-    self.w(u'</div>'    # closes class="row"
-           u'</div>')   # closes class="container"
-    self.w(u'</body>')
-
-@monkeypatch(basetemplates.TheMainTemplate)
-def nav_column(self, view, context):
-    boxes = list(self._cw.vreg['ctxcomponents'].poss_visible_objects(
-        self._cw, rset=self.cw_rset, view=view, context=context))
-    if boxes:
-        getlayout = self._cw.vreg['components'].select
-        self.w(u'<div id="aside-main-%s" class="col-md-3">\n' %
-               context) # XXX Should arrange Facets soon
-        self.w(u'<div class="navboxes" id="navColumn%s">\n' % context.capitalize())
-        for box in boxes:
-            box.render(w=self.w, view=view)
-        self.w(u'</div>'
-               u'</div>')
-    return len(boxes)
-
+    self.w(u'</div>\n') # closes id="page"
+    self.w(u'</body>\n')
 
 @monkeypatch(basetemplates.HTMLPageHeader)
 def main_header(self, view):
@@ -149,12 +166,12 @@
         for comp in components:
             comp.render(w=w)
             w(u'&#160;')
-        w(u'</div>')
-    w(u'</div></div>\n')
-
+        w(u'</div>\n')
+    w(u'</div>\n') # closes class="container,
+    w(u'</div>\n') # closes id="header"
 
 @monkeypatch(basetemplates.HTMLPageFooter)
 def call(self, **kwargs):
     self.w(u'<footer id="pagefooter">')
     self.w(u'<div id="footer" class="container">')
     self.footer_content()
@@ -155,8 +172,8 @@
 
 @monkeypatch(basetemplates.HTMLPageFooter)
 def call(self, **kwargs):
     self.w(u'<footer id="pagefooter">')
     self.w(u'<div id="footer" class="container">')
     self.footer_content()
-    self.w(u'</div>')
-    self.w(u'</footer>')
+    self.w(u'</div>\n')
+    self.w(u'</footer>\n')