diff --git a/CHANGELOG.md b/CHANGELOG.md
index 55ce5e8..0935339 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -5,6 +5,13 @@ All notable changes to wuttaweb will be documented in this file.
 The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/)
 and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html).
 
+## v0.11.0 (2024-08-20)
+
+### Feat
+
+- split up base templates into more sections (def blocks)
+- simplify base/page/form template structure; add docs
+
 ## v0.10.2 (2024-08-19)
 
 ### Fix
diff --git a/docs/narr/index.rst b/docs/narr/index.rst
index 20f3a85..f9beea1 100644
--- a/docs/narr/index.rst
+++ b/docs/narr/index.rst
@@ -2,15 +2,7 @@
 Documentation
 =============
 
-TODO
+.. toctree::
+   :maxdepth: 2
 
-..
-   .. toctree::
-      :maxdepth: 2
-
-      install/index
-      config/index
-      cli/index
-      handlers/index
-      providers/index
-      db/index
+   templates/index
diff --git a/docs/narr/templates/base.rst b/docs/narr/templates/base.rst
new file mode 100644
index 0000000..554c721
--- /dev/null
+++ b/docs/narr/templates/base.rst
@@ -0,0 +1,251 @@
+
+Base Templates
+==============
+
+This describes the base templates.  When creating a custom page
+template, you most often need to inherit from one of these:
+
+* :ref:`page_base_template`
+* :ref:`form_base_template`
+* :ref:`master_base_templates`
+
+.. note::
+
+   Any of these templates may be overridden; see
+   :ref:`mako-template-override`.
+
+
+Global Base
+~~~~~~~~~~~
+
+There is exactly one "true base template" for the web app, designated
+as: ``/base.mako``
+
+The default base template is ``wuttaweb:templates/base.mako`` and all
+page templates inherit from it.  However they inherit it by *name*
+only (``/base.mako``) - therefore if you override this via custom
+template search paths, effectively you have changed the **theme**.
+
+In addition to general layout/structure, this template is reponsible
+for creating the Vue app which encompasses the whole of every page.
+It also establishes the ``WholePage`` component which is the Vue app's
+one and only child component.
+
+(``WholePage`` in turn will have other children, for page content.)
+
+There is usually no need to define a template which inherits directly
+from ``/base.mako``, rather you should inherit from ``/page.mako``
+(see next section) or similar.
+
+As pertains to Vue component logic, there are 3 blocks which you may
+find a need to override.  These are defined by ``/base.mako`` so will
+apply to *all* templates:
+
+* ``render_vue_templates()``
+* ``modify_vue_vars()``
+* ``make_vue_components()``
+
+Most often it is necessary to customize ``modify_vue_vars()`` but keep
+reading for an example.
+
+
+.. _page_base_template:
+
+Page Base
+~~~~~~~~~
+
+The common base template for pages, designated as: ``/page.mako``
+
+This extends the Vue logic from ``/base.mako`` by establishing
+``ThisPage`` component, which wraps all content within the current
+page.
+
+The final structure then is conceptually like:
+
+.. code-block:: html
+
+   <div id="app">
+     <whole-page>
+       <!-- menu etc. -->
+       <this-page>
+         <!-- page contents -->
+       </this-page>
+     </whole-page>
+   </div>
+
+Simple usage is to create a template which inherits from
+``/page.mako`` and defines a ``page_content()`` block, e.g.:
+
+.. code-block:: mako
+
+   <%inherit file="/page.mako" />
+
+   <%def name="page_content()">
+     <p>hello world!</p>
+   </%def>
+
+The default ``/page.mako`` logic knows where to render the
+``page_content()`` block so that it fits properly into the
+component/layout structure.
+
+Often you may need to customize Vue component logic for a page; this
+is done by defining one of the blocks mentioned in previous section.
+
+Here is a simple example which shows how this works:
+
+.. code-block:: mako
+
+   <%inherit file="/page.mako" />
+
+   <%def name="page_content()">
+     <b-field label="Foo">
+       <b-input v-model="foo" />
+     </b-field>
+     <b-field>
+     <b-button @click="alertFoo()">
+       Alert
+     </b-button>
+     </b-field>
+   </%def>
+
+   <%def name="modify_vue_vars()">
+     ${parent.modify_vue_vars()}
+     <script>
+
+       // nb. this becomes ThisPage.data.foo
+       ThisPageData.foo = 'bar'
+
+       ThisPage.methods.alertFoo = function() {
+           alert("value of foo is: " + this.foo)
+       }
+
+     </script>
+   </%def>
+
+You can see that ``page_content()`` is able to reference things from
+``ThisPage`` component, while the ``modify_vue_vars()`` block is used
+to define those same things on the component.
+
+
+.. _form_base_template:
+
+Form Base
+~~~~~~~~~
+
+The common base template for pages with a form, designated as:
+``/form.mako``
+
+This expects the context dict to contain ``'form'`` which points to a
+:class:`~wuttaweb.forms.base.Form` instance.
+
+This template extends the Vue logic from ``/page.mako`` by
+establishing a Vue component specific to the form object.
+
+The final structure then is conceptually like:
+
+.. code-block:: html
+
+   <div id="app">
+     <whole-page>
+       <!-- menu etc. -->
+       <this-page>
+         <wutta-form>
+           <!-- fields etc. -->
+         </wutta-form>
+       </this-page>
+     </whole-page>
+   </div>
+
+A simple example which assumes one of the form fields exposes a button
+with click event that triggers ``alertFoo()`` method on the form
+component:
+
+.. code-block:: mako
+
+   <%inherit file="/form.mako" />
+
+   <%def name="modify_vue_vars()">
+     ${parent.modify_vue_vars()}
+     <script>
+
+       // nb. this becomes e.g. WuttaForm.foo when component is created
+       ${form.vue_component}Data.foo = 'bar'
+
+       ${form.vue_component}.methods.alertFoo = function() {
+           alert("value of foo is: " + this.foo)
+       }
+
+     </script>
+   </%def>
+
+.. note::
+
+   By default, ``${form.vue_compoment}`` is rendered as ``WuttaForm``
+   but that is not guaranteed.  You should resist the temptation to
+   hard-code that; always use ``${form.vue_component}`` and (where
+   applicable) ``${form.vue_tagname}``.
+
+   The reason for this is to allow multiple forms to exist on a single
+   page, each with a separate Vue component.  (Which is not shown in
+   the above example.)
+
+   See also :attr:`~wuttaweb.forms.base.Form.vue_component` and
+   :attr:`~wuttaweb.forms.base.Form.vue_tagname`.
+
+
+.. _master_base_templates:
+
+Master Base
+~~~~~~~~~~~
+
+These templates are for use with
+:class:`~wuttaweb.views.master.MasterView`.  Each is the default
+template used for the corresponding route/view, unless a more specific
+template is defined.
+
+The "index" template is unique in that it is (usually) for listing the
+model data:
+
+* ``/master/index.mako``
+
+The "form" template is just a base template, does not directly
+correspond to a route/view.  Other CRUD templates inherit from it.
+This inherits from ``/form.mako`` (see previous section).
+
+* ``/master/form.mako``
+
+These CRUD templates inherit from ``/master/form.mako`` and so
+require a ``'form'`` in the context dict.
+
+* ``/master/create.mako``
+* ``/master/view.mako``
+* ``/master/edit.mako``
+* ``/master/delete.mako``
+
+The "configure" template is for master views which have a
+configuration page.
+
+* ``/master/configure.mako``
+
+Usage for these is not significantly different from the ones shown
+above, in cases where you actually need to override the template.
+
+As an example let's say you have defined a ``WidgetMasterView`` class
+and want to override its "view" template.  You would then create a
+file as ``/widgets/view.mako`` (within your templates folder) and
+be sure to inherit from the correct base template:
+
+.. code-block:: mako
+
+   <%inherit file="/master/view.mako" />
+
+   <%def name="page_content()">
+
+     <p>THIS APPEARS FIRST!</p>
+
+     ## nb. the form will appear here
+     ${parent.page_content()}
+
+     <p>MADE IT TO THE END!</p>
+
+   </%def>
diff --git a/docs/narr/templates/index.rst b/docs/narr/templates/index.rst
new file mode 100644
index 0000000..d560666
--- /dev/null
+++ b/docs/narr/templates/index.rst
@@ -0,0 +1,10 @@
+
+Templates
+=========
+
+.. toctree::
+   :maxdepth: 2
+
+   overview
+   base
+   lookup
diff --git a/docs/narr/templates/lookup.rst b/docs/narr/templates/lookup.rst
new file mode 100644
index 0000000..11247c2
--- /dev/null
+++ b/docs/narr/templates/lookup.rst
@@ -0,0 +1,69 @@
+
+Template Lookup
+===============
+
+The discovery of templates is handled by Mako, and is configurable.
+
+WuttaWeb comes with all templates it needs, in the path designated as
+``wuttaweb:templates``.
+
+When the app renders a page, it invokes the Mako lookup logic, which
+searches one or more folders and returns the first matching file it
+encounters.  By default ``wuttaweb:templates`` is the only place it
+looks.
+
+A template is searched for by "name" but it is more path-like, e.g.
+``/page.mako`` or ``/master/index.mako`` etc.  So for example the file
+at ``wuttaweb:templates/home.mako`` is used for home page (using
+lookup name ``/home.mako``) by default.
+
+
+.. _mako-template-override:
+
+Overriding the Search Paths
+~~~~~~~~~~~~~~~~~~~~~~~~~~~
+
+The basic idea is to give it a list of paths it should search when
+trying to find a template.  The first template file found for a given
+search name is used and no further search is done for that name.
+
+You can define the Mako lookup sequence in your ``web.conf`` as
+follows:
+
+.. code-block:: ini
+
+   [app:main]
+   mako.directories =
+       /random/path/on/disk
+       poser.web:templates
+       wuttaweb:templates
+
+This setting is interpreted by ``pyramid_mako`` (`docs`_).
+
+.. _docs: https://docs.pylonsproject.org/projects/pyramid_mako/en/latest/index.html#mako-directories
+
+Here ``wuttaweb:templates/home.mako`` would still be used by default
+for home page, *unless* e.g. ``/random/path/on/disk/home.mako``
+existed in which case that would be used.
+
+Each path can have an arbitrary set of templates, they will
+effectively be combined to a single set by the app, with the
+definition order determining search priority.
+
+If you are already using a custom ``app.main()`` function for
+constructing the web app during startup, it may be a good idea to
+change the *default* search paths to include your package.
+
+Setup for custom ``app.main()`` is beyond the scope here, but assuming
+you *do* already have one, this is what it looks like::
+
+   from wuttaweb import app as base
+
+   def main(global_config, **settings):
+
+       # nb. set the *default* mako search paths; however config can
+       # still override with method shown above
+       settings.setdefault('mako.directories', ['poser.web:templates',
+                                                'wuttaweb:templates'])
+
+       return base.main(global_config, **settings)
diff --git a/docs/narr/templates/overview.rst b/docs/narr/templates/overview.rst
new file mode 100644
index 0000000..4801e17
--- /dev/null
+++ b/docs/narr/templates/overview.rst
@@ -0,0 +1,15 @@
+
+Overview
+========
+
+WuttaWeb uses the `Mako`_ template language for page rendering.
+
+.. _Mako: https://www.makotemplates.org/
+
+There is a "global" base template which effectively defines the
+"theme" (page layout, Vue component structure).  A few other base
+templates provide a starting point for any custom pages; see
+:doc:`base`.
+
+Templates are found via lookup which is handled by Mako.  This is
+configurable so you can override any or all; see :doc:`lookup`.
diff --git a/pyproject.toml b/pyproject.toml
index 5902f86..fe91ac9 100644
--- a/pyproject.toml
+++ b/pyproject.toml
@@ -6,7 +6,7 @@ build-backend = "hatchling.build"
 
 [project]
 name = "WuttaWeb"
-version = "0.10.2"
+version = "0.11.0"
 description = "Web App for Wutta Framework"
 readme = "README.md"
 authors = [{name = "Lance Edgar", email = "lance@edbob.org"}]
diff --git a/src/wuttaweb/templates/appinfo/configure.mako b/src/wuttaweb/templates/appinfo/configure.mako
index 1ea6481..da7d94d 100644
--- a/src/wuttaweb/templates/appinfo/configure.mako
+++ b/src/wuttaweb/templates/appinfo/configure.mako
@@ -140,8 +140,8 @@
   </div>
 </%def>
 
-<%def name="modify_this_page_vars()">
-  ${parent.modify_this_page_vars()}
+<%def name="modify_vue_vars()">
+  ${parent.modify_vue_vars()}
   <script>
 
     ThisPageData.weblibs = ${json.dumps(weblibs or [])|n}
diff --git a/src/wuttaweb/templates/appinfo/index.mako b/src/wuttaweb/templates/appinfo/index.mako
index 710d02c..342617e 100644
--- a/src/wuttaweb/templates/appinfo/index.mako
+++ b/src/wuttaweb/templates/appinfo/index.mako
@@ -48,8 +48,8 @@
 
 </%def>
 
-<%def name="modify_this_page_vars()">
-  ${parent.modify_this_page_vars()}
+<%def name="modify_vue_vars()">
+  ${parent.modify_vue_vars()}
   <script>
     ThisPageData.configFiles = ${json.dumps([dict(path=p, priority=i) for i, p in enumerate(config.get_prioritized_files(), 1)])|n}
   </script>
diff --git a/src/wuttaweb/templates/auth/login.mako b/src/wuttaweb/templates/auth/login.mako
index 6f07542..e6b77c6 100644
--- a/src/wuttaweb/templates/auth/login.mako
+++ b/src/wuttaweb/templates/auth/login.mako
@@ -19,7 +19,8 @@
   </div>
 </%def>
 
-<%def name="modify_this_page_vars()">
+<%def name="modify_vue_vars()">
+  ${parent.modify_vue_vars()}
   <script>
 
     ${form.vue_component}Data.usernameInput = null
diff --git a/src/wuttaweb/templates/base.mako b/src/wuttaweb/templates/base.mako
index 32635d7..ddee561 100644
--- a/src/wuttaweb/templates/base.mako
+++ b/src/wuttaweb/templates/base.mako
@@ -15,17 +15,27 @@
       <whole-page />
     </div>
 
+    ## nb. sometimes a template needs to define something
+    ## before the body content proper is rendered
+    ${self.before_content()}
+
     ## content body from derived/child template
     ${self.body()}
 
     ## Vue app
-    ${self.make_whole_page_component()}
-    ${self.make_whole_page_app()}
+    ${self.render_vue_templates()}
+    ${self.modify_vue_vars()}
+    ${self.make_vue_components()}
+    ${self.make_vue_app()}
   </body>
 </html>
 
+## nb. this becomes part of the page <title> tag within <head>
+## it also is used as default value for content_title() below
 <%def name="title()"></%def>
 
+## nb. this is the "content title" as shown on screen, within the
+## "hero bar" just below the "index title"
 <%def name="content_title()">${self.title()}</%def>
 
 <%def name="header_core()">
@@ -39,7 +49,10 @@
   ${self.vuejs()}
   ${self.buefy()}
   ${self.fontawesome()}
+  ${self.hamburger_menu_js()}
+</%def>
 
+<%def name="hamburger_menu_js()">
   <script>
 
     ## NOTE: this code was copied from
@@ -86,14 +99,21 @@
 
 <%def name="core_styles()">
   ${self.buefy_styles()}
+  ${self.base_styles()}
+</%def>
 
+<%def name="buefy_styles()">
+  ${h.stylesheet_link(h.get_liburl(request, 'buefy.css'))}
+</%def>
+
+<%def name="base_styles()">
   <style>
 
-    /* ****************************** */
-    /* page */
-    /* ****************************** */
+    ##############################
+    ## page
+    ##############################
 
-    /* nb. helps force footer to bottom of screen */
+    ## nb. helps force footer to bottom of screen
     html, body {
         height: 100%;
     }
@@ -104,12 +124,14 @@
         }
     % endif
 
-    /* nb. this refers to the "menu-sized" app title in far left of main menu */
-    #global-header-title {
+    ## nb. this refers to the "menu-sized" app title in far left of main menu
+    #navbar-brand-title {
         font-weight: bold;
     }
 
-    #current-context {
+    #header-index-title {
+        display: flex;
+        gap: 1.5rem;
         padding-left: 0.5rem;
     }
 
@@ -130,92 +152,44 @@
   </style>
 </%def>
 
-<%def name="buefy_styles()">
-  ${h.stylesheet_link(h.get_liburl(request, 'buefy.css'))}
-</%def>
-
 <%def name="extra_styles()">
   ${base_meta.extra_styles()}
 </%def>
 
 <%def name="head_tags()"></%def>
 
-<%def name="render_whole_page_template()">
+<%def name="render_vue_template_whole_page()">
   <script type="text/x-template" id="whole-page-template">
+
+    ## nb. the whole-page contains 3 elements:
+    ## 1) header-wrapper
+    ## 2) content-wrapper
+    ## 3) footer
     <div id="whole-page"
          style="height: 100%; display: flex; flex-direction: column; justify-content: space-between;">
 
+      ## nb. the header-wrapper contains 2 elements:
+      ## 1) header proper (menu + index title area)
+      ## 2) page/content title area
       <div class="header-wrapper">
 
+        ## nb. the header proper contains 2 elements:
+        ## 1) menu bar
+        ## 2) index title area
         <header>
+
+          ## nb. this is the main menu bar
           <nav class="navbar" role="navigation" aria-label="main navigation">
-
-            <div class="navbar-brand">
-              <a class="navbar-item" href="${url('home')}">
-                <div style="display: flex; gap: 0.3rem; align-items: center;">
-                  ${base_meta.header_logo()}
-                  <div id="global-header-title">
-                    ${base_meta.global_title()}
-                  </div>
-                </div>
-              </a>
-              <a role="button" class="navbar-burger" data-target="navbar-menu" aria-label="menu" aria-expanded="false">
-                <span aria-hidden="true"></span>
-                <span aria-hidden="true"></span>
-                <span aria-hidden="true"></span>
-              </a>
-            </div>
-
-            <div class="navbar-menu" id="navbar-menu">
-              <div class="navbar-start">
-
-                % for topitem in menus:
-                    % if topitem['is_link']:
-                        ${h.link_to(topitem['title'], topitem['url'], target=topitem['target'], class_='navbar-item')}
-                    % else:
-                        <div class="navbar-item has-dropdown is-hoverable">
-                          <a class="navbar-link">${topitem['title']}</a>
-                          <div class="navbar-dropdown">
-                            % for item in topitem['items']:
-                                % if item['is_menu']:
-                                    <% item_hash = id(item) %>
-                                    <% toggle = 'menu_{}_shown'.format(item_hash) %>
-                                    <div>
-                                      <a class="navbar-link" @click.prevent="toggleNestedMenu('${item_hash}')">
-                                        ${item['title']}
-                                      </a>
-                                    </div>
-                                    % for subitem in item['items']:
-                                        % if subitem['is_sep']:
-                                            <hr class="navbar-divider" v-show="${toggle}">
-                                        % else:
-                                            ${h.link_to("{}".format(subitem['title']), subitem['url'], class_='navbar-item nested', target=subitem['target'], **{'v-show': toggle})}
-                                        % endif
-                                    % endfor
-                                % else:
-                                    % if item['is_sep']:
-                                        <hr class="navbar-divider">
-                                    % else:
-                                        ${h.link_to(item['title'], item['url'], class_='navbar-item', target=item['target'])}
-                                    % endif
-                                % endif
-                            % endfor
-                          </div>
-                        </div>
-                    % endif
-                % endfor
-
-              </div><!-- navbar-start -->
-              ${self.render_navbar_end()}
-            </div>
+            ${self.render_navbar_brand()}
+            ${self.render_navbar_menu()}
           </nav>
 
+          ## nb. this is the "index title" area
           <nav class="level" style="margin: 0.5rem 0.5rem 0.5rem auto;">
-            <div class="level-left">
 
-              ## Current Context
-              <div id="current-context" class="level-item"
-                   style="display: flex; gap: 1.5rem;">
+            ## nb. this is the index title proper
+            <div class="level-left">
+              <div id="header-index-title" class="level-item">
                 % if index_title:
                     % if index_url:
                         <h1 class="title">${h.link_to(index_title, index_url)}</h1>
@@ -230,11 +204,12 @@
                     % endif
                 % endif
               </div>
+            </div>
 
-            </div><!-- level-left -->
-
+            ## nb. this is a utility area for the master context
             <div class="level-right">
 
+              ## Configure button
               % if master and master.configurable and not master.configuring and master.has_perm('configure'):
                   <div class="level-item">
                     <wutta-button once type="is-primary"
@@ -244,11 +219,14 @@
                   </div>
               % endif
 
-            </div> <!-- level-right -->
-          </nav><!-- level -->
+              ${self.render_theme_picker()}
+              ${self.render_feedback_button()}
+
+            </div>
+          </nav>
         </header>
 
-        ## Page Title
+        ## nb. the page / content title area (aka. hero bar)
         % if capture(self.content_title):
             <section id="content-title"
                      class="has-background-primary">
@@ -272,10 +250,9 @@
 
       </div> <!-- header-wrapper -->
 
+      ## nb. the page content area
       <div class="content-wrapper"
            style="flex-grow: 1; padding: 0.5rem;">
-
-        ## Page Body
         <section id="page-body" style="height: 100%;">
 
           % if request.session.peek_flash('error'):
@@ -306,10 +283,9 @@
             ${self.render_this_page_component()}
           </div>
         </section>
-
       </div><!-- content-wrapper -->
 
-      ## Footer
+      ## nb. the page footer
       <footer class="footer">
         <div class="content">
           ${base_meta.footer()}
@@ -320,8 +296,71 @@
   </script>
 </%def>
 
-<%def name="render_this_page_component()">
-  <this-page @change-content-title="changeContentTitle" />
+<%def name="render_navbar_brand()">
+  <div class="navbar-brand">
+    <a class="navbar-item" href="${url('home')}">
+      <div style="display: flex; gap: 0.3rem; align-items: center;">
+        ${base_meta.header_logo()}
+        <div id="navbar-brand-title">
+          ${base_meta.global_title()}
+        </div>
+      </div>
+    </a>
+    <a role="button" class="navbar-burger" data-target="navbar-menu" aria-label="menu" aria-expanded="false">
+      <span aria-hidden="true"></span>
+      <span aria-hidden="true"></span>
+      <span aria-hidden="true"></span>
+    </a>
+  </div>
+</%def>
+
+<%def name="render_navbar_menu()">
+  <div class="navbar-menu" id="navbar-menu">
+    ${self.render_navbar_start()}
+    ${self.render_navbar_end()}
+  </div>
+</%def>
+
+<%def name="render_navbar_start()">
+  <div class="navbar-start">
+
+    % for topitem in menus:
+        % if topitem['is_link']:
+            ${h.link_to(topitem['title'], topitem['url'], target=topitem['target'], class_='navbar-item')}
+        % else:
+            <div class="navbar-item has-dropdown is-hoverable">
+              <a class="navbar-link">${topitem['title']}</a>
+              <div class="navbar-dropdown">
+                % for item in topitem['items']:
+                    % if item['is_menu']:
+                        <% item_hash = id(item) %>
+                        <% toggle = 'menu_{}_shown'.format(item_hash) %>
+                        <div>
+                          <a class="navbar-link" @click.prevent="toggleNestedMenu('${item_hash}')">
+                            ${item['title']}
+                          </a>
+                        </div>
+                        % for subitem in item['items']:
+                            % if subitem['is_sep']:
+                                <hr class="navbar-divider" v-show="${toggle}">
+                            % else:
+                                ${h.link_to("{}".format(subitem['title']), subitem['url'], class_='navbar-item nested', target=subitem['target'], **{'v-show': toggle})}
+                            % endif
+                        % endfor
+                    % else:
+                        % if item['is_sep']:
+                            <hr class="navbar-divider">
+                        % else:
+                            ${h.link_to(item['title'], item['url'], class_='navbar-item', target=item['target'])}
+                        % endif
+                    % endif
+                % endfor
+              </div>
+            </div>
+        % endif
+    % endfor
+
+  </div>
 </%def>
 
 <%def name="render_navbar_end()">
@@ -330,6 +369,74 @@
   </div>
 </%def>
 
+<%def name="render_theme_picker()"></%def>
+
+<%def name="render_feedback_button()"></%def>
+
+<%def name="render_vue_script_whole_page()">
+  <script>
+
+    const WholePage = {
+        template: '#whole-page-template',
+        computed: {},
+
+        mounted() {
+            for (let hook of this.mountedHooks) {
+                hook(this)
+            }
+        },
+
+        methods: {
+
+            changeContentTitle(newTitle) {
+                this.contentTitleHTML = newTitle
+            },
+
+            toggleNestedMenu(hash) {
+                const key = 'menu_' + hash + '_shown'
+                this[key] = !this[key]
+            },
+
+            % if request.is_admin:
+
+                startBeingRoot() {
+                    this.$refs.startBeingRootForm.submit()
+                },
+
+                stopBeingRoot() {
+                    this.$refs.stopBeingRootForm.submit()
+                },
+
+            % endif
+        },
+    }
+
+    const WholePageData = {
+        contentTitleHTML: ${json.dumps(capture(self.content_title))|n},
+        referrer: location.href,
+        mountedHooks: [],
+    }
+
+    ## declare nested menu visibility toggle flags
+    % for topitem in menus:
+        % if topitem['is_menu']:
+            % for item in topitem['items']:
+                % if item['is_menu']:
+                    WholePageData.menu_${id(item)}_shown = false
+                % endif
+            % endfor
+        % endif
+    % endfor
+
+  </script>
+</%def>
+
+<%def name="before_content()"></%def>
+
+<%def name="render_this_page_component()">
+  <this-page @change-content-title="changeContentTitle" />
+</%def>
+
 <%def name="render_user_menu()">
   % if request.user:
       <div class="navbar-item has-dropdown is-hoverable">
@@ -409,88 +516,29 @@
 
 <%def name="render_prevnext_header_buttons()"></%def>
 
-<%def name="declare_whole_page_vars()">
-  <script>
+##############################
+## vue components + app
+##############################
 
-    let WholePage = {
-        template: '#whole-page-template',
-        computed: {},
-
-        mounted() {
-            for (let hook of this.mountedHooks) {
-                hook(this)
-            }
-        },
-
-        methods: {
-
-            changeContentTitle(newTitle) {
-                this.contentTitleHTML = newTitle
-            },
-
-            toggleNestedMenu(hash) {
-                const key = 'menu_' + hash + '_shown'
-                this[key] = !this[key]
-            },
-
-            % if request.is_admin:
-
-                startBeingRoot() {
-                    this.$refs.startBeingRootForm.submit()
-                },
-
-                stopBeingRoot() {
-                    this.$refs.stopBeingRootForm.submit()
-                },
-
-            % endif
-        },
-    }
-
-    let WholePageData = {
-        contentTitleHTML: ${json.dumps(capture(self.content_title))|n},
-        mountedHooks: [],
-    }
-
-    ## declare nested menu visibility toggle flags
-    % for topitem in menus:
-        % if topitem['is_menu']:
-            % for item in topitem['items']:
-                % if item['is_menu']:
-                    WholePageData.menu_${id(item)}_shown = false
-                % endif
-            % endfor
-        % endif
-    % endfor
-
-  </script>
+<%def name="render_vue_templates()">
+  ${self.render_vue_template_whole_page()}
+  ${self.render_vue_script_whole_page()}
 </%def>
 
-<%def name="modify_whole_page_vars()"></%def>
+<%def name="modify_vue_vars()"></%def>
 
-<%def name="finalize_whole_page_vars()"></%def>
-
-<%def name="make_whole_page_component()">
+<%def name="make_vue_components()">
   ${make_wutta_components()}
-  ${self.render_whole_page_template()}
-  ${self.declare_whole_page_vars()}
-  ${self.modify_whole_page_vars()}
-  ${self.finalize_whole_page_vars()}
-
   <script>
-
     WholePage.data = function() { return WholePageData }
     Vue.component('whole-page', WholePage)
-
   </script>
 </%def>
 
-<%def name="make_whole_page_app()">
+<%def name="make_vue_app()">
   <script>
-
     new Vue({
         el: '#app'
     })
-
   </script>
 </%def>
diff --git a/src/wuttaweb/templates/base_meta.mako b/src/wuttaweb/templates/base_meta.mako
index c65e68c..65d1ede 100644
--- a/src/wuttaweb/templates/base_meta.mako
+++ b/src/wuttaweb/templates/base_meta.mako
@@ -1,8 +1,6 @@
 ## -*- coding: utf-8; -*-
 
-<%def name="app_title()">${app.get_title()}</%def>
-
-<%def name="global_title()">${self.app_title()}</%def>
+<%def name="global_title()">${app.get_title()}</%def>
 
 <%def name="extra_styles()"></%def>
 
diff --git a/src/wuttaweb/templates/configure.mako b/src/wuttaweb/templates/configure.mako
index 58b707e..d430672 100644
--- a/src/wuttaweb/templates/configure.mako
+++ b/src/wuttaweb/templates/configure.mako
@@ -134,8 +134,8 @@
   </b-notification>
 </%def>
 
-<%def name="modify_this_page_vars()">
-  ${parent.modify_this_page_vars()}
+<%def name="modify_vue_vars()">
+  ${parent.modify_vue_vars()}
   <script>
 
     % if simple_settings is not Undefined:
diff --git a/src/wuttaweb/templates/form.mako b/src/wuttaweb/templates/form.mako
index 9a0d21e..2baf95a 100644
--- a/src/wuttaweb/templates/form.mako
+++ b/src/wuttaweb/templates/form.mako
@@ -9,19 +9,16 @@
   </div>
 </%def>
 
-<%def name="render_this_page_template()">
-  ${parent.render_this_page_template()}
+<%def name="render_vue_templates()">
+  ${parent.render_vue_templates()}
   % if form is not Undefined:
       ${form.render_vue_template()}
   % endif
 </%def>
 
-<%def name="finalize_this_page_vars()">
-  ${parent.finalize_this_page_vars()}
+<%def name="make_vue_components()">
+  ${parent.make_vue_components()}
   % if form is not Undefined:
       ${form.render_vue_finalize()}
   % endif
 </%def>
-
-
-${parent.body()}
diff --git a/src/wuttaweb/templates/master/index.mako b/src/wuttaweb/templates/master/index.mako
index 6731f65..a16aced 100644
--- a/src/wuttaweb/templates/master/index.mako
+++ b/src/wuttaweb/templates/master/index.mako
@@ -12,18 +12,20 @@
   % endif
 </%def>
 
-<%def name="render_this_page_template()">
-  ${parent.render_this_page_template()}
+<%def name="render_vue_templates()">
+  ${parent.render_vue_templates()}
+  ${self.render_vue_template_grid()}
+</%def>
+
+<%def name="render_vue_template_grid()">
   % if grid is not Undefined:
       ${grid.render_vue_template()}
   % endif
 </%def>
 
-<%def name="finalize_this_page_vars()">
-  ${parent.finalize_this_page_vars()}
+<%def name="make_vue_components()">
+  ${parent.make_vue_components()}
   % if grid is not Undefined:
       ${grid.render_vue_finalize()}
   % endif
 </%def>
-
-${parent.body()}
diff --git a/src/wuttaweb/templates/page.mako b/src/wuttaweb/templates/page.mako
index c014e06..840e918 100644
--- a/src/wuttaweb/templates/page.mako
+++ b/src/wuttaweb/templates/page.mako
@@ -1,45 +1,29 @@
 ## -*- coding: utf-8; -*-
 <%inherit file="/base.mako" />
 
-<%def name="context_menu_items()">
-  % if context_menu_list_items is not Undefined:
-      % for item in context_menu_list_items:
-          <li>${item}</li>
-      % endfor
-  % endif
-</%def>
-
 <%def name="page_content()"></%def>
 
-<%def name="render_this_page()">
-  <div style="display: flex;">
-
-    <div class="this-page-content" style="flex-grow: 1;">
-      ${self.page_content()}
-    </div>
-
-    <ul id="context-menu">
-      ${self.context_menu_items()}
-    </ul>
-
-  </div>
+<%def name="render_vue_templates()">
+  ${parent.render_vue_templates()}
+  ${self.render_vue_template_this_page()}
+  ${self.render_vue_script_this_page()}
 </%def>
 
-<%def name="render_this_page_template()">
+<%def name="render_vue_template_this_page()">
   <script type="text/x-template" id="this-page-template">
     <div style="height: 100%;">
-      ${self.render_this_page()}
+      ${self.page_content()}
     </div>
   </script>
 </%def>
 
-<%def name="declare_this_page_vars()">
-  <script type="text/javascript">
+<%def name="render_vue_script_this_page()">
+  <script>
 
-    let ThisPage = {
+    const ThisPage = {
         template: '#this-page-template',
         props: {
-            configureFieldsHelp: Boolean,
+            ## configureFieldsHelp: Boolean,
         },
         computed: {},
         watch: {},
@@ -51,31 +35,16 @@
         },
     }
 
-    let ThisPageData = {
-    }
+    const ThisPageData = {}
 
   </script>
 </%def>
 
-<%def name="modify_this_page_vars()"></%def>
-
-<%def name="finalize_this_page_vars()"></%def>
-
-<%def name="make_this_page_component()">
-  ${self.declare_this_page_vars()}
-  ${self.modify_this_page_vars()}
-  ${self.finalize_this_page_vars()}
-
-  <script type="text/javascript">
-
+<%def name="make_vue_components()">
+  ${parent.make_vue_components()}
+  <script>
     ThisPage.data = function() { return ThisPageData }
-
     Vue.component('this-page', ThisPage)
     ## <% request.register_component('this-page', 'ThisPage') %>
-
   </script>
 </%def>
-
-
-${self.render_this_page_template()}
-${self.make_this_page_component()}