diff --git a/.vagrant-puppet/files/etc/icinga2/conf.d/test-config.conf b/.vagrant-puppet/files/etc/icinga2/conf.d/test-config.conf
index 412307328..f25df325b 100644
--- a/.vagrant-puppet/files/etc/icinga2/conf.d/test-config.conf
+++ b/.vagrant-puppet/files/etc/icinga2/conf.d/test-config.conf
@@ -1,6 +1,6 @@
 object CheckCommand "dummy-host" {
     import "plugin-check-command"
-    command = [ PluginDir + "/libexec/test_hostcheck.pl" ]
+    command = [ PluginDir + "/test_hostcheck.pl" ]
     arguments = {
             "--type"            = "$check_type$"
             "--failchance"      = "$check_failchance$"
@@ -18,7 +18,7 @@ object CheckCommand "dummy-host" {
 
 object CheckCommand "dummy-service" {
     import "plugin-check-command"
-    command = [ PluginDir + "/libexec/test_servicecheck.pl" ]
+    command = [ PluginDir + "/test_servicecheck.pl" ]
     arguments = {
             "--total-critical-on-host"  = "$check_critical_on_host$"
             "--total-warning-on-host"   = "$check_warning_on_host$"
diff --git a/.vagrant-puppet/files/etc/icingaweb/menu.ini b/.vagrant-puppet/files/etc/icingaweb/menu.ini
index f01ebaecc..d6f342655 100644
--- a/.vagrant-puppet/files/etc/icingaweb/menu.ini
+++ b/.vagrant-puppet/files/etc/icingaweb/menu.ini
@@ -18,10 +18,15 @@ title = "Configuration"
 url   = "config"
 priority = 300
 
+[System.Modules]
+title = "Modules"
+url   = "config/modules"
+priority = 400
+
 [System.ApplicationLog]
 title = "Application log"
 url   = "list/applicationlog"
-priority = 400
+priority = 500
 
 [Logout]
 url = "authentication/logout"
diff --git a/.vagrant-puppet/files/etc/icingaweb/modules/doc/menu.ini b/.vagrant-puppet/files/etc/icingaweb/modules/doc/menu.ini
new file mode 100644
index 000000000..86889b239
--- /dev/null
+++ b/.vagrant-puppet/files/etc/icingaweb/modules/doc/menu.ini
@@ -0,0 +1,5 @@
+[Documentation]
+title       = "Documentation"
+icon        = "img/icons/comment.png"
+url         = "doc"
+priority    = 80
diff --git a/.vagrant-puppet/manifests/default.pp b/.vagrant-puppet/manifests/default.pp
index 3f77ab870..86065d8fc 100644
--- a/.vagrant-puppet/manifests/default.pp
+++ b/.vagrant-puppet/manifests/default.pp
@@ -3,7 +3,7 @@ include mysql
 include pgsql
 include openldap
 
-Exec { path => '/bin:/usr/bin:/sbin' }
+Exec { path => '/bin:/usr/bin:/sbin:/usr/sbin' }
 
 $icingaVersion = '1.11.5'
 $icinga2Version = '2.0.1'
@@ -76,10 +76,10 @@ cmmi { 'icinga-mysql':
               --with-htmurl=/icinga-mysql --with-httpd-conf-file=/etc/httpd/conf.d/icinga-mysql.conf \
               --with-cgiurl=/icinga-mysql/cgi-bin \
               --with-http-auth-file=/usr/share/icinga/htpasswd.users \
-              --with-plugin-dir=/usr/lib64/nagios/plugins/libexec',
+              --with-plugin-dir=/usr/lib64/nagios/plugins',
   creates => '/usr/local/icinga-mysql',
   make    => 'make all && make fullinstall install-config',
-  require => [ User['icinga'], Cmmi['icinga-plugins'], Package['apache'] ],
+  require => [ User['icinga'], Class['monitoring-plugins'], Package['apache'] ],
   notify  => Service['apache']
 }
 
@@ -102,10 +102,10 @@ cmmi { 'icinga-pgsql':
               --with-htmurl=/icinga-pgsql --with-httpd-conf-file=/etc/httpd/conf.d/icinga-pgsql.conf \
               --with-cgiurl=/icinga-pgsql/cgi-bin \
               --with-http-auth-file=/usr/share/icinga/htpasswd.users \
-              --with-plugin-dir=/usr/lib64/nagios/plugins/libexec',
+              --with-plugin-dir=/usr/lib64/nagios/plugins',
   creates => '/usr/local/icinga-pgsql',
   make    => 'make all && make fullinstall install-config',
-  require => [ User['icinga'], Cmmi['icinga-plugins'], Package['apache'] ],
+  require => [ User['icinga'], Class['monitoring-plugins'], Package['apache'] ],
   notify  => Service['apache']
 }
 
@@ -210,16 +210,7 @@ exec { 'icinga-htpasswd':
   require => Class['apache']
 }
 
-cmmi { 'icinga-plugins':
-  url     => "https://www.monitoring-plugins.org/download/monitoring-plugins-${pluginVersion}.tar.gz",
-  output  => "monitoring-plugins-${pluginVersion}.tar.gz",
-  flags   => '--prefix=/usr/lib64/nagios/plugins \
-              --with-nagios-user=icinga --with-nagios-group=icinga \
-              --with-cgiurl=/icinga-mysql/cgi-bin',
-  creates => '/usr/lib64/nagios/plugins/libexec',
-  make    => 'make && make install',
-  require => User['icinga']
-}
+include monitoring-plugins
 
 cmmi { 'mk-livestatus':
   url     => "http://mathias-kettner.de/download/mk-livestatus-${livestatusVersion}.tar.gz",
@@ -425,7 +416,7 @@ package { 'icinga2-ido-mysql':
 
 exec { 'populate-icinga2-mysql-db':
   unless  => 'mysql -uicinga2 -picinga2 icinga2 -e "SELECT * FROM icinga_dbversion;" &> /dev/null',
-  command => 'mysql -uroot icinga2 < /usr/share/doc/icinga2-ido-mysql-$(rpm -q icinga2-ido-mysql | cut -d\'-\' -f4)/schema/mysql.sql',
+  command => 'mysql -uroot icinga2 < /usr/share/icinga2-ido-mysql/schema/mysql.sql',
   require => [ Exec['create-mysql-icinga2-db'], Package['icinga2-ido-mysql'] ]
 }
 
@@ -577,7 +568,7 @@ populate_monitoring_test_config { ['commands', 'contacts', 'dependencies',
 }
 
 define populate_monitoring_test_config_plugins {
-  file { "/usr/lib64/nagios/plugins/libexec/${name}":
+  file { "/usr/lib64/nagios/plugins/${name}":
     owner   => 'icinga',
     group   => 'icinga',
     source  => "/usr/local/share/misc/monitoring_test_config/plugins/${name}",
@@ -795,3 +786,15 @@ file { '/etc/bash_completion.d/icingacli':
    require   => Exec['install bash-completion']
 }
 
+file { '/etc/icingaweb/modules/doc/':
+  ensure    => 'directory',
+  owner     => 'apache',
+  group     => 'apache'
+}
+
+file { '/etc/icingaweb/modules/doc/menu.ini':
+  source    => 'puppet:////vagrant/.vagrant-puppet/files/etc/icingaweb/modules/doc/menu.ini',
+  owner     => 'apache',
+  group     => 'apache',
+}
+
diff --git a/.vagrant-puppet/modules/monitoring-plugins/manifests/init.pp b/.vagrant-puppet/modules/monitoring-plugins/manifests/init.pp
new file mode 100644
index 000000000..6dc7be09d
--- /dev/null
+++ b/.vagrant-puppet/modules/monitoring-plugins/manifests/init.pp
@@ -0,0 +1,9 @@
+class monitoring-plugins {
+  include epel
+
+  # nagios plugins from epel
+  package { 'nagios-plugins-all':
+    ensure => installed,
+    require => Class['epel']
+  }
+}
\ No newline at end of file
diff --git a/application/controllers/AuthenticationController.php b/application/controllers/AuthenticationController.php
index 7d4864d5a..4b43ed71b 100644
--- a/application/controllers/AuthenticationController.php
+++ b/application/controllers/AuthenticationController.php
@@ -35,12 +35,17 @@ class AuthenticationController extends ActionController
     public function loginAction()
     {
         $auth = $this->Auth();
-        $this->view->form = new LoginForm();
-        $this->view->form->setRequest($this->_request);
+        $this->view->form = $form = new LoginForm();
+        $form->setRequest($this->_request);
         $this->view->title = $this->translate('Icingaweb Login');
 
         try {
-            $redirectUrl = Url::fromPath($this->params->get('redirect', 'dashboard'));
+            $redirectUrl = $this->view->form->getValue('redirect');
+            if ($redirectUrl) {
+                $redirectUrl = Url::fromPath($redirectUrl);
+            } else {
+                $redirectUrl = Url::fromPath('dashboard');
+            }
 
             if ($auth->isAuthenticated()) {
                 $this->rerenderLayout()->redirectNow($redirectUrl);
@@ -49,13 +54,9 @@ class AuthenticationController extends ActionController
             try {
                 $config = Config::app('authentication');
             } catch (NotReadableError $e) {
-                Logger::error(
-                    new Exception('Cannot load authentication configuration. An exception was thrown:', 0, $e)
-                );
                 throw new ConfigurationError(
-                    t(
-                        'No authentication methods available. Authentication configuration could not be loaded.'
-                        . ' Please check the system log or Icinga Web 2 log for more information'
+                    $this->translate(
+                        'Could not read your authentiction.ini, no authentication methods are available.'
                     )
                 );
             }
@@ -72,12 +73,20 @@ class AuthenticationController extends ActionController
                         }
                     }
                 }
-            } elseif ($this->view->form->isSubmittedAndValid()) {
-                $user = new User($this->view->form->getValue('username'));
-                $password = $this->view->form->getValue('password');
+            } elseif ($form->isSubmittedAndValid()) {
+                $user = new User($form->getValue('username'));
+                $password = $form->getValue('password');
                 $backendsTried = 0;
                 $backendsWithError = 0;
 
+                $redirectUrl = $form->getValue('redirect');
+
+                if ($redirectUrl) {
+                    $redirectUrl = Url::fromPath($redirectUrl);
+                } else {
+                    $redirectUrl = Url::fromPath('dashboard');
+                }
+
                 foreach ($chain as $backend) {
                     if ($backend instanceof AutoLoginBackend) {
                         continue;
@@ -97,29 +106,29 @@ class AuthenticationController extends ActionController
                 }
                 if ($backendsTried === 0) {
                     throw new ConfigurationError(
-                        t(
-                            'No authentication methods available. It seems that no authentication method has been set'
-                            . ' up. Please check the system log or Icinga Web 2 log for more information'
-                        )
+                        $this->translate(
+                            'No authentication methods available. Did you create'
+                          . ' authentication.ini when installing Icinga Web 2?'
+                         )
                     );
                 }
                 if ($backendsTried === $backendsWithError) {
                     throw new ConfigurationError(
                         $this->translate(
-                            'No authentication methods available. It seems that all set up authentication methods have'
-                            . ' errors. Please check the system log or Icinga Web 2 log for more information'
+                            'All configured authentication methods failed.'
+                          . ' Please check the system log or Icinga Web 2 log for more information.'
                         )
                     );
                 }
                 if ($backendsWithError) {
-                    $this->view->form->addNote(
+                    $form->addNote(
                         $this->translate(
-                            'Note that not all authentication backends are available for authentication because they'
-                            . ' have errors. Please check the system log or Icinga Web 2 log for more information'
+                            'Please note that not all authentication methods where available.'
+                          . ' Check the system log or Icinga Web 2 log for more information.'
                         )
                     );
                 }
-                $this->view->form->getElement('password')->addError($this->translate('Incorrect username or password'));
+                $form->getElement('password')->addError($this->translate('Incorrect username or password'));
             }
         } catch (Exception $e) {
             $this->view->errorInfo = $e->getMessage();
diff --git a/application/controllers/ConfigController.php b/application/controllers/ConfigController.php
index 31739c30e..45be1d3fd 100644
--- a/application/controllers/ConfigController.php
+++ b/application/controllers/ConfigController.php
@@ -48,9 +48,6 @@ class ConfigController extends BaseConfigController
         ))->add('logging', array(
             'title' => 'Logging',
             'url'   => 'config/logging'
-        ))->add('modules', array(
-            'title' => 'Modules',
-            'url'   => 'config/modules'
         ));
     }
 
@@ -70,7 +67,7 @@ class ConfigController extends BaseConfigController
         $form->setConfiguration(IcingaConfig::app());
         $form->setRequest($this->_request);
         if ($form->isSubmittedAndValid()) {
-            if (!$this->writeConfigFile($form->getConfig(), 'config'))  {
+            if (!$this->writeConfigFile($form->getConfig(), 'config')) {
                 return;
             }
             Notification::success('New configuration has successfully been stored');
@@ -107,6 +104,11 @@ class ConfigController extends BaseConfigController
      */
     public function modulesAction()
     {
+        $this->view->tabs = Widget::create('tabs')->add('modules', array(
+            'title' => 'Modules',
+            'url'   => 'config/modules'
+        ));
+
         $this->view->tabs->activate('modules');
         $this->view->modules = Icinga::app()->getModuleManager()->select()
             ->from('modules')
diff --git a/application/controllers/LayoutController.php b/application/controllers/LayoutController.php
index 2d1abfb72..8df5c2c21 100644
--- a/application/controllers/LayoutController.php
+++ b/application/controllers/LayoutController.php
@@ -18,7 +18,9 @@ class LayoutController extends ActionController
      */
     public function menuAction()
     {
-        $this->view->menuRenderer = new MenuRenderer(Menu::fromConfig()->order(), Url::fromRequest()->getRelativeUrl());
+        $this->view->menuRenderer = new MenuRenderer(
+            Menu::fromConfig()->order(), Url::fromRequest()->without('renderLayout')->getRelativeUrl()
+        );
     }
 
     /**
diff --git a/application/forms/Authentication/LoginForm.php b/application/forms/Authentication/LoginForm.php
index cbe25b623..0eff8006c 100644
--- a/application/forms/Authentication/LoginForm.php
+++ b/application/forms/Authentication/LoginForm.php
@@ -5,6 +5,7 @@
 namespace Icinga\Form\Authentication;
 
 use Icinga\Web\Form;
+use Icinga\Web\Url;
 
 /**
  * Class LoginForm
@@ -16,12 +17,19 @@ class LoginForm extends Form
      */
     protected function create()
     {
+        $url = Url::fromRequest()->without('renderLayout');
+        
         $this->setName('form_login');
         $this->addElement('text', 'username', array(
             'label'       => t('Username'),
             'placeholder' => t('Please enter your username...'),
             'required'    => true,
         ));
+        $redir = $this->addElement('hidden', 'redirect');
+        $redirectUrl = $url->shift('redirect');
+        if ($redirectUrl) {
+            $this->setDefault('redirect', $redirectUrl);
+        }
 
         $this->addElement('password', 'password', array(
             'label'       => t('Password'),
@@ -34,6 +42,7 @@ class LoginForm extends Form
         } else {
             $this->getElement('username')->setAttrib('class', 'autofocus');
         }
+        $this->setAction((string) $url);
         $this->setSubmitLabel('Login');
     }
 }
diff --git a/application/layouts/scripts/body.phtml b/application/layouts/scripts/body.phtml
index be9531adc..a40715458 100644
--- a/application/layouts/scripts/body.phtml
+++ b/application/layouts/scripts/body.phtml
@@ -38,7 +38,7 @@ if ($notifications->hasMessages()) {
  </div>
 <?php endif ?>
  <div id="main" role="main">
-  <div id="col1" class="container<?= $moduleClass ?>"<?php if ($moduleName): ?> data-icinga-module="<?= $moduleName ?>" <?php endif ?> data-icinga-url="<?= Url::fromRequest() ?>"<?= $refresh ?> style="display: block">
+  <div id="col1" class="container<?= $moduleClass ?>"<?php if ($moduleName): ?> data-icinga-module="<?= $moduleName ?>" <?php endif ?> data-icinga-url="<?= Url::fromRequest()->without('renderLayout') ?>"<?= $refresh ?> style="display: block">
    <?= $this->render('inline.phtml') ?>
   </div>
   <div id="col2" class="container">
diff --git a/application/layouts/scripts/layout.phtml b/application/layouts/scripts/layout.phtml
index b4fd504ed..32c4879b4 100644
--- a/application/layouts/scripts/layout.phtml
+++ b/application/layouts/scripts/layout.phtml
@@ -44,6 +44,7 @@ $iframeClass = $isIframe ? ' iframe' : '';
   <!--[if lt IE 9]>
   <script src="<?= $this->baseUrl('js/vendor/respond.min.js');?>"></script>
   <![endif]-->
+  <link type="image/png" rel="shortcut icon" href="<?= $this->baseUrl('img/favicon.png') ?>" />
 
 </head>
 <body id="body">
diff --git a/application/layouts/scripts/parts/navigation.phtml b/application/layouts/scripts/parts/navigation.phtml
index c689db54a..8b557e32f 100644
--- a/application/layouts/scripts/parts/navigation.phtml
+++ b/application/layouts/scripts/parts/navigation.phtml
@@ -14,5 +14,5 @@ if (! $this->auth()->isAuthenticated()) {
   <form action="<?= $this->href('search') ?>" method="get" role="search">
     <input type="text" name="q" class="search autofocus" placeholder="<?= $this->translate('Search...') ?>" autocomplete="off" autocorrect="off" autocapitalize="off" spellcheck="false" />
   </form>
-<?= new MenuRenderer(Menu::fromConfig()->order(), Url::fromRequest()->getRelativeUrl()); ?>
+<?= new MenuRenderer(Menu::fromConfig()->order(), Url::fromRequest()->without('renderLayout')->getRelativeUrl()); ?>
 </div>
diff --git a/config/menu.ini b/config/menu.ini
index 65959c93a..075f4230e 100644
--- a/config/menu.ini
+++ b/config/menu.ini
@@ -18,10 +18,15 @@ title = "Configuration"
 url   = "config"
 priority = 300
 
+[System.Modules]
+title = "Modules"
+url   = "config/modules"
+priority = 400
+
 [System.ApplicationLog]
 title = "Application log"
 url   = "list/applicationlog"
-priority = 400
+priority = 500
 
 [Logout]
 url = "authentication/logout"
diff --git a/config/modules/doc/menu.ini b/config/modules/doc/menu.ini
new file mode 100644
index 000000000..86889b239
--- /dev/null
+++ b/config/modules/doc/menu.ini
@@ -0,0 +1,5 @@
+[Documentation]
+title       = "Documentation"
+icon        = "img/icons/comment.png"
+url         = "doc"
+priority    = 80
diff --git a/icingaweb2.spec b/icingaweb2.spec
index 0da22b8ea..cfdcdafc2 100644
--- a/icingaweb2.spec
+++ b/icingaweb2.spec
@@ -40,11 +40,12 @@
 %endif
 # SLE 11 = 1110
 %if 0%{?suse_version} == 1110
+%define phpname php53
 %define apache2modphpname apache2-mod_php53
 %define usermodparam -A
 %endif
 
-%if "%{_vendor}" == "redhat" || 0%{?suse_version} == 1110
+%if "%{_vendor}" == "redhat"
 %define phpname php
 %define phpzendname php-ZendFramework
 %endif
diff --git a/library/Icinga/Application/Modules/Module.php b/library/Icinga/Application/Modules/Module.php
index 8dd189097..3d5e3752e 100644
--- a/library/Icinga/Application/Modules/Module.php
+++ b/library/Icinga/Application/Modules/Module.php
@@ -5,6 +5,7 @@
 namespace Icinga\Application\Modules;
 
 use Exception;
+use Zend_Controller_Router_Route_Abstract;
 use Zend_Controller_Router_Route as Route;
 use Icinga\Application\ApplicationBootstrap;
 use Icinga\Application\Config;
@@ -135,6 +136,16 @@ class Module
      */
     private $app;
 
+
+    /**
+     * Routes to add to the route chain
+     *
+     * @var array Array of name-route pairs
+     *
+     * @see addRoute()
+     */
+    protected $routes = array();
+
     /**
      * Create a new module object
      *
@@ -166,8 +177,7 @@ class Module
      */
     public function register()
     {
-        $this->registerAutoloader()
-             ->registerWebIntegration();
+        $this->registerAutoloader();
         try {
             $this->launchRunScript();
         } catch (Exception $e) {
@@ -179,6 +189,7 @@ class Module
             );
             return false;
         }
+        $this->registerWebIntegration();
         return true;
     }
 
@@ -658,24 +669,29 @@ class Module
     }
 
     /**
-     * Register routes for web access
+     * Add routes for static content and any route added via addRoute() to the route chain
      *
-     * @return self
+     * @return  self
+     * @see     addRoute()
      */
     protected function registerRoutes()
     {
-        $this->app->getFrontController()->getRouter()->addRoute(
+        $router = $this->app->getFrontController()->getRouter();
+        foreach ($this->routes as $name => $route) {
+            $router->addRoute($name, $route);
+        }
+        $router->addRoute(
             $this->name . '_jsprovider',
             new Route(
                 'js/' . $this->name . '/:file',
                 array(
                     'controller'    => 'static',
                     'action'        =>'javascript',
-                    'module_name'    => $this->name
+                    'module_name'   => $this->name
                 )
             )
         );
-        $this->app->getFrontController()->getRouter()->addRoute(
+        $router->addRoute(
             $this->name . '_img',
             new Route(
                 'img/' . $this->name . '/:file',
@@ -750,4 +766,19 @@ class Module
 
         return $this;
     }
+
+    /**
+     * Add a route which will be added to the route chain
+     *
+     * @param   string                                  $name   Name of the route
+     * @param   Zend_Controller_Router_Route_Abstract   $route  Instance of the route
+     *
+     * @return  self
+     * @see     registerRoutes()
+     */
+    protected function addRoute($name, Zend_Controller_Router_Route_Abstract $route)
+    {
+        $this->routes[$name] = $route;
+        return $this;
+    }
 }
diff --git a/library/Icinga/Cli/Command.php b/library/Icinga/Cli/Command.php
index 081be318c..acb2462e4 100644
--- a/library/Icinga/Cli/Command.php
+++ b/library/Icinga/Cli/Command.php
@@ -7,6 +7,7 @@ namespace Icinga\Cli;
 use Icinga\Cli\Screen;
 use Icinga\Util\Translator;
 use Icinga\Cli\Params;
+use Icinga\Application\Config;
 use Icinga\Application\ApplicationBootstrap as App;
 use Exception;
 
@@ -23,6 +24,10 @@ abstract class Command
     protected $commandName;
     protected $actionName;
 
+    private $config;
+
+    private $configs;
+
     protected $defaultActionName = 'default';
 
     public function __construct(App $app, $moduleName, $commandName, $actionName, $initialize = true)
@@ -41,6 +46,51 @@ abstract class Command
         }
     }
 
+    public function Config($file = null)
+    {
+        if ($this->isModule()) {
+            return $this->getModuleConfig($file);
+        } else {
+            return $this->getMainConfig($file);
+        }
+    }
+
+    private function getModuleConfig($file = null)
+    {
+        if ($file === null) {
+            if ($this->config === null) {
+                $this->config = Config::module($this->moduleName);
+            }
+            return $this->config;
+        } else {
+            if (! array_key_exists($file, $this->configs)) {
+                $this->configs[$file] = Config::module($this->moduleName, $file);
+            }
+            return $this->configs[$file];
+        }
+    }
+
+    private function getMainConfig($file = null)
+    {
+        if ($file === null) {
+            if ($this->config === null) {
+                $this->config = Config::app();
+            }
+            return $this->config;
+        } else {
+            if (! array_key_exists($file, $this->configs)) {
+                $this->configs[$file] = Config::module($module, $file);
+            }
+            return $this->configs[$file];
+        }
+        return $this->config;
+    }
+
+    public function isModule()
+    {
+        return substr(get_class($this), 0, 14) === 'Icinga\\Module\\';
+    }
+
     public function setParams(Params $params)
     {
         $this->params = $params;
diff --git a/library/Icinga/Data/Identifiable.php b/library/Icinga/Data/Identifiable.php
new file mode 100644
index 000000000..cfa727a1d
--- /dev/null
+++ b/library/Icinga/Data/Identifiable.php
@@ -0,0 +1,18 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Data;
+
+/**
+ * Interface for objects that are identifiable by an ID of any type
+ */
+interface Identifiable
+{
+    /**
+     * Get the ID associated with this Identifiable object
+     *
+     * @return mixed
+     */
+    public function getId();
+}
diff --git a/library/Icinga/Data/Tree/Node.php b/library/Icinga/Data/Tree/Node.php
new file mode 100644
index 000000000..b57b0fad4
--- /dev/null
+++ b/library/Icinga/Data/Tree/Node.php
@@ -0,0 +1,79 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Data\Tree;
+
+use SplDoublyLinkedList;
+
+class Node extends SplDoublyLinkedList implements NodeInterface
+{
+    /**
+     * The node's value
+     *
+     * @var mixed
+     */
+    protected $value;
+
+    /**
+     * Create a new node
+     *
+     * @param mixed $value The node's value
+     */
+    public function __construct($value = null)
+    {
+        $this->value = $value;
+    }
+
+    /**
+     * Get the node's value
+     *
+     * @return mixed
+     */
+    public function getValue()
+    {
+        return $this->value;
+    }
+
+    /**
+     * Create a new node from the given value and insert the node as the last child of this node
+     *
+     * @param   mixed           $value  The node's value
+     *
+     * @return  NodeInterface           The appended node
+     */
+    public function appendChild($value)
+    {
+        $child = new static($value);
+        $this->push($child);
+        return $child;
+    }
+
+    /**
+     * Whether this node has child nodes
+     *
+     * @return bool
+     */
+    public function hasChildren()
+    {
+        $current = $this->current();
+        if ($current === null) {
+            $current = $this;
+        }
+        return ! $current->isEmpty();
+    }
+
+    /**
+     * Get the node's child nodes
+     *
+     * @return NodeInterface
+     */
+    public function getChildren()
+    {
+        $current = $this->current();
+        if ($current === null) {
+            $current = $this;
+        }
+        return $current;
+    }
+}
diff --git a/library/Icinga/Data/Tree/NodeInterface.php b/library/Icinga/Data/Tree/NodeInterface.php
new file mode 100644
index 000000000..6953214dc
--- /dev/null
+++ b/library/Icinga/Data/Tree/NodeInterface.php
@@ -0,0 +1,26 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Data\Tree;
+
+use RecursiveIterator;
+
+interface NodeInterface extends RecursiveIterator
+{
+    /**
+     * Create a new node from the given value and insert the node as the last child of this node
+     *
+     * @param   mixed           $value  The node's value
+     *
+     * @return  NodeInterface           The appended node
+     */
+    public function appendChild($value);
+
+    /**
+     * Get the node's value
+     *
+     * @return mixed
+     */
+    public function getValue();
+}
diff --git a/library/Icinga/Web/Controller/ActionController.php b/library/Icinga/Web/Controller/ActionController.php
index 1c4111e4b..13809e83d 100644
--- a/library/Icinga/Web/Controller/ActionController.php
+++ b/library/Icinga/Web/Controller/ActionController.php
@@ -254,10 +254,27 @@ class ActionController extends Zend_Controller_Action
      *
      * @throws  \Exception
      */
-    protected function redirectToLogin($afterLogin = '/dashboard')
+    protected function redirectToLogin($afterLogin = null)
     {
+        $redir = null;
+        if ($afterLogin !== null) {
+            if (! $afterLogin instanceof Url) {
+                $afterLogin = Url::fromPath($afterLogin);
+            }
+            if ($this->isXhr()) {
+                $redir = '__SELF__';
+            } else {
+                // TODO: Ignore /?
+                $redir = $afterLogin->getRelativeUrl();
+            }
+        }
+
         $url = Url::fromPath('authentication/login');
-        $url->setParam('redirect', $afterLogin);
+
+        if ($redir) {
+            $url->setParam('redirect', $redir);
+        }
+
         $this->rerenderLayout()->redirectNow($url);
     }
 
@@ -273,6 +290,27 @@ class ActionController extends Zend_Controller_Action
         return $this->getRequest()->isXmlHttpRequest();
     }
 
+    protected function redirectXhr($url)
+    {
+        if (! $url instanceof Url) {
+            $url = Url::fromPath($url);
+        }
+
+        if ($this->rerenderLayout) {
+            $this->getResponse()->setHeader('X-Icinga-Rerender-Layout', 'yes');
+        }
+        if ($this->reloadCss) {
+            $this->getResponse()->setHeader('X-Icinga-Reload-Css', 'now');
+        }
+
+        $this->getResponse()
+            ->setHeader('X-Icinga-Redirect', rawurlencode($url->getAbsoluteUrl()))
+            ->sendHeaders();
+
+        // TODO: Session shutdown?
+        exit;
+    }
+
     /**
     *  Redirect to a specific url, updating the browsers URL field
     *
@@ -280,26 +318,13 @@ class ActionController extends Zend_Controller_Action
     **/
     public function redirectNow($url)
     {
-        if (! $url instanceof Url) {
-            $url = Url::fromPath($url);
-        }
-        $url = preg_replace('~&amp;~', '&', $url);
         if ($this->isXhr()) {
-            if ($this->rerenderLayout) {
-                $this->getResponse()->setHeader('X-Icinga-Rerender-Layout', 'yes');
-            }
-            if ($this->reloadCss) {
-                $this->getResponse()->setHeader('X-Icinga-Reload-Css', 'now');
-            }
-
-            $this->getResponse()
-                ->setHeader('X-Icinga-Redirect', rawurlencode($url))
-                ->sendHeaders();
-
-            // TODO: Session shutdown?
-            exit;
+            $this->redirectXhr($url);
         } else {
-            $this->_helper->Redirector->gotoUrlAndExit(Url::fromPath($url)->getRelativeUrl());
+            if (! $url instanceof Url) {
+                $url = Url::fromPath($url);
+            }
+            $this->_helper->Redirector->gotoUrlAndExit($url->getRelativeUrl());
         }
     }
 
diff --git a/library/Icinga/Web/Url.php b/library/Icinga/Web/Url.php
index ef5a15aa9..ed3acf6a7 100644
--- a/library/Icinga/Web/Url.php
+++ b/library/Icinga/Web/Url.php
@@ -127,10 +127,6 @@ class Url
         $baseUrl = $request->getBaseUrl();
         $urlObject->setBaseUrl($baseUrl);
 
-        // Fetch fragment manually and remove it from the url, to 'help' the parse_url() function
-        // parsing the url properly. Otherwise calling the function with a fragment, but without a
-        // query will cause unpredictable behaviour.
-        $fragment = self::stripUrlFragment($url);
         $urlParts = parse_url($url);
         if (isset($urlParts['path'])) {
             if ($baseUrl !== '' && strpos($urlParts['path'], $baseUrl) === 0) {
@@ -144,29 +140,14 @@ class Url
             $params = UrlParams::fromQueryString($urlParts['query'])->mergeValues($params);
         }
 
-        if ($fragment) {
-            $urlObject->setAnchor($fragment);
+        if (isset($urlParts['fragment'])) {
+            $urlObject->setAnchor($urlParts['fragment']);
         }
 
         $urlObject->setParams($params);
         return $urlObject;
     }
 
-    /**
-     * Remove the fragment-part of a given url and return it
-     *
-     * @param   string  $url    The url to strip its fragment from
-     *
-     * @return  null|string     The stripped fragment, without the '#'
-     */
-    protected static function stripUrlFragment(&$url)
-    {
-        if (preg_match('@#(.*)$@', $url, $matches)) {
-            $url = str_replace('#' . $matches[1], '', $url);
-            return $matches[1];
-        }
-    }
-
     /**
      * Overwrite the baseUrl
      *
@@ -226,12 +207,12 @@ class Url
      *
      * @return  string
      */
-    public function getRelativeUrl()
+    public function getRelativeUrl($separator = '&')
     {
         if ($this->params->isEmpty()) {
             return $this->path . $this->anchor;
         } else {
-            return $this->path . '?' . $this->params->setSeparator('&amp;') . $this->anchor;
+            return $this->path . '?' . $this->params->toString($separator) . $this->anchor;
         }
     }
 
@@ -251,9 +232,9 @@ class Url
      *
      * @return  string
      */
-    public function getAbsoluteUrl()
+    public function getAbsoluteUrl($separator = '&')
     {
-        return $this->baseUrl . ($this->baseUrl !== '/' ? '/' : '') . $this->getRelativeUrl();
+        return $this->baseUrl . ($this->baseUrl !== '/' ? '/' : '') . $this->getRelativeUrl($separator);
     }
 
     /**
@@ -435,6 +416,6 @@ class Url
      */
     public function __toString()
     {
-        return $this->getAbsoluteUrl();
+        return $this->getAbsoluteUrl('&amp;');
     }
 }
diff --git a/library/Icinga/Web/UrlParams.php b/library/Icinga/Web/UrlParams.php
index 11fed8333..bdf776401 100644
--- a/library/Icinga/Web/UrlParams.php
+++ b/library/Icinga/Web/UrlParams.php
@@ -114,6 +114,18 @@ class UrlParams
         return $ret;
     }
 
+    public function addEncoded($param, $value = true)
+    {
+        $this->params[] = array($param, $this->cleanupValue($value));
+        $this->indexLastOne();
+        return $this;
+    }
+
+    protected function urlEncode($value)
+    {
+        return rawurlencode((string) $value);
+    }
+
     /**
      * Add the given parameter with the given value
      *
@@ -127,9 +139,7 @@ class UrlParams
      */
     public function add($param, $value = true)
     {
-        $this->params[] = array($param, $this->cleanupValue($value));
-        $this->indexLastOne();
-        return $this;
+        return $this->addEncoded($this->urlEncode($param), $this->urlEncode($value));
     }
 
     /**
@@ -198,7 +208,7 @@ class UrlParams
      */
     public function unshift($param, $value)
     {
-        array_unshift($this->params, array($param, $this->cleanupValue($value)));
+        array_unshift($this->params, array($this->urlEncode($param), $this->urlEncode($value)));
         $this->reIndexAll();
         return $this;
     }
@@ -224,7 +234,10 @@ class UrlParams
             unset($this->params[$remove]);
         }
 
-        $this->params[$this->index[$param][0]] = array($param, $this->cleanupValue($value));
+        $this->params[$this->index[$param][0]] = array(
+            $this->urlEncode($param),
+            $this->urlEncode($this->cleanupValue($value))
+        );
         $this->reIndexAll();
 
         return $this;        
@@ -243,7 +256,7 @@ class UrlParams
                 foreach ($this->index[$p] as $key) {
                     unset($this->params[$key]);
                 }
-                $this->changed = true;
+                $changed = true;
             }
         }
 
@@ -303,10 +316,10 @@ class UrlParams
     protected function parseQueryStringPart($part)
     {
         if (strpos($part, '=') === false) {
-            $this->add($part, true);
+            $this->addEncoded($part, true);
         } else {
             list($key, $val) = preg_split('/=/', $part, 2);
-            $this->add($key, $val);
+            $this->addEncoded($key, $val);
         }
     }
 
@@ -315,8 +328,11 @@ class UrlParams
         return $this->params;
     }
 
-    public function __toString()
+    public function toString($separator = null)
     {
+        if ($separator === null) {
+            $separator = $this->separator;
+        }
         $parts = array();
         foreach ($this->params as $p) {
             if ($p[1] === true) {
@@ -325,13 +341,18 @@ class UrlParams
                 $parts[] = $p[0] . '=' . $p[1];
             }
         }
-        return implode($this->separator, $parts);
+        return implode($separator, $parts);
+    }
+
+    public function __toString()
+    {
+        return $this->toString();
     }
 
     public static function fromQueryString($queryString = null)
     {
         if ($queryString === null) {
-            $queryString = $_SERVER['QUERY_STRING'];
+            $queryString = isset($_SERVER['QUERY_STRING']) ? $_SERVER['QUERY_STRING'] : '';
         }
         $params = new static();
         $params->parseQueryString($queryString);
diff --git a/library/Icinga/Web/Widget/Limiter.php b/library/Icinga/Web/Widget/Limiter.php
index 9c6a726ba..5afa7e4b8 100644
--- a/library/Icinga/Web/Widget/Limiter.php
+++ b/library/Icinga/Web/Widget/Limiter.php
@@ -81,7 +81,7 @@ class Limiter extends AbstractWidget
                     $this->url->setParam('limit', $limit),
                     null,
                     array(
-                        'title' => t(sprintf('Show %s rows on one page', $caption))
+                        'title' => sprintf(t('Show %s rows on one page'), $caption)
                     )
                 );
             }
diff --git a/modules/doc/application/controllers/IcingawebController.php b/modules/doc/application/controllers/IcingawebController.php
new file mode 100644
index 000000000..967a2b768
--- /dev/null
+++ b/modules/doc/application/controllers/IcingawebController.php
@@ -0,0 +1,48 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+use \Zend_Controller_Action_Exception;
+use Icinga\Application\Icinga;
+use Icinga\Module\Doc\DocController;
+
+class Doc_IcingawebController extends DocController
+{
+    /**
+     * View the toc of Icinga Web 2's documentation
+     */
+    public function tocAction()
+    {
+        $this->renderToc(Icinga::app()->getApplicationDir('/../doc'), 'Icinga Web 2', 'doc/icingaweb/chapter');
+    }
+
+    /**
+     * View a chapter of Icinga Web 2's documentation
+     *
+     * @throws Zend_Controller_Action_Exception If the required parameter 'chapterId' is missing
+     */
+    public function chapterAction()
+    {
+        $chapterId = $this->getParam('chapterId');
+        if ($chapterId === null) {
+            throw new Zend_Controller_Action_Exception(
+                $this->translate('Missing parameter \'chapterId\''),
+                404
+            );
+        }
+        $this->renderChapter(
+            Icinga::app()->getApplicationDir('/../doc'),
+            $chapterId,
+            'doc/icingaweb/toc',
+            'doc/icingaweb/chapter'
+        );
+    }
+
+    /**
+     * View Icinga Web 2's documentation as PDF
+     */
+    public function pdfAction()
+    {
+        $this->renderPdf(Icinga::app()->getApplicationDir('/../doc'), 'Icinga Web 2', 'doc/icingaweb/chapter');
+    }
+}
diff --git a/modules/doc/application/controllers/IndexController.php b/modules/doc/application/controllers/IndexController.php
index f46fdad87..c83cfabab 100644
--- a/modules/doc/application/controllers/IndexController.php
+++ b/modules/doc/application/controllers/IndexController.php
@@ -2,34 +2,9 @@
 // {{{ICINGA_LICENSE_HEADER}}}
 // {{{ICINGA_LICENSE_HEADER}}}
 
-use Icinga\Module\Doc\Controller as DocController;
-
-use Icinga\Module\Doc\DocParser;
+use Icinga\Module\Doc\DocController;
 
 class Doc_IndexController extends DocController
 {
-    protected $parser;
-
-
-    public function init()
-    {
-        $module = null;
-        $this->parser = new DocParser($module);
-    }
-
-
-    public function tocAction()
-    {
-        // Temporary workaround
-        list($html, $toc)   = $this->parser->getDocumentation();
-        $this->view->toc = $toc;
-    }
-
-    /**
-     * Display the application's documentation
-     */
-    public function indexAction()
-    {
-        $this->populateView();
-    }
+    public function indexAction() {}
 }
diff --git a/modules/doc/application/controllers/ModuleController.php b/modules/doc/application/controllers/ModuleController.php
index 41ba42db6..40913368c 100644
--- a/modules/doc/application/controllers/ModuleController.php
+++ b/modules/doc/application/controllers/ModuleController.php
@@ -2,44 +2,131 @@
 // {{{ICINGA_LICENSE_HEADER}}}
 // {{{ICINGA_LICENSE_HEADER}}}
 
+use \Zend_Controller_Action_Exception;
 use Icinga\Application\Icinga;
-use Icinga\Module\Doc\Controller as DocController;
+use Icinga\Module\Doc\DocController;
+use Icinga\Module\Doc\Exception\DocException;
 
 class Doc_ModuleController extends DocController
 {
     /**
-     * Display module documentations index
+     * List modules which are enabled and having the 'doc' directory
      */
     public function indexAction()
     {
-        $this->view->enabledModules = Icinga::app()->getModuleManager()->listEnabledModules();
-    }
-
-    /**
-     * Display a module's documentation
-     */
-    public function viewAction()
-    {
-        $this->populateView($this->getParam('name'));
-    }
-
-    /**
-     * Provide run-time dispatching of module documentation
-     *
-     * @param   string    $methodName
-     * @param   array     $args
-     *
-     * @return  mixed
-     */
-    public function __call($methodName, $args)
-    {
-        // TODO(el): Setup routing to retrieve module name as param and point route to moduleAction
-        $moduleManager  = Icinga::app()->getModuleManager();
-        $moduleName     = substr($methodName, 0, -6);  // Strip 'Action' suffix
-        if (!$moduleManager->hasEnabled($moduleName)) {
-            // TODO(el): Throw a not found exception once the code has been moved to the moduleAction (see TODO above)
-            return parent::__call($methodName, $args);
+        $moduleManager = Icinga::app()->getModuleManager();
+        $modules = array();
+        foreach (Icinga::app()->getModuleManager()->listEnabledModules() as $enabledModule) {
+            $docDir = $moduleManager->getModuleDir($enabledModule, '/doc');
+            if (is_dir($docDir)) {
+                $modules[] = $enabledModule;
+            }
         }
-        $this->_helper->redirector->gotoSimpleAndExit('view', null, null, array('name' => $moduleName));
+        $this->view->modules = $modules;
+    }
+
+    /**
+     * Assert that the given module is enabled
+     *
+     * @param   $moduleName
+     *
+     * @throws  Zend_Controller_Action_Exception    If the required parameter 'moduleName' is empty or either if the
+     *                                              given module is neither installed nor enabled
+     */
+    protected function assertModuleEnabled($moduleName)
+    {
+        if (empty($moduleName)) {
+            throw new Zend_Controller_Action_Exception(
+                $this->translate('Missing parameter \'moduleName\''),
+                404
+            );
+        }
+        $moduleManager = Icinga::app()->getModuleManager();
+        if (! $moduleManager->hasInstalled($moduleName)) {
+            throw new Zend_Controller_Action_Exception(
+                sprintf($this->translate('Module \'%s\' is not installed'), $moduleName),
+                404
+            );
+        }
+        if (! $moduleManager->hasEnabled($moduleName)) {
+            throw new Zend_Controller_Action_Exception(
+                sprintf($this->translate('Module \'%s\' is not enabled'), $moduleName),
+                404
+            );
+        }
+    }
+
+    /**
+     * View the toc of a module's documentation
+     *
+     * @see assertModuleEnabled()
+     */
+    public function tocAction()
+    {
+        $moduleName = $this->getParam('moduleName');
+        $this->assertModuleEnabled($moduleName);
+        $moduleManager = Icinga::app()->getModuleManager();
+        try {
+            $this->renderToc(
+                $moduleManager->getModuleDir($moduleName, '/doc'),
+                $moduleName,
+                'doc/module/chapter',
+                array('moduleName' => $moduleName)
+            );
+        } catch (DocException $e) {
+            throw new Zend_Controller_Action_Exception($e->getMessage(), 404);
+        }
+        $this->view->moduleName = $moduleName;
+    }
+
+    /**
+     * View a chapter of a module's documentation
+     *
+     * @throws  Zend_Controller_Action_Exception    If the required parameter 'chapterId' is missing or if an error in
+     *                                              the documentation module's library occurs
+     * @see     assertModuleEnabled()
+     */
+    public function chapterAction()
+    {
+        $moduleName = $this->getParam('moduleName');
+        $this->assertModuleEnabled($moduleName);
+        $chapterId = $this->getParam('chapterId');
+        if ($chapterId === null) {
+            throw new Zend_Controller_Action_Exception(
+                $this->translate('Missing parameter \'chapterId\''),
+                404
+            );
+        }
+        $moduleManager = Icinga::app()->getModuleManager();
+        try {
+            $this->renderChapter(
+                $moduleManager->getModuleDir($moduleName, '/doc'),
+                $chapterId,
+                $this->_helper->url->url(array('moduleName' => $moduleName), 'doc/module/toc'),
+                'doc/module/chapter',
+                array('moduleName' => $moduleName)
+            );
+        } catch (DocException $e) {
+            throw new Zend_Controller_Action_Exception($e->getMessage(), 404);
+        }
+        $this->view->moduleName = $moduleName;
+    }
+
+    /**
+     * View a module's documentation as PDF
+     *
+     * @see assertModuleEnabled()
+     */
+    public function pdfAction()
+    {
+        $moduleName = $this->getParam('moduleName');
+        $this->assertModuleEnabled($moduleName);
+        $moduleManager = Icinga::app()->getModuleManager();
+        $this->renderPdf(
+            $moduleManager->getModuleDir($moduleName, '/doc'),
+            $moduleName,
+            'doc/module/chapter',
+            array('moduleName' => $moduleName)
+        );
     }
 }
diff --git a/modules/doc/application/views/scripts/chapter.phtml b/modules/doc/application/views/scripts/chapter.phtml
new file mode 100644
index 000000000..7657d69fb
--- /dev/null
+++ b/modules/doc/application/views/scripts/chapter.phtml
@@ -0,0 +1,3 @@
+<div class="chapter">
+    <?= $sectionRenderer->render($this, $this->getHelper('Url')); ?>
+</div>
diff --git a/modules/doc/application/views/scripts/index/index.phtml b/modules/doc/application/views/scripts/index/index.phtml
index a178cc155..e4218bee2 100644
--- a/modules/doc/application/views/scripts/index/index.phtml
+++ b/modules/doc/application/views/scripts/index/index.phtml
@@ -1,5 +1,6 @@
-<h1>Icinga 2 Documentation</h1>
-<?= $this->partial('module/view.phtml', 'doc', array(
-    'toc'   => $toc,
-    'html'  => $html
-)); ?>
\ No newline at end of file
+<div class="controls"></div>
+<h1><?= $this->translate('Available documentations'); ?></h1>
+<ul>
+    <li><a href="<?= $this->href('doc/icingaweb/toc'); ?>">Icinga Web 2</a></li>
+    <li><a href="<?= $this->href('doc/module/'); ?>"><?= $this->translate('Module documentations'); ?></a></li>
+</ul>
diff --git a/modules/doc/application/views/scripts/index/toc.phtml b/modules/doc/application/views/scripts/index/toc.phtml
deleted file mode 100644
index 9188e21ff..000000000
--- a/modules/doc/application/views/scripts/index/toc.phtml
+++ /dev/null
@@ -1,14 +0,0 @@
-<div class="controls">
-<h1>Module documentations</h1>
-</div>
-<div class="content" data-base-target="_next">
-<?= $this->partial(
-    'layout/menu.phtml',
-    'default',
-    array(
-        'items' => $toc->getChildren(),
-        'sub'   => false,
-        'url'   => ''
-    )
-) ?>
-</div>
diff --git a/modules/doc/application/views/scripts/module/index.phtml b/modules/doc/application/views/scripts/module/index.phtml
index 36f11e15e..cc184016f 100644
--- a/modules/doc/application/views/scripts/module/index.phtml
+++ b/modules/doc/application/views/scripts/module/index.phtml
@@ -1,6 +1,10 @@
-<h1>Module documentations</h1>
+<h1><?= $this->translate('Module documentations'); ?></h1>
 <ul>
-<?php foreach ($enabledModules as $module): ?>
-    <li><a href="<?= $this->href('doc/module/view', array('name' => $module)); ?>"><?= $module ?></a></li>
-<?php endforeach ?>
+    <?php foreach ($modules as $module): ?>
+        <li>
+            <a href="<?= $this->getHelper('Url')->url(array('moduleName' => $module), 'doc/module/toc', false, false); ?>">
+                <?= $module ?>
+            </a>
+        </li>
+    <?php endforeach ?>
 </ul>
diff --git a/modules/doc/application/views/scripts/module/view.phtml b/modules/doc/application/views/scripts/module/view.phtml
deleted file mode 100644
index 291947ad7..000000000
--- a/modules/doc/application/views/scripts/module/view.phtml
+++ /dev/null
@@ -1,7 +0,0 @@
-<?php if ($html === null): ?>
-    <p>No documentation available.</p>
-<?php else: ?>
-<div class="content">
-<?= $html ?>
-</div>
-<?php endif ?>
diff --git a/modules/doc/application/views/scripts/pdf.phtml b/modules/doc/application/views/scripts/pdf.phtml
new file mode 100644
index 000000000..72d77f3c0
--- /dev/null
+++ b/modules/doc/application/views/scripts/pdf.phtml
@@ -0,0 +1,7 @@
+<h1><?= $docName ?> <?= $this->translate('Documentation'); ?></h1>
+<div class="toc">
+    <?= $tocRenderer->render($this, $this->getHelper('Url')); ?>
+</div>
+<div class="chapter">
+    <?= $sectionRenderer->render($this, $this->getHelper('Url')); ?>
+</div>
diff --git a/modules/doc/application/views/scripts/toc.phtml b/modules/doc/application/views/scripts/toc.phtml
new file mode 100644
index 000000000..ca6283d67
--- /dev/null
+++ b/modules/doc/application/views/scripts/toc.phtml
@@ -0,0 +1,6 @@
+<div class="controls">
+    <h1><?= $title ?></h1>
+</div>
+<div class="content toc">
+    <?= $tocRenderer->render($this, $this->getHelper('Url')); ?>
+</div>
diff --git a/modules/doc/library/Doc/Controller.php b/modules/doc/library/Doc/Controller.php
deleted file mode 100644
index 2c5a07d49..000000000
--- a/modules/doc/library/Doc/Controller.php
+++ /dev/null
@@ -1,23 +0,0 @@
-<?php
-// {{{ICINGA_LICENSE_HEADER}}}
-// {{{ICINGA_LICENSE_HEADER}}}
-
-namespace Icinga\Module\Doc;
-
-use Icinga\Web\Controller\ModuleActionController;
-
-class Controller extends ModuleActionController
-{
-    /**
-     * Set HTML and toc
-     *
-     * @param string $module
-     */
-    protected function populateView($module = null)
-    {
-        $parser             = new DocParser($module);
-        list($html, $toc)   = $parser->getDocumentation();
-        $this->view->html   = $html;
-        $this->view->toc    = $toc;
-    }
-}
diff --git a/modules/doc/library/Doc/DocController.php b/modules/doc/library/Doc/DocController.php
new file mode 100644
index 000000000..42f8dce9b
--- /dev/null
+++ b/modules/doc/library/Doc/DocController.php
@@ -0,0 +1,76 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc;
+
+use Icinga\Web\Controller\ModuleActionController;
+
+class DocController extends ModuleActionController
+{
+    /**
+     * Render a chapter
+     *
+     * @param string    $path           Path to the documentation
+     * @param string    $chapterId      ID of the chapter
+     * @param string    $tocUrl
+     * @param string    $url
+     * @param array     $urlParams
+     */
+    protected function renderChapter($path, $chapterId, $tocUrl, $url, array $urlParams = array())
+    {
+        $parser = new DocParser($path);
+        $this->view->sectionRenderer = new SectionRenderer(
+            $parser->getDocTree(),
+            SectionRenderer::decodeUrlParam($chapterId),
+            $tocUrl,
+            $url,
+            $urlParams
+        );
+        $this->view->title = $chapterId;
+        $this->_helper->viewRenderer('chapter', null, true);
+    }
+
+    /**
+     * Render a toc
+     *
+     * @param string    $path           Path to the documentation
+     * @param string    $name           Name of the documentation
+     * @param string    $url
+     * @param array     $urlParams
+     */
+    protected function renderToc($path, $name, $url, array $urlParams = array())
+    {
+        $parser = new DocParser($path);
+        $this->view->tocRenderer = new TocRenderer($parser->getDocTree(), $url, $urlParams);
+        $name = ucfirst($name);
+        $this->view->docName = $name;
+        $this->view->title = sprintf($this->translate('%s Documentation'), $name);
+        $this->_helper->viewRenderer('toc', null, true);
+    }
+
+    /**
+     * Render a pdf
+     *
+     * @param string    $path           Path to the documentation
+     * @param string    $name           Name of the documentation
+     * @param string    $url
+     * @param array     $urlParams
+     */
+    protected function renderPdf($path, $name, $url, array $urlParams = array())
+    {
+        $parser = new DocParser($path);
+        $docTree = $parser->getDocTree();
+        $this->view->tocRenderer = new TocRenderer($docTree, $url, $urlParams);
+        $this->view->sectionRenderer = new SectionRenderer(
+            $docTree,
+            null,
+            null,
+            $url,
+            $urlParams
+        );
+        $this->view->docName = $name;
+        $this->_helper->viewRenderer('pdf', null, true);
+        $this->_request->setParam('format', 'pdf');
+    }
+}
diff --git a/modules/doc/library/Doc/DocException.php b/modules/doc/library/Doc/DocException.php
deleted file mode 100644
index cb7134045..000000000
--- a/modules/doc/library/Doc/DocException.php
+++ /dev/null
@@ -1,11 +0,0 @@
-<?php
-// {{{ICINGA_LICENSE_HEADER}}}
-// {{{ICINGA_LICENSE_HEADER}}}
-
-namespace Icinga\Module\Doc;
-
-use \Exception;
-
-class DocException extends Exception
-{
-}
diff --git a/modules/doc/library/Doc/DocIterator.php b/modules/doc/library/Doc/DocIterator.php
new file mode 100644
index 000000000..43a9c7727
--- /dev/null
+++ b/modules/doc/library/Doc/DocIterator.php
@@ -0,0 +1,62 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc;
+
+use ArrayIterator;
+use Countable;
+use IteratorAggregate;
+use RecursiveIteratorIterator;
+use RecursiveDirectoryIterator;
+
+/**
+ * Iterator over non-empty Markdown files ordered by the case insensitive "natural order" of file names
+ */
+class DocIterator implements Countable, IteratorAggregate
+{
+    /**
+     * Ordered files
+     *
+     * @var array
+     */
+    protected $fileInfo;
+
+    /**
+     * Create a new DocIterator
+     *
+     * @param string $path Path to the documentation
+     */
+    public function __construct($path)
+    {
+        $it = new RecursiveIteratorIterator(
+            new NonEmptyFileIterator(
+                new MarkdownFileIterator(
+                    new RecursiveDirectoryIterator($path)
+                )
+            )
+        );
+        // Unfortunately we have no chance to sort the iterator
+        $fileInfo = iterator_to_array($it);
+        natcasesort($fileInfo);
+        $this->fileInfo = $fileInfo;
+    }
+
+    /**
+     * (non-PHPDoc)
+     * @see Countable::count()
+     */
+    public function count()
+    {
+        return count($this->fileInfo);
+    }
+
+    /**
+     * (non-PHPDoc)
+     * @see IteratorAggregate::getIterator()
+     */
+    public function getIterator()
+    {
+        return new ArrayIterator($this->fileInfo);
+    }
+}
diff --git a/modules/doc/library/Doc/DocParser.php b/modules/doc/library/Doc/DocParser.php
index d4e6875d1..c63532dc1 100644
--- a/modules/doc/library/Doc/DocParser.php
+++ b/modules/doc/library/Doc/DocParser.php
@@ -4,146 +4,65 @@
 
 namespace Icinga\Module\Doc;
 
-use RecursiveIteratorIterator;
-use RecursiveDirectoryIterator;
-use Parsedown;
-use Icinga\Application\Icinga;
-use Icinga\Web\Menu;
-use Icinga\Web\Url;
-
-require_once 'IcingaVendor/Parsedown/Parsedown.php';
+use SplDoublyLinkedList;
+use Icinga\Exception\NotReadableError;
+use Icinga\Module\Doc\Exception\DocEmptyException;
+use Icinga\Module\Doc\Exception\DocException;
 
 /**
  * Parser for documentation written in Markdown
  */
 class DocParser
 {
-    protected $dir;
-
-    protected $module;
+    /**
+     * Path to the documentation
+     *
+     * @var string
+     */
+    protected $path;
 
     /**
-     * Create a new documentation parser for the given module or the application
+     * Iterator over documentation files
      *
-     * @param   string $module
-     *
-     * @throws  DocException
+     * @var DocIterator
      */
-    public function __construct($module = null)
-    {
-        if ($module === null) {
-            $dir = Icinga::app()->getApplicationDir('/../doc');
-        } else {
-            $mm = Icinga::app()->getModuleManager();
-            if (!$mm->hasInstalled($module)) {
-                throw new DocException('Module is not installed');
-            }
-            if (!$mm->hasEnabled($module)) {
-                throw new DocException('Module is not enabled');
-            }
-            $dir = $mm->getModuleDir($module, '/doc');
-        }
-        if (!is_dir($dir)) {
-            throw new DocException('Doc directory does not exist');
-        }
-        $this->dir      = $dir;
-        $this->module   = $module;
-    }
+    protected $docIterator;
 
     /**
-     * Retrieve table of contents and HTML converted from markdown files sorted by filename
+     * Create a new documentation parser for the given path
      *
-     * @return  array
-     * @throws  DocException
+     * @param   string $path Path to the documentation
+     *
+     * @throws  DocException        If the documentation directory does not exist
+     * @throws  NotReadableError    If the documentation directory is not readable
+     * @throws  DocEmptyException   If the documentation directory is empty
      */
-    public function getDocumentation()
+    public function __construct($path)
     {
-        $iter = new RecursiveIteratorIterator(
-            new MarkdownFileIterator(
-                new RecursiveDirectoryIterator($this->dir)
-            )
-        );
-        $fileInfos = iterator_to_array($iter);
-        natcasesort($fileInfos);
-        $cat    = array();
-        $toc    = array((object) array(
-            'level' => 0,
-            'item'  => new Menu('doc')
-        ));
-        $itemPriority = 1;
-        foreach ($fileInfos as $fileInfo) {
-            try {
-                $fileObject = $fileInfo->openFile();
-            } catch (RuntimeException $e) {
-                throw new DocException($e->getMessage());
-            }
-            if ($fileObject->flock(LOCK_SH) === false) {
-                throw new DocException('Couldn\'t get the lock');
-            }
-            $line = null;
-            while (!$fileObject->eof()) {
-                // Save last line for setext-style headers
-                $lastLine   = $line;
-                $line       = $fileObject->fgets();
-                $header     = $this->extractHeader($line, $lastLine);
-                if ($header !== null) {
-                    list($header, $level)   = $header;
-                    $id                     = $this->extractHeaderId($header);
-                    $attribs                = array();
-                    $this->reduceToc($toc, $level);
-                    if ($id === null) {
-                        $path = array();
-                        foreach (array_slice($toc, 1) as $entry) {
-                            $path[] = $entry->item->getTitle();
-                        }
-                        $path[]         = $header;
-                        $id             = implode('-', $path);
-                        $attribs['rel'] = 'nofollow';
-                    }
-                    $id     = urlencode(str_replace('.', '&#46;', strip_tags($id)));
-                    $item   = end($toc)->item->addChild(
-                        $id,
-                        array(
-                            'url' => Url::fromPath(
-                                'doc/module/view',
-                                array(
-                                    'name' => $this->module
-                                )
-                            )->setAnchor($id)->getRelativeUrl(),
-                            'title'     => htmlspecialchars($header),
-                            'priority'  => $itemPriority++,
-                            'attribs'   => $attribs
-                        )
-                    );
-                    $toc[]  = ((object) array(
-                        'level' => $level,
-                        'item'  => $item
-                    ));
-                    $line = '<a name="' . $id . '"></a>' . PHP_EOL . $line;
-                }
-                $cat[] = $line;
-            }
-            $fileObject->flock(LOCK_UN);
+        if (! is_dir($path)) {
+            throw new DocException(
+                sprintf(mt('doc', 'Documentation directory \'%s\' does not exist'), $path)
+            );
         }
-        $html = Parsedown::instance()->parse(implode('', $cat));
-        $html = preg_replace_callback(
-            '#<pre><code class="language-php">(.*?)\</code></pre>#s',
-            array($this, 'highlight'),
-            $html
-        );
-        return array($html, $toc[0]->item);
-    }
-
-    /**
-     * Syntax highlighting for PHP code
-     *
-     * @param   $match
-     *
-     * @return  string
-     */
-    protected function highlight($match)
-    {
-        return highlight_string(htmlspecialchars_decode($match[1]), true);
+        if (! is_readable($path)) {
+            throw new DocException(
+                sprintf(mt('doc', 'Documentation directory \'%s\' is not readable'), $path)
+            );
+        }
+        $docIterator = new DocIterator($path);
+        if ($docIterator->count() === 0) {
+            throw new DocEmptyException(
+                sprintf(
+                    mt(
+                        'doc',
+                        'Documentation directory \'%s\' does not contain any non-empty Markdown file (\'.md\' suffix)'
+                    ),
+                    $path
+                )
+            );
+        }
+        $this->path = $path;
+        $this->docIterator = $docIterator;
     }
 
     /**
@@ -156,28 +75,28 @@ class DocParser
      */
     protected function extractHeader($line, $lastLine)
     {
-        if (!$line) {
+        if (! $line) {
             return null;
         }
         $header = null;
-        if ($line &&
-            $line[0] === '#' &&
-            preg_match('/^#+/', $line, $match) === 1
+        if ($line
+            && $line[0] === '#'
+            && preg_match('/^#+/', $line, $match) === 1
         ) {
-            // Atx-style
+            // Atx
             $level  = strlen($match[0]);
             $header = trim(substr($line, $level));
-            if (!$header) {
+            if (! $header) {
                 return null;
             }
         } elseif (
-            $line &&
-            ($line[0] === '=' || $line[0] === '-') &&
-            preg_match('/^[=-]+\s*$/', $line, $match) === 1
+            $line
+            && ($line[0] === '=' || $line[0] === '-')
+            && preg_match('/^[=-]+\s*$/', $line, $match) === 1
         ) {
             // Setext
             $header = trim($lastLine);
-            if (!$header) {
+            if (! $header) {
                 return null;
             }
             if ($match[0][0] === '=') {
@@ -189,36 +108,67 @@ class DocParser
         if ($header === null) {
             return null;
         }
-        return array($header, $level);
-    }
-
-    /**
-     * Extract header id in an a or a span tag
-     *
-     * @param   string  &$header
-     *
-     * @return  id|null
-     */
-    protected function extractHeaderId(&$header)
-    {
-        if ($header[0] === '<' &&
-            preg_match('#(?:<(?P<tag>a|span) id="(?P<id>.+)"></(?P=tag)>)#u', $header, $match)
+        if ($header[0] === '<'
+            && preg_match('#(?:<(?P<tag>a|span) (?:id|name)="(?P<id>.+)"></(?P=tag)>)\s*#u', $header, $match)
         ) {
             $header = str_replace($match[0], '', $header);
-            return $match['id'];
+            $id = $match['id'];
+        } else {
+            $id = null;
         }
-        return null;
+        return array($header, $id, $level);
     }
 
     /**
-     * Reduce the toc to the given level
+     * Get the documentation tree
      *
-     * @param array &$toc
-     * @param int   $level
+     * @return DocTree
      */
-    protected function reduceToc(array &$toc, $level) {
-        while (end($toc)->level >= $level) {
-            array_pop($toc);
+    public function getDocTree()
+    {
+        $tree = new DocTree();
+        $stack = new SplDoublyLinkedList();
+        foreach ($this->docIterator as $fileInfo) {
+            /* @var $file \SplFileInfo */
+            $file = $fileInfo->openFile();
+            /* @var $file \SplFileObject */
+            $lastLine = null;
+            foreach ($file as $line) {
+                $header = $this->extractHeader($line, $lastLine);
+                if ($header !== null) {
+                    list($title, $id, $level) = $header;
+                    while (! $stack->isEmpty() && $stack->top()->getLevel() >= $level) {
+                        $stack->pop();
+                    }
+                    if ($id === null) {
+                        $path = array();
+                        foreach ($stack as $section) {
+                            /* @var $section Section */
+                            $path[] = $section->getTitle();
+                        }
+                        $path[] = $title;
+                        $id = implode('-', $path);
+                        $noFollow = true;
+                    } else {
+                        $noFollow = false;
+                    }
+                    if ($stack->isEmpty()) {
+                        $chapterId = $id;
+                        $section = new Section($id, $title, $level, $noFollow, $chapterId);
+                        $tree->addRoot($section);
+                    } else {
+                        $chapterId = $stack->bottom()->getId();
+                        $section = new Section($id, $title, $level, $noFollow, $chapterId);
+                        $tree->addChild($section, $stack->top());
+                    }
+                    $stack->push($section);
+                } else {
+                    $stack->top()->appendContent($line);
+                }
+                // Save last line for setext-style headers
+                $lastLine = $line;
+            }
         }
+        return $tree;
     }
 }
diff --git a/modules/doc/library/Doc/DocTree.php b/modules/doc/library/Doc/DocTree.php
new file mode 100644
index 000000000..1b112649c
--- /dev/null
+++ b/modules/doc/library/Doc/DocTree.php
@@ -0,0 +1,80 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc;
+
+use LogicException;
+use Icinga\Data\Identifiable;
+use Icinga\Data\Tree\Node;
+
+/**
+ * Documentation tree
+ */
+class DocTree extends Node
+{
+    /**
+     * All nodes of the tree
+     *
+     * @var array
+     */
+    protected $nodes = array();
+
+    /**
+     * Append a root node to the tree
+     *
+     * @param Identifiable $root
+     */
+    public function addRoot(Identifiable $root)
+    {
+        $rootId = $root->getId();
+        if (isset($this->nodes[$rootId])) {
+            $rootId = uniqid($rootId);
+//            throw new LogicException(
+//                sprintf('Can\'t add root node: a root node with the id \'%s\' already exists', $rootId)
+//            );
+        }
+        $this->nodes[$rootId] = $this->appendChild($root);
+    }
+
+    /**
+     * Append a child node to a parent node
+     *
+     * @param   Identifiable $child
+     * @param   Identifiable $parent
+     *
+     * @throws  LogicException If the the tree does not contain the parent node
+     */
+    public function addChild(Identifiable $child, Identifiable $parent)
+    {
+        $childId = $child->getId();
+        $parentId = $parent->getId();
+        if (isset($this->nodes[$childId])) {
+            $childId = uniqid($childId);
+//            throw new LogicException(
+//                sprintf('Can\'t add child node: a child node with the id \'%s\' already exists', $childId)
+//            );
+        }
+        if (! isset($this->nodes[$parentId])) {
+            throw new LogicException(
+                sprintf(mt('doc', 'Can\'t add child node: there\'s no parent node having the id \'%s\''), $parentId)
+            );
+        }
+        $this->nodes[$childId] = $this->nodes[$parentId]->appendChild($child);
+    }
+
+    /**
+     * Get a node
+     *
+     * @param   mixed $id
+     *
+     * @return  Node|null
+     */
+    public function getNode($id)
+    {
+        if (! isset($this->nodes[$id])) {
+            return null;
+        }
+        return $this->nodes[$id];
+    }
+}
diff --git a/modules/doc/library/Doc/Exception/ChapterNotFoundException.php b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php
new file mode 100644
index 000000000..cd048a162
--- /dev/null
+++ b/modules/doc/library/Doc/Exception/ChapterNotFoundException.php
@@ -0,0 +1,10 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc\Exception;
+
+/**
+ * Exception thrown if a chapter was not found
+ */
+class ChapterNotFoundException extends DocException {}
diff --git a/modules/doc/library/Doc/Exception/DocEmptyException.php b/modules/doc/library/Doc/Exception/DocEmptyException.php
new file mode 100644
index 000000000..2869e2678
--- /dev/null
+++ b/modules/doc/library/Doc/Exception/DocEmptyException.php
@@ -0,0 +1,10 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc\Exception;
+
+/**
+ * Exception thrown if a documentation directory is empty
+ */
+class DocEmptyException extends DocException {}
diff --git a/modules/doc/library/Doc/Exception/DocException.php b/modules/doc/library/Doc/Exception/DocException.php
new file mode 100644
index 000000000..374ea3e01
--- /dev/null
+++ b/modules/doc/library/Doc/Exception/DocException.php
@@ -0,0 +1,12 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc\Exception;
+
+use RuntimeException;
+
+/**
+ * Exception thrown if an error in the documentation module's library occurs
+ */
+class DocException extends RuntimeException {}
diff --git a/modules/doc/library/Doc/MarkdownFileIterator.php b/modules/doc/library/Doc/MarkdownFileIterator.php
index 1a8f1c53c..6f317ce6a 100644
--- a/modules/doc/library/Doc/MarkdownFileIterator.php
+++ b/modules/doc/library/Doc/MarkdownFileIterator.php
@@ -4,15 +4,15 @@
 
 namespace Icinga\Module\Doc;
 
-use \RecursiveFilterIterator;
+use RecursiveFilterIterator;
 
 /**
- * Iterator over Markdown files recursively
+ * Recursive iterator over Markdown files
  */
 class MarkdownFileIterator extends RecursiveFilterIterator
 {
     /**
-     * Accept files with .md suffix
+     * Accept files with '.md' suffix
      *
      * @return bool Whether the current element of the iterator is acceptable
      *              through this filter
@@ -20,7 +20,8 @@ class MarkdownFileIterator extends RecursiveFilterIterator
     public function accept()
     {
         $current = $this->getInnerIterator()->current();
-        if (!$current->isFile()) {
+        /* @var $current \SplFileInfo */
+        if (! $current->isFile()) {
             return false;
         }
         $filename = $current->getFilename();
diff --git a/modules/doc/library/Doc/NonEmptyFileIterator.php b/modules/doc/library/Doc/NonEmptyFileIterator.php
new file mode 100644
index 000000000..71bf5acfa
--- /dev/null
+++ b/modules/doc/library/Doc/NonEmptyFileIterator.php
@@ -0,0 +1,31 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}
+
+namespace Icinga\Module\Doc;
+
+use RecursiveFilterIterator;
+
+/**
+ * Recursive iterator over non-empty files
+ */
+class NonEmptyFileIterator extends RecursiveFilterIterator
+{
+    /**
+     * Accept non-empty files
+     *
+     * @return bool Whether the current element of the iterator is acceptable
+     *              through this filter
+     */
+    public function accept()
+    {
+        $current = $this->getInnerIterator()->current();
+        /* @var $current \SplFileInfo */
+        if (! $current->isFile()
+            || $current->getSize() === 0
+        ) {
+            return false;
+        }
+        return true;
+    }
+}
diff --git a/modules/doc/library/Doc/Renderer.php b/modules/doc/library/Doc/Renderer.php
new file mode 100644
index 000000000..0aebb89b9
--- /dev/null
+++ b/modules/doc/library/Doc/Renderer.php
@@ -0,0 +1,75 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc;
+
+use RecursiveIteratorIterator;
+use Zend_View_Helper_Url;
+use Icinga\Web\View;
+
+/**
+ * Base class for toc and section renderer
+ */
+abstract class Renderer extends RecursiveIteratorIterator
+{
+    /**
+     * Encode an anchor identifier
+     *
+     * @param   string $anchor
+     *
+     * @return  string
+     */
+    public static function encodeAnchor($anchor)
+    {
+        return rawurlencode($anchor);
+    }
+
+    /**
+     * Decode an anchor identifier
+     *
+     * @param   string $anchor
+     *
+     * @return  string
+     */
+    public static function decodeAnchor($anchor)
+    {
+        return rawurldecode($anchor);
+    }
+
+    /**
+     * Encode a URL parameter
+     *
+     * @param   string $param
+     *
+     * @return  string
+     */
+    public static function encodeUrlParam($param)
+    {
+        return str_replace(array('%2F','%5C'), array('%252F','%255C'), rawurlencode($param));
+    }
+
+    /**
+     * Decode a URL parameter
+     *
+     * @param   string $param
+     *
+     * @return  string
+     */
+    public static function decodeUrlParam($param)
+    {
+        return str_replace(array('%2F', '%5C'), array('/', '\\'), $param);
+    }
+
+    /**
+     * Render to HTML
+     *
+     * Meant to be overwritten by concrete classes.
+     *
+     * @param   View                    $view
+     * @param   Zend_View_Helper_Url    $zendUrlHelper
+     *
+     * @return  string
+     */
+    abstract public function render(View $view, Zend_View_Helper_Url $zendUrlHelper);
+}
diff --git a/modules/doc/library/Doc/Section.php b/modules/doc/library/Doc/Section.php
new file mode 100644
index 000000000..5cf3d61e2
--- /dev/null
+++ b/modules/doc/library/Doc/Section.php
@@ -0,0 +1,143 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}}
+
+namespace Icinga\Module\Doc;
+
+use Icinga\Data\Identifiable;
+
+/**
+ * A section of a documentation
+ */
+class Section implements Identifiable
+{
+    /**
+     * The ID of the section
+     *
+     * @var string
+     */
+    protected $id;
+
+    /**
+     * The title of the section
+     *
+     * @var string
+     */
+    protected $title;
+
+    /**
+     * The header level
+     *
+     * @var int
+     */
+    protected $level;
+
+    /**
+     * Whether to instruct search engines to not index the link to the section
+     *
+     * @var bool
+     */
+    protected $noFollow;
+
+    /**
+     * The ID of the chapter the section is part of
+     *
+     * @var string
+     */
+    protected $chapterId;
+
+    /**
+     * The content of the section
+     *
+     * @var array
+     */
+    protected $content = array();
+
+    /**
+     * Create a new section
+     *
+     * @param string    $id             The ID of the section
+     * @param string    $title          The title of the section
+     * @param int       $level          The header level
+     * @param bool      $noFollow       Whether to instruct search engines to not index the link to the section
+     * @param string    $chapterId      The ID of the chapter the section is part of
+     */
+    public function __construct($id, $title, $level, $noFollow, $chapterId)
+    {
+        $this->id = $id;
+        $this->title = $title;
+        $this->level = $level;
+        $this->noFollow = $noFollow;
+        $this->chapterId= $chapterId;
+    }
+
+    /**
+     * Get the ID of the section
+     *
+     * @return string
+     */
+    public function getId()
+    {
+        return $this->id;
+    }
+
+    /**
+     * Get the title of the section
+     *
+     * @return string
+     */
+    public function getTitle()
+    {
+        return $this->title;
+    }
+
+    /**
+     * Get the header level
+     *
+     * @return int
+     */
+    public function getLevel()
+    {
+        return $this->level;
+    }
+
+    /**
+     * Whether to instruct search engines to not index the link to the section
+     *
+     * @return bool
+     */
+    public function isNoFollow()
+    {
+        return $this->noFollow;
+    }
+
+    /**
+     * The ID of the chapter the section is part of
+     *
+     * @return string
+     */
+    public function getChapterId()
+    {
+        return $this->chapterId;
+    }
+
+    /**
+     * Append content
+     *
+     * @param string $content
+     */
+    public function appendContent($content)
+    {
+        $this->content[] = $content;
+    }
+
+    /**
+     * Get the content of the section
+     *
+     * @return array
+     */
+    public function getContent()
+    {
+        return $this->content;
+    }
+}
diff --git a/modules/doc/library/Doc/SectionFilterIterator.php b/modules/doc/library/Doc/SectionFilterIterator.php
new file mode 100644
index 000000000..e20d80359
--- /dev/null
+++ b/modules/doc/library/Doc/SectionFilterIterator.php
@@ -0,0 +1,68 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}
+
+namespace Icinga\Module\Doc;
+
+use Countable;
+use RecursiveFilterIterator;
+use Icinga\Data\Tree\NodeInterface;
+
+/**
+ * Recursive iterator over sections that are part of a particular chapter
+ */
+class SectionFilterIterator extends RecursiveFilterIterator implements Countable
+{
+    /**
+     * The chapter ID to filter for
+     *
+     * @var string
+     */
+    protected $chapterId;
+
+    /**
+     * Create a new SectionFilterIterator
+     *
+     * @param NodeInterface $node           Node
+     * @param string        $chapterId      The chapter ID to filter for
+     */
+    public function __construct(NodeInterface $node, $chapterId)
+    {
+        parent::__construct($node);
+        $this->chapterId = $chapterId;
+    }
+
+    /**
+     * Accept sections that are part of the given chapter
+     *
+     * @return bool Whether the current element of the iterator is acceptable
+     *              through this filter
+     */
+    public function accept()
+    {
+        $section = $this->getInnerIterator()->current()->getValue();
+        /* @var $section \Icinga\Module\Doc\Section */
+        if ($section->getChapterId() === $this->chapterId) {
+            return true;
+        }
+        return false;
+    }
+
+    /**
+     * (non-PHPDoc)
+     * @see RecursiveFilterIterator::getChildren()
+     */
+    public function getChildren()
+    {
+        return new static($this->getInnerIterator()->getChildren(), $this->chapterId);
+    }
+
+    /**
+     * (non-PHPDoc)
+     * @see Countable::count()
+     */
+    public function count()
+    {
+        return iterator_count($this);
+    }
+}
diff --git a/modules/doc/library/Doc/SectionRenderer.php b/modules/doc/library/Doc/SectionRenderer.php
new file mode 100644
index 000000000..938e5ed7b
--- /dev/null
+++ b/modules/doc/library/Doc/SectionRenderer.php
@@ -0,0 +1,292 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}
+
+namespace Icinga\Module\Doc;
+
+require_once 'IcingaVendor/Parsedown/Parsedown.php';
+
+use DOMDocument;
+use DOMXPath;
+use RecursiveIteratorIterator;
+use Parsedown;
+use Zend_View_Helper_Url;
+use Icinga\Module\Doc\Exception\ChapterNotFoundException;
+use Icinga\Web\Url;
+use Icinga\Web\View;
+
+/**
+ * preg_replace_callback helper to replace links
+ */
+class Callback
+{
+    protected $docTree;
+
+    protected $view;
+
+    protected $zendUrlHelper;
+
+    protected $url;
+
+    protected $urlParams;
+
+    public function __construct(
+        DocTree $docTree,
+        View $view,
+        Zend_View_Helper_Url $zendUrlHelper,
+        $url,
+        array $urlParams)
+    {
+        $this->docTree = $docTree;
+        $this->view = $view;
+        $this->zendUrlHelper = $zendUrlHelper;
+        $this->url = $url;
+        $this->urlParams = $urlParams;
+    }
+
+    public function render($match)
+    {
+        $node = $this->docTree->getNode(Renderer::decodeAnchor($match['fragment']));
+        /* @var $node \Icinga\Data\Tree\Node */
+        if ($node === null) {
+            return $match[0];
+        }
+        $section = $node->getValue();
+        /* @var $section \Icinga\Module\Doc\Section */
+        $path = $this->zendUrlHelper->url(
+            array_merge(
+                $this->urlParams,
+                array(
+                    'chapterId' => SectionRenderer::encodeUrlParam($section->getChapterId())
+                )
+            ),
+            $this->url,
+            false,
+            false
+        );
+        $url = $this->view->url($path);
+        $url->setAnchor(SectionRenderer::encodeAnchor($section->getId()));
+        return sprintf(
+            '<a %s%shref="%s"',
+            strlen($match['attribs']) ? trim($match['attribs']) . ' ' : '',
+            $section->isNoFollow() ? 'rel="nofollow" ' : '',
+            $url->getAbsoluteUrl()
+        );
+    }
+}
+
+/**
+ * Section renderer
+ */
+class SectionRenderer extends Renderer
+{
+    /**
+     * The documentation tree
+     *
+     * @var DocTree
+     */
+    protected $docTree;
+
+    protected $tocUrl;
+
+    /**
+     * The URL to replace links with
+     *
+     * @var string
+     */
+    protected $url;
+
+    /**
+     * Additional URL parameters
+     *
+     * @var array
+     */
+    protected $urlParams;
+
+    /**
+     * Parsedown instance
+     *
+     * @var Parsedown
+     */
+    protected $parsedown;
+
+    /**
+     * Content
+     *
+     * @var array
+     */
+    protected $content = array();
+
+    /**
+     * Create a new section renderer
+     *
+     * @param   DocTree     $docTree        The documentation tree
+     * @param   string|null $chapterId      If not null, the chapter ID to filter for
+     * @param   string      $tocUrl
+     * @param   string      $url            The URL to replace links with
+     * @param   array       $urlParams      Additional URL parameters
+     *
+     * @throws  ChapterNotFoundException    If the chapter to filter for was not found
+     */
+    public function __construct(DocTree $docTree, $chapterId, $tocUrl, $url, array $urlParams)
+    {
+        if ($chapterId !== null) {
+            $filter = new SectionFilterIterator($docTree, $chapterId);
+            if ($filter->count() === 0) {
+                throw new ChapterNotFoundException(
+                    sprintf(mt('doc', 'Chapter \'%s\' not found'), $chapterId)
+                );
+            }
+            parent::__construct(
+                $filter,
+                RecursiveIteratorIterator::SELF_FIRST
+            );
+        } else {
+            parent::__construct($docTree, RecursiveIteratorIterator::SELF_FIRST);
+        }
+        $this->docTree = $docTree;
+        $this->tocUrl = $tocUrl;
+        $this->url = $url;
+        $this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams);
+        $this->parsedown = Parsedown::instance();
+    }
+
+    /**
+     * Syntax highlighting for PHP code
+     *
+     * @param   $match
+     *
+     * @return  string
+     */
+    protected function highlightPhp($match)
+    {
+        return '<pre>' . highlight_string(htmlspecialchars_decode($match[1]), true) . '</pre>';
+    }
+
+    /**
+     * Replace img src tags
+     *
+     * @param   $match
+     *
+     * @return  string
+     */
+    protected function replaceImg($match)
+    {
+        $doc = new DOMDocument();
+        $doc->loadHTML($match[0]);
+        $xpath = new DOMXPath($doc);
+        $img = $xpath->query('//img[1]')->item(0);
+        /* @var $img \DOMElement */
+        $img->setAttribute('src', Url::fromPath($img->getAttribute('src'))->getAbsoluteUrl());
+        return substr_replace($doc->saveXML($img), '', -2, 1);  // Replace '/>' with '>'
+    }
+
+    /**
+     * Render the section
+     *
+     * @param   View                    $view
+     * @param   Zend_View_Helper_Url    $zendUrlHelper
+     * @param   bool                    $renderNavigation
+     *
+     * @return  string
+     */
+    public function render(View $view, Zend_View_Helper_Url $zendUrlHelper, $renderNavigation = true)
+    {
+        $callback = new Callback($this->docTree, $view, $zendUrlHelper, $this->url, $this->urlParams);
+        $content = array();
+        foreach ($this as $node) {
+            $section = $node->getValue();
+            /* @var $section \Icinga\Module\Doc\Section */
+            $content[] = sprintf(
+                '<a name="%1$s"></a><h%2$d>%3$s</h%2$d>',
+                Renderer::encodeAnchor($section->getId()),
+                $section->getLevel(),
+                $view->escape($section->getTitle())
+            );
+            $html = preg_replace_callback(
+                '#<pre><code class="language-php">(.*?)</code></pre>#s',
+                array($this, 'highlightPhp'),
+                $this->parsedown->text(implode('', $section->getContent()))
+            );
+            $html = preg_replace_callback(
+                '/<img[^>]+>/',
+                array($this, 'replaceImg'),
+                $html
+            );
+            $content[] = preg_replace_callback(
+                '/<a\s+(?P<attribs>[^>]*?\s+)?href="#(?P<fragment>[^"]+)"/',
+                array($callback, 'render'),
+                $html
+            );
+        }
+        if ($renderNavigation) {
+            foreach ($this->docTree as $chapter) {
+                if ($chapter->getValue()->getId() === $section->getChapterId()) {
+                    $navigation = array('<ul class="navigation">');
+                    $this->docTree->prev();
+                    $prev = $this->docTree->current();
+                    if ($prev !== null) {
+                        $prev = $prev->getValue();
+                        $path = $zendUrlHelper->url(
+                            array_merge(
+                                $this->urlParams,
+                                array(
+                                    'chapterId' => $this->encodeUrlParam($prev->getChapterId())
+                                )
+                            ),
+                            $this->url,
+                            false,
+                            false
+                        );
+                        $url = $view->url($path);
+                        $url->setAnchor($this->encodeAnchor($prev->getId()));
+                        $navigation[] = sprintf(
+                            '<li class="prev"><a %shref="%s">%s</a></li>',
+                            $prev->isNoFollow() ? 'rel="nofollow" ' : '',
+                            $url->getAbsoluteUrl(),
+                            $view->escape($prev->getTitle())
+                        );
+                        $this->docTree->next();
+                        $this->docTree->next();
+                    } else {
+                        $this->docTree->rewind();
+                        $this->docTree->next();
+                    }
+                    $url = $view->url($this->tocUrl);
+                    $navigation[] = sprintf(
+                        '<li><a href="%s">%s</a></li>',
+                        $url->getAbsoluteUrl(),
+                        mt('doc', 'Index')
+                    );
+                    $next = $this->docTree->current();
+                    if ($next !== null) {
+                        $next = $next->getValue();
+                        $path = $zendUrlHelper->url(
+                            array_merge(
+                                $this->urlParams,
+                                array(
+                                    'chapterId' => $this->encodeUrlParam($next->getChapterId())
+                                )
+                            ),
+                            $this->url,
+                            false,
+                            false
+                        );
+                        $url = $view->url($path);
+                        $url->setAnchor($this->encodeAnchor($next->getId()));
+                        $navigation[] = sprintf(
+                            '<li class="next"><a %shref="%s">%s</a></li>',
+                            $next->isNoFollow() ? 'rel="nofollow" ' : '',
+                            $url->getAbsoluteUrl(),
+                            $view->escape($next->getTitle())
+                        );
+                    }
+                    $navigation[] = '</ul>';
+                    $content = array_merge($navigation, $content, $navigation);
+                    break;
+                }
+            }
+        }
+        return implode("\n", $content);
+    }
+}
diff --git a/modules/doc/library/Doc/TocRenderer.php b/modules/doc/library/Doc/TocRenderer.php
new file mode 100644
index 000000000..4061e80e3
--- /dev/null
+++ b/modules/doc/library/Doc/TocRenderer.php
@@ -0,0 +1,109 @@
+<?php
+// {{{ICINGA_LICENSE_HEADER}}}
+// {{{ICINGA_LICENSE_HEADER}}
+
+namespace Icinga\Module\Doc;
+
+use RecursiveIteratorIterator;
+use Zend_View_Helper_Url;
+use Icinga\Web\View;
+
+/**
+ * TOC renderer
+ */
+class TocRenderer extends Renderer
+{
+    /**
+     * The URL to replace links with
+     *
+     * @var string
+     */
+    protected $url;
+
+    /**
+     * Additional URL parameters
+     *
+     * @var array
+     */
+    protected $urlParams;
+
+    /**
+     * Content
+     *
+     * @var array
+     */
+    protected $content = array();
+
+    /**
+     * Create a new toc renderer
+     *
+     * @param DocTree   $docTree    The documentation tree
+     * @param string    $url        The URL to replace links with
+     * @param array     $urlParams  Additional URL parameters
+     */
+    public function __construct(DocTree $docTree, $url, array $urlParams)
+    {
+        parent::__construct($docTree, RecursiveIteratorIterator::SELF_FIRST);
+        $this->url = $url;
+        $this->urlParams = array_map(array($this, 'encodeUrlParam'), $urlParams);
+    }
+
+    public function beginIteration()
+    {
+        $this->content[] = '<nav><ul>';
+    }
+
+    public function endIteration()
+    {
+        $this->content[] = '</ul></nav>';
+    }
+
+    public function beginChildren()
+    {
+        $this->content[] = '<ul>';
+    }
+
+    public function endChildren()
+    {
+        $this->content[] = '</ul></li>';
+    }
+
+    /**
+     * Render the toc
+     *
+     * @param   View                    $view
+     * @param   Zend_View_Helper_Url    $zendUrlHelper
+     *
+     * @return  string
+     */
+    public function render(View $view, Zend_View_Helper_Url $zendUrlHelper)
+    {
+        foreach ($this as $node) {
+            $section = $node->getValue();
+            /* @var $section \Icinga\Module\Doc\Section */
+            $path = $zendUrlHelper->url(
+                array_merge(
+                    $this->urlParams,
+                    array(
+                        'chapterId' => $this->encodeUrlParam($section->getChapterId())
+                    )
+                ),
+                $this->url,
+                false,
+                false
+            );
+            $url = $view->url($path);
+            $url->setAnchor($this->encodeAnchor($section->getId()));
+            $this->content[] = sprintf(
+                '<li><a %shref="%s">%s</a>',
+                $section->isNoFollow() ? 'rel="nofollow" ' : '',
+                $url->getAbsoluteUrl(),
+                $view->escape($section->getTitle())
+            );
+            if (! $this->getInnerIterator()->current()->hasChildren()) {
+                $this->content[] = '</li>';
+            }
+        }
+        return implode("\n", $this->content);
+    }
+}
diff --git a/modules/doc/public/css/module.less b/modules/doc/public/css/module.less
new file mode 100644
index 000000000..d6d0d2a94
--- /dev/null
+++ b/modules/doc/public/css/module.less
@@ -0,0 +1,62 @@
+// W3C Recommendation <http://www.w3.org/TR/CSS21/sample.html> (except h4)
+h1 { font-size:    2em !important; }
+h2 { font-size:  1.5em !important; }
+h3 { font-size: 1.17em !important; }
+h4 { font-size:    1em !important; }
+h5 { font-size:  .83em !important; }
+h6 { font-size:  .75em !important; }
+
+div.chapter {
+  padding-left: 5px;
+}
+
+table th {
+  text-align: left;
+}
+
+table th,
+table td {
+  border: solid 1px lightgray;
+  padding-left: 5px;
+  padding-right: 5px;
+}
+
+code {
+  width: 100%;
+  overflow-x: auto;
+  padding: 0.2em;
+  display: inline;
+}
+
+pre > code {
+  display: inline-block;
+}
+
+div.chapter > ul.navigation {
+  margin: 0;
+  padding: 0.4em;
+  text-align: center;
+  background-color: #888;
+
+  li {
+    list-style: none;
+    display: inline;
+    margin: 0.2em;
+    padding: 0;
+
+    a {
+      color: #fff;
+      text-decoration: none;
+    }
+
+    &.prev {
+      padding-right: 0.6em;
+      border-right: 2px solid #fff;
+    }
+
+    &.next {
+      padding-left: 0.6em;
+      border-left: 2px solid #fff;
+    }
+  }
+}
diff --git a/modules/doc/run.php b/modules/doc/run.php
new file mode 100644
index 000000000..7392e4c22
--- /dev/null
+++ b/modules/doc/run.php
@@ -0,0 +1,50 @@
+<?php
+
+use \Zend_Controller_Router_Route;
+use Icinga\Application\Icinga;
+
+if (Icinga::app()->isCli()) {
+    return;
+}
+
+$docModuleChapter = new Zend_Controller_Router_Route(
+    'doc/module/:moduleName/chapter/:chapterId',
+    array(
+        'controller'    => 'module',
+        'action'        => 'chapter',
+        'module'        => 'doc'
+    )
+);
+
+$docIcingaWebChapter = new Zend_Controller_Router_Route(
+    'doc/icingaweb/chapter/:chapterId',
+    array(
+        'controller'    => 'icingaweb',
+        'action'        => 'chapter',
+        'module'        => 'doc'
+    )
+);
+
+$docModuleToc = new Zend_Controller_Router_Route(
+    'doc/module/:moduleName/toc',
+    array(
+        'controller'    => 'module',
+        'action'        => 'toc',
+        'module'        => 'doc'
+    )
+);
+
+$docModulePdf = new Zend_Controller_Router_Route(
+    'doc/module/:moduleName/pdf',
+    array(
+        'controller'    => 'module',
+        'action'        => 'pdf',
+        'module'        => 'doc'
+    )
+);
+
+$this->addRoute('doc/module/chapter', $docModuleChapter);
+$this->addRoute('doc/icingaweb/chapter', $docIcingaWebChapter);
+$this->addRoute('doc/module/toc', $docModuleToc);
+$this->addRoute('doc/module/pdf', $docModulePdf);
+
diff --git a/modules/monitoring/application/controllers/ListController.php b/modules/monitoring/application/controllers/ListController.php
index 6c4cc0101..3e02a4757 100644
--- a/modules/monitoring/application/controllers/ListController.php
+++ b/modules/monitoring/application/controllers/ListController.php
@@ -94,7 +94,6 @@ class Monitoring_ListController extends Controller
             'host_last_check',
             'host_last_state_change' => $stateChangeColumn,
             'host_notifications_enabled',
-            // 'host_unhandled_service_count',
             'host_unhandled_services',
             'host_action_url',
             'host_notes_url',
diff --git a/modules/monitoring/application/controllers/MultiController.php b/modules/monitoring/application/controllers/MultiController.php
index a623dd88b..6459edd59 100644
--- a/modules/monitoring/application/controllers/MultiController.php
+++ b/modules/monitoring/application/controllers/MultiController.php
@@ -24,7 +24,6 @@ class Monitoring_MultiController extends Controller
             array(
                 'host_name',
                 'host_in_downtime',
-                'host_unhandled_service_count',
                 'host_passive_checks_enabled',
                 'host_obsessing',
                 'host_state',
diff --git a/public/img/favicon.png b/public/img/favicon.png
new file mode 100644
index 000000000..6d6f71398
Binary files /dev/null and b/public/img/favicon.png differ
diff --git a/public/js/helpers.js b/public/js/helpers.js
index 082689e40..416bf1185 100644
--- a/public/js/helpers.js
+++ b/public/js/helpers.js
@@ -95,6 +95,13 @@ if (!Function.prototype.bind) {
 
     'use strict';
 
+    /* Whether a HTML tag has a specific attribute */
+    $.fn.hasAttr = function(name) {
+        // We have inconsistent behaviour across browsers (false VS undef)
+        var val = this.attr(name);
+        return typeof val !== 'undefined' && val !== false;
+    };
+
     /* Get class list */
     $.fn.classes = function (callback) {
 
diff --git a/public/js/icinga/events.js b/public/js/icinga/events.js
index 6c6978577..d04912d0f 100644
--- a/public/js/icinga/events.js
+++ b/public/js/icinga/events.js
@@ -474,12 +474,20 @@
                 return true;
             }
 
-            // Ignore clicks on multiselect table inner links while key pressed
-            if ((event.ctrlKey || event.metaKey || event.shiftKey) &&
-                ! $a.is('tr[href]') && $a.closest('table.multiselect').length > 0 &&
-                $a.closest('tr[href]').length > 0)
-            {
-                return self.rowSelected.call($a.closest('tr[href]'), event);
+            // Special checks for link clicks in multiselect rows
+            if (! $a.is('tr[href]') && $a.closest('tr[href]').length > 0 && $a.closest('table.multiselect').length > 0) {
+
+                // Forward clicks to ANY link with special key pressed to rowSelected
+                if (event.ctrlKey || event.metaKey || event.shiftKey)
+                {
+                    return self.rowSelected.call($a.closest('tr[href]'), event);
+                }
+
+                // Forward inner links matching the row URL to rowSelected
+                if ($a.attr('href') === $a.closest('tr[href]').attr('href'))
+                {
+                    return self.rowSelected.call($a.closest('tr[href]'), event);
+                }
             }
 
             // Let remote links pass through
@@ -493,6 +501,12 @@
                 return false;
             }
 
+            // Ignore form elements in action rows
+            if ($(event.target).is('input') || $(event.target).is('button')) {
+                return;
+            }
+
+
             // ignore multiselect table row clicks
             if ($a.is('tr') && $a.closest('table.multiselect').length > 0) {
                 return;
diff --git a/public/js/icinga/history.js b/public/js/icinga/history.js
index dded86379..a3fcf5deb 100644
--- a/public/js/icinga/history.js
+++ b/public/js/icinga/history.js
@@ -162,11 +162,18 @@
 
                 parts = document.location.hash.split(/#!/);
 
-                if ($('#col2').data('icingaUrl') !== main) {
-                    icinga.loader.loadUrl(
-                        parts[1],
-                        $('#col2')
-                    ).historyTriggered = true;
+                if ($('#layout > #login').length) {
+                    // We are on the login page!
+                    $('#login form #redirect').val(
+                        $('#login form #redirect').val() + '#!' + parts[1]
+                    );
+                } else {
+                    if ($('#col2').data('icingaUrl') !== main) {
+                        icinga.loader.loadUrl(
+                            parts[1],
+                            $('#col2')
+                        ).historyTriggered = true;
+                    }
                 }
 
                 // TODO: Replace with dynamic columns
diff --git a/public/js/icinga/loader.js b/public/js/icinga/loader.js
index b871b4362..f91aa20b9 100644
--- a/public/js/icinga/loader.js
+++ b/public/js/icinga/loader.js
@@ -250,14 +250,24 @@
             var icinga = this.icinga;
             var redirect = req.getResponseHeader('X-Icinga-Redirect');
             if (! redirect) return false;
+            redirect = decodeURIComponent(redirect);
+            if (redirect.match(/__SELF__/)) {
+                redirect = redirect.replace(/__SELF__/, encodeURIComponent(document.location.pathname + document.location.search + document.location.hash));
+            }
             icinga.logger.debug(
                 'Got redirect for ', req.$target, ', URL was ' + redirect
             );
-            redirect = decodeURIComponent(redirect);
 
             if (req.getResponseHeader('X-Icinga-Rerender-Layout')) {
+                var parts = redirect.split(/#!/);
+                redirect = parts.shift();
                 var redirectionUrl = this.addUrlFlag(redirect, 'renderLayout');
-                this.loadUrl(redirectionUrl, $('#layout')).url = redirect;
+                var r = this.loadUrl(redirectionUrl, $('#layout'));
+                r.url = redirect;
+                if (parts.length) {
+                    r.loadNext = parts;
+                }
+
             } else {
                 if (req.$target.attr('id') === 'col2') { // TODO: multicol
                     if ($('#col1').data('icingaUrl') === redirect) {
@@ -556,6 +566,17 @@
             req.$target.data('lastUpdate', (new Date()).getTime());
             delete this.requests[req.$target.attr('id')];
             this.icinga.ui.fadeNotificationsAway();
+
+
+            if (typeof req.loadNext !== 'undefined') {
+                if ($('#col2').length) {
+                    this.loadUrl(req.loadNext[0], $('#col2'));
+                    this.icinga.ui.layout2col();
+                } else {
+                  this.icinga.logger.error('Failed to load URL for #col2', req.loadNext);
+                }
+            }
+
             this.icinga.ui.refreshDebug();
         },
 
diff --git a/public/js/icinga/ui.js b/public/js/icinga/ui.js
index e9f3d741c..8ff3733f0 100644
--- a/public/js/icinga/ui.js
+++ b/public/js/icinga/ui.js
@@ -100,7 +100,7 @@
             icinga.logger.info('Reloading CSS');
             $('link').each(function() {
                 var $oldLink = $(this);
-                if ($oldLink.attr('type').indexOf('css') > -1) {
+                if ($oldLink.hasAttr('type') && $oldLink.attr('type').indexOf('css') > -1) {
                     var $newLink = $oldLink.clone().attr(
                         'href',
                         icinga.utils.addUrlParams(
diff --git a/test/php/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorderTest.php b/test/php/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorderTest.php
index d9eb0b25b..a77dcb553 100644
--- a/test/php/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorderTest.php
+++ b/test/php/library/Icinga/Web/Paginator/ScrollingStyle/SlidingWithBorderTest.php
@@ -19,8 +19,8 @@ class SlidingwithborderTest extends BaseTestCase
 
         $pages = $scrollingStyle->getPages($paginator);
         $this->assertInternalType('array', $pages);
-        $this->assertCount(13, $pages);
-        $this->assertEquals('...', $pages[11]);
+        $this->assertCount(10, $pages);
+        $this->assertEquals('...', $pages[8]);
     }
 
     public function testGetPages3()
@@ -31,9 +31,9 @@ class SlidingwithborderTest extends BaseTestCase
 
         $pages = $scrollingStyle->getPages($paginator);
         $this->assertInternalType('array', $pages);
-        $this->assertCount(16, $pages);
+        $this->assertCount(10, $pages);
         $this->assertEquals('...', $pages[3]);
-        $this->assertEquals('...', $pages[14]);
+        $this->assertEquals('...', $pages[12]);
     }
 
     protected function getPaginatorAdapter()
diff --git a/test/php/library/Icinga/Web/UrlTest.php b/test/php/library/Icinga/Web/UrlTest.php
index 0eff54e5c..183f33369 100644
--- a/test/php/library/Icinga/Web/UrlTest.php
+++ b/test/php/library/Icinga/Web/UrlTest.php
@@ -21,7 +21,7 @@ class UrlTest extends BaseTestCase
         $url = Url::fromRequest();
         $this->assertEquals(
             '/path/to/my/test/url.html?param1=value1&amp;param2=value2',
-            $url->getAbsoluteUrl(),
+            $url->getAbsoluteUrl('&amp;'),
             'Url::fromRequest does not reassemble the correct url from the global request'
         );
     }
@@ -119,7 +119,7 @@ class UrlTest extends BaseTestCase
      */
     public function testWhetherFromPathProperlyRecognizesAndDecodesQueryParameters()
     {
-        $url = Url::fromPath('/my/test/url.html?param1=%25arg1&param2=arg+2'
+        $url = Url::fromPath('/my/test/url.html?param1=%25arg1&param2=arg%202'
             . '&param3[]=1&param3[]=2&param3[]=3&param4[key1]=val1&param4[key2]=val2');
 
         $this->assertEquals(