diff --git a/sources/global3.php b/sources/global3.php
index 5df8027..5038a2d 100644
--- a/sources/global3.php
+++ b/sources/global3.php
@@ -582,6 +582,47 @@ function attach_to_screen_footer($data)
     $EXTRA_FOOT->attach($data);
 }
 
+/**
+ * Get header text.
+ *
+ * @return string Header text
+ */
+function get_header_text()
+{
+    global $ZONE, $SHORT_TITLE, $DISPLAYED_TITLE;
+    if ($ZONE === null) {
+        load_zone_data();
+    }
+    if ($SHORT_TITLE === null) { // Take from either zone header or screen title
+        if ($DISPLAYED_TITLE !== null) {
+            $_displayed_title = $DISPLAYED_TITLE->evaluate();
+        }
+        if (($DISPLAYED_TITLE !== null) && (strip_tags($_displayed_title) != '')) {
+            $value = strip_html(preg_replace('#<a[^<>]*>.*</a>#U', '', $_displayed_title)); // The regexp is to remove possible FRACTIONAL_EDIT.tpl link
+        } else {
+            if ($ZONE !== null) {
+                $value = get_translated_text($ZONE['zone_header_text']);
+            }
+        }
+    } else { // Take from short title
+        $comcodeless = strip_comcode($SHORT_TITLE); // This is not HTML
+
+        // Strip 'Welcome to' off if it's there
+        $value = cms_preg_replace_safe('#' . preg_quote(do_lang('WELCOME_TO_STRIPPABLE') . ' ' . get_site_name(), '#') . '([^\-]+\s*-\s*)?#', '', $comcodeless);
+
+        // Strip site name off it it's there (it'll be put on in the templates, so we don't want it twice)
+        $stub = get_site_name() . ' - ';
+        if (substr($value, strlen($stub)) == $stub) {
+            $value = substr($value, strlen($stub));
+        }
+        if ($value == get_site_name()) {
+            $value = '';
+        }
+    }
+    $value = trim($value);
+    return $value;
+}
+
 /**
  * Add some metadata for the request.
  *
diff --git a/sources/symbols.php b/sources/symbols.php
index 591305a..f5ab4a8 100644
--- a/sources/symbols.php
+++ b/sources/symbols.php
@@ -1741,37 +1741,7 @@ function ecv_PAGE($lang, $escaped, $param)
  */
 function ecv_HEADER_TEXT($lang, $escaped, $param)
 {
-    global $ZONE, $SHORT_TITLE, $DISPLAYED_TITLE;
-    if ($ZONE === null) {
-        load_zone_data();
-    }
-    if ($SHORT_TITLE === null) { // Take from either zone header or screen title
-        if ($DISPLAYED_TITLE !== null) {
-            $_displayed_title = $DISPLAYED_TITLE->evaluate();
-        }
-        if (($DISPLAYED_TITLE !== null) && (strip_tags($_displayed_title) != '')) {
-            $value = strip_html(preg_replace('#<a[^<>]*>.*</a>#U', '', $_displayed_title)); // The regexp is to remove possible FRACTIONAL_EDIT.tpl link
-        } else {
-            if ($ZONE !== null) {
-                $value = get_translated_text($ZONE['zone_header_text']);
-            }
-        }
-    } else { // Take from short title
-        $comcodeless = strip_comcode($SHORT_TITLE); // This is not HTML
-
-        // Strip 'Welcome to' off if it's there
-        $value = cms_preg_replace_safe('#' . preg_quote(do_lang('WELCOME_TO_STRIPPABLE') . ' ' . get_site_name(), '#') . '([^\-]+\s*-\s*)?#', '', $comcodeless);
-
-        // Strip site name off it it's there (it'll be put on in the templates, so we don't want it twice)
-        $stub = get_site_name() . ' - ';
-        if (substr($value, strlen($stub)) == $stub) {
-            $value = substr($value, strlen($stub));
-        }
-        if ($value == get_site_name()) {
-            $value = '';
-        }
-    }
-    $value = trim($value);
+    $value = get_header_text();
 
     if ($escaped !== array()) {
         apply_tempcode_escaping($escaped, $value);
@@ -5052,6 +5022,8 @@ function ecv_SMART_LINK_STRIP($lang, $escaped, $param)
                 $value .= '<br /><em>' . do_lang('LINKS_STRIPPED') . '</em>';
             }
         }
+    } elseif (isset($param[0])) {
+        $value = cms_strip_tags($param[0], '<a>', false);
     }
 
     if ($escaped !== array()) {
diff --git a/data_custom/set_raw_page.php b/data_custom/set_raw_page.php
new file mode 100644
index 0000000..894cfbb
--- /dev/null
+++ b/data_custom/set_raw_page.php
@@ -0,0 +1,44 @@
+<?php
+
+// Fixup SCRIPT_FILENAME potentially being missing
+$_SERVER['SCRIPT_FILENAME'] = __FILE__;
+
+// Find Composr base directory, and chdir into it
+global $FILE_BASE, $RELATIVE_PATH;
+$FILE_BASE = (strpos(__FILE__, './') === false) ? __FILE__ : realpath(__FILE__);
+$FILE_BASE = dirname($FILE_BASE);
+if (!is_file($FILE_BASE . '/sources/global.php')) {
+    $RELATIVE_PATH = basename($FILE_BASE);
+    $FILE_BASE = dirname($FILE_BASE);
+} else {
+    $RELATIVE_PATH = '';
+}
+if (!is_file($FILE_BASE . '/sources/global.php')) {
+    $FILE_BASE = $_SERVER['SCRIPT_FILENAME']; // this is with symlinks-unresolved (__FILE__ has them resolved); we need as we may want to allow zones to be symlinked into the base directory without getting path-resolved
+    $FILE_BASE = dirname($FILE_BASE);
+    if (!is_file($FILE_BASE . '/sources/global.php')) {
+        $RELATIVE_PATH = basename($FILE_BASE);
+        $FILE_BASE = dirname($FILE_BASE);
+    } else {
+        $RELATIVE_PATH = '';
+    }
+}
+@chdir($FILE_BASE);
+
+global $FORCE_INVISIBLE_GUEST;
+$FORCE_INVISIBLE_GUEST = false;
+global $EXTERNAL_CALL;
+$EXTERNAL_CALL = false;
+global $CSRF_TOKENS;
+$CSRF_TOKENS = true;
+global $STATIC_CACHE_ENABLED;
+$STATIC_CACHE_ENABLED = true;
+global $IN_SELF_ROUTING_SCRIPT;
+$IN_SELF_ROUTING_SCRIPT = true;
+if (!is_file($FILE_BASE . '/sources/global.php')) {
+    exit('<!DOCTYPE html>' . "\n" . '<html lang="EN"><head><title>Critical startup error</title></head><body><h1>Composr startup error</h1><p>The second most basic Composr startup file, sources/global.php, could not be located. This is almost always due to an incomplete upload of the Composr system, so please check all files are uploaded correctly.</p><p>Once all Composr files are in place, Composr must actually be installed by running the installer. You must be seeing this message either because your system has become corrupt since installation, or because you have uploaded some but not all files from our manual installer package: the quick installer is easier, so you might consider using that instead.</p><p>ocProducts maintains full documentation for all procedures and tools, especially those for installation. These may be found on the <a href="http://compo.sr">Composr website</a>. If you are unable to easily solve this problem, we may be contacted from our website and can help resolve it for you.</p><hr /><p style="font-size: 0.8em">Composr is a website engine created by ocProducts.</p></body></html>');
+}
+require($FILE_BASE . '/sources/global.php');
+
+require_code('sets');
+set_raw_page_script();
diff --git a/themes/default/templates_custom/NEWS_ENTRY_SCREEN.tpl b/themes/default/templates_custom/NEWS_ENTRY_SCREEN.tpl
new file mode 100644
index 0000000..cc8b3a7
--- /dev/null
+++ b/themes/default/templates_custom/NEWS_ENTRY_SCREEN.tpl
@@ -0,0 +1,107 @@
+{+START,INCLUDE,SET_NAV}
+	SET=site:news
+	VALID_NODE_TYPES=news
+	SKIP=
+	PAGE_LINK=site:news:view:{ID}
+{+END}
diff --git a/themes/default/templates_custom/GALLERY_ENTRY_SCREEN.tpl b/themes/default/templates_custom/GALLERY_ENTRY_SCREEN.tpl
new file mode 100644
index 0000000..3c73caa
--- /dev/null
+++ b/themes/default/templates_custom/GALLERY_ENTRY_SCREEN.tpl
@@ -0,0 +1,190 @@
+	{+START,INCLUDE,SET_NAV}
+		SET=site:galleries
+		VALID_NODE_TYPES=image,video
+		SKIP=
+		PAGE_LINK=site:galleries:{MEDIA_TYPE}:{ID}
+	{+END}
diff --git a/themes/default/templates_custom/DOWNLOAD_SCREEN.tpl b/themes/default/templates_custom/DOWNLOAD_SCREEN.tpl
new file mode 100644
index 0000000..dc6a167
--- /dev/null
+++ b/themes/default/templates_custom/DOWNLOAD_SCREEN.tpl
@@ -0,0 +1,227 @@
+{+START,INCLUDE,SET_NAV}
+	SET=site:downloads
+	VALID_NODE_TYPES=download
+	SKIP=
+	PAGE_LINK=site:downloads:entry:{ID}
+{+END}
diff --git a/themes/default/templates_custom/COMCODE_PAGE_SCREEN.tpl b/themes/default/templates_custom/COMCODE_PAGE_SCREEN.tpl
new file mode 100644
index 0000000..5eb9255
--- /dev/null
+++ b/themes/default/templates_custom/COMCODE_PAGE_SCREEN.tpl
@@ -0,0 +1,71 @@
+{+START,IF,{$NOR,{IS_PANEL},{BEING_INCLUDED}}}
+	{+START,INCLUDE,SET_NAV}
+		SET=:
+		VALID_NODE_TYPES=comcode_page,root
+		SKIP=site:,adminzone:
+		PAGE_LINK={$ZONE}:{+START,IF,{$NEQ,{NAME},start}}{NAME}{+END}
+	{+END}
+{+END}
diff --git a/themes/default/templates_custom/CNS_TOPIC_SCREEN.tpl b/themes/default/templates_custom/CNS_TOPIC_SCREEN.tpl
new file mode 100644
index 0000000..b9371f8
--- /dev/null
+++ b/themes/default/templates_custom/CNS_TOPIC_SCREEN.tpl
@@ -0,0 +1,10 @@
+{+START,INCLUDE,SET_NAV}
+	SET=forum:forumview
+	VALID_NODE_TYPES=topic
+	SKIP=
+	PAGE_LINK=forum:topicview:id={ID}
+{+END}
diff --git a/themes/default/templates_custom/CALENDAR_EVENT_SCREEN.tpl b/themes/default/templates_custom/CALENDAR_EVENT_SCREEN.tpl
new file mode 100644
index 0000000..3b01911
--- /dev/null
+++ b/themes/default/templates_custom/CALENDAR_EVENT_SCREEN.tpl
@@ -0,0 +1,10 @@
+{+START,INCLUDE,SET_NAV}
+	SET=site:calendar
+	VALID_NODE_TYPES=event
+	SKIP=
+	PAGE_LINK=site:calendar:view:{ID}
+{+END}
diff --git a/themes/default/javascript_custom/sets.js b/themes/default/javascript_custom/sets.js
new file mode 100644
index 0000000..61a83e6
--- /dev/null
+++ b/themes/default/javascript_custom/sets.js
@@ -0,0 +1,181 @@
+if (typeof window.set_cached_pages == 'undefined') {
+    window.set_cached_pages = {};
+    window.set_nav_locked = false;
+
+    window.addEventListener('popstate', _goto_set_page_history);
+
+    window.addEventListener('DOMContentLoaded', function() {
+        save_set_state(window.location.href);
+
+        // Code for swiping...
+
+        // https://gist.github.com/SleepWalker/da5636b1abcbaff48c4d
+        var pageWidth = window.innerWidth || document.body.clientWidth;
+        var horizontalThreshold = Math.floor(0.3 * pageWidth);
+        var verticalThreshold = 20; // Low, as we want to give up/down precedence to make it not swipe when scrolling
+        var touchstartX = 0;
+        var touchstartY = 0;
+        var touchendX = 0;
+        var touchendY = 0;
+
+        var limit = Math.tan(45 * 1.5 / 180 * Math.PI);
+        var gestureZone = document.getElementById('page_content');
+
+        gestureZone.addEventListener('touchstart', function(event) {
+            touchstartX = event.changedTouches[0].screenX;
+            touchstartY = event.changedTouches[0].screenY;
+        }, false);
+
+        gestureZone.addEventListener('touchend', function(event) {
+            touchendX = event.changedTouches[0].screenX;
+            touchendY = event.changedTouches[0].screenY;
+            handleGesture(event);
+        }, false);
+
+        function handleGesture(e) {
+            var navigate_to = null;
+
+            var x = touchendX - touchstartX;
+            var y = touchendY - touchstartY;
+            var xy = Math.abs(x / y);
+            var yx = Math.abs(y / x);
+            if (Math.abs(y) > verticalThreshold) {
+                // Up/down has precedence, as that's just scrolling the page
+                if (xy <= limit) {
+                    if (y < 0) {
+                        console.log("swipe up");
+                    } else {
+                        console.log("swipe down");
+                    }
+                }
+            } else if (Math.abs(x) > horizontalThreshold) {
+                if (yx <= limit) {
+                    if (x < 0) {
+                        navigate_to = document.getElementById('set_nav_next');
+                        console.log("swipe left");
+                    } else {
+                        navigate_to = document.getElementById('set_nav_prev');
+                        console.log("swipe right");
+                    }
+                }
+            } else {
+                console.log("tap");
+            }
+            if ((navigate_to !== null) && (navigate_to.href)) {
+                try {
+                    goto_set_page(navigate_to.href, true);
+                }
+                catch (e) {}
+            }
+        }
+    });
+}
+
+function goto_set_page(url, swipe)
+{
+    if (window.set_nav_locked) {
+        return;
+    }
+
+    if ((typeof window.set_cached_pages[url] == 'undefined') || (window.set_cached_pages[url] === false)) {
+        window.set_nav_locked = true;
+
+        _load_set_page(url, function(html, title, body_classname) {
+            window.set_nav_locked = false;
+
+            var page_content = document.getElementById('page_content');
+            if (page_content) {
+                set_inner_html(page_content,'<div aria-busy="true"><div class="spaced"><div class="ajax_loading vertical_alignment"><img id="loading_image" src="'+'{$IMG_INLINE*;,loading}'.replace(/^https?:/,window.location.protocol)+'" alt="{!LOADING;^}" /> <span class="vertical_alignment">{!LOADING;^}<\/span><\/div><\/div><\/div>');
+            }
+
+            _goto_set_page(url, html, title, body_classname, swipe);
+            save_set_state(url);
+        });
+        return false;
+    }
+
+    _goto_set_page(url, window.set_cached_pages[url].html, window.set_cached_pages[url].title, window.set_cached_pages[url].body_classname, swipe);
+    save_set_state(url);
+    return false;
+}
+
+function _goto_set_page(url, html, title, body_classname, swipe)
+{
+    if (!swipe) {
+        window.scrollTo(0, 0);
+    }
+
+    var page_content = document.getElementById('page_content');
+
+    set_inner_html(page_content, html);
+    document.title = title;
+    document.body.className = body_classname;
+
+    window.setTimeout(function() {
+        var prev=document.getElementById('set_nav_prev');
+        if (prev) cache_set_page(prev.href);
+
+        var next=document.getElementById('set_nav_next');
+        if (next) cache_set_page(next.href);
+    },0);
+
+    change_menu_highlighting(url);
+}
+
+function change_menu_highlighting()
+{
+    var elements = document.getElementsByClassName('menu');
+    for (var i = 0; i < elements.length; i++) {
+        menu_active_selection(elements[i].id,true);
+    }
+}
+
+function save_set_state(url)
+{
+    var page_content = document.getElementById('page_content');
+
+    var html = get_inner_html(page_content);
+    var title = document.title;
+    var body_classname = document.body.className;
+
+    window.history.pushState({url: url, html: html, title: title, body_classname: body_classname}, title, url);
+}
+
+function _goto_set_page_history(event)
+{
+    if (!event.state) {
+        window.history.back();
+        return;
+    }
+    var url = event.state.url;
+    var html = event.state.html;
+    var title = event.state.title;
+    var body_classname = event.state.body_classname;
+    _goto_set_page(url, html, title, body_classname);
+}
+
+function cache_set_page(url)
+{
+    if (typeof window.set_cached_pages[url] != 'undefined') {
+        // Already cached
+        return;
+    }
+
+    _load_set_page(url);
+}
+
+function _load_set_page(url, then)
+{
+    var _url = '{$FIND_SCRIPT;,set_raw_page}?url=' + window.encodeURIComponent(url);
+
+    do_ajax_request(_url, function(ajax_result) {
+        var html = ajax_result.responseText;
+        var title = ajax_result.getResponseHeader('x-title');
+        var body_classname = ajax_result.getResponseHeader('x-body-classname');
+
+        window.set_cached_pages[url] = {html: html, title: title, body_classname: body_classname};
+        if (then) {
+            then(html, title, body_classname);
+        }
+    });
+}
diff --git a/sources_custom/sets.php b/sources_custom/sets.php
new file mode 100644
index 0000000..6d88cf3
--- /dev/null
+++ b/sources_custom/sets.php
@@ -0,0 +1,98 @@
+<?php
+
+function set_raw_page_script()
+{
+    $url = get_param_string('url', false, true);
+    $page_link = url_to_page_link($url);
+    list($zone, $map) = page_link_decode($page_link);
+
+    require_code('urls2');
+    set_execution_context($map, $zone, 'set_raw_page');
+
+    $page = isset($map['page']) ? $map['page'] : get_zone_default_page($zone);
+
+    process_url_monikers($page);
+
+    prepare_for_known_ajax_response();
+    header('Content-type: text/plain; charset=' . get_charset());
+
+    // Check permissions
+    $zones = $GLOBALS['SITE_DB']->query_select('zones', array('*'), array('zone_name' => $zone), '', 1);
+    if (!array_key_exists(0, $zones)) {
+        warn_exit(do_lang_tempcode('MISSING_RESOURCE'));
+    }
+    if (($zones[0]['zone_name'] != '') && (get_value('windows_auth_is_enabled') !== '1') && ((get_session_id() == '') || (!$GLOBALS['SESSION_CONFIRMED_CACHE'])) && (!is_guest()) && ($zones[0]['zone_require_session'] == 1) && (!is_guest())) {
+        access_denied('ZONE_ACCESS_SESSION');
+    }
+    if (!has_actual_page_access(get_member(), $page, $zone)) {
+        access_denied('ZONE_ACCESS');
+    }
+
+    // Closed site
+    $site_closed = get_option('site_closed');
+    if (($site_closed == '1') && (!has_privilege(get_member(), 'access_closed_site')) && (!$GLOBALS['IS_ACTUALLY_ADMIN'])) {
+        @exit(get_option('closed'));
+    }
+
+    header('x-title: ' . get_header_text() . ' - ' . get_site_name()); // Can't use ndash because the unicode contains an ASCII new line and complains
+    header('x-zone: ' . $zone);
+    header('x-page: ' . $page);
+    header('x-body-classname: website_body zone_running_' . $zone . ' page_running_' . $page);
+
+    safe_ini_set('ocproducts.xss_detect', '0');
+
+    // Load page
+    $output = request_page($page, true, $zone);
+    $output->handle_symbol_preprocessing();
+
+    $out = new Tempcode();
+    $out->attach(symbol_tempcode('CSS_TEMPCODE'));
+    $out->attach(symbol_tempcode('JS_TEMPCODE'));
+    $out->attach($output);
+    $out->attach(symbol_tempcode('JS_TEMPCODE', array('footer')));
+
+    echo $out->evaluate();
+}
+
+function set_navigation($page_link_current, $offset_diff, $type)
+{
+    global $SITEMAP_SET;
+    if (!isset($SITEMAP_SET)) {
+        return 'No set loaded';
+    }
+
+    static $cache = array();
+
+    if ($page_link_current === null) {
+        $page_link_current = url_to_page_link(get_self_url_easy());
+    }
+
+    if (isset($cache[$page_link_current][$offset_diff][$type])) {
+        return $cache[$page_link_current][$offset_diff][$type];
+    }
+
+    if (!isset($SITEMAP_SET['index'][$page_link_current])) {
+        return '';
+    }
+    $current_pos = $SITEMAP_SET['index'][$page_link_current];
+
+    $set = $SITEMAP_SET[$type];
+
+    if (!isset($set[$current_pos + $offset_diff])) {
+        return '';
+    }
+    $_sequential = $set[$current_pos + $offset_diff];
+
+    if ($type == 'page_links') {
+        $sequential = page_link_to_url($_sequential);
+    } else {
+        $sequential = $_sequential;
+        if (($type == 'titles') && (trim($sequential) == '')) {
+            $sequential = '(Unnamed)';
+        }
+    }
+
+    $cache[$page_link_current][$offset_diff][$type] = $sequential;
+
+    return $sequential;
+}
diff --git a/sources_custom/hooks/systems/symbols/SET_LOAD.php b/sources_custom/hooks/systems/symbols/SET_LOAD.php
new file mode 100644
index 0000000..f3df10d
--- /dev/null
+++ b/sources_custom/hooks/systems/symbols/SET_LOAD.php
@@ -0,0 +1,54 @@
+<?php
+
+class Hook_symbol_SET_LOAD
+{
+    public function run($param)
+    {
+        if (!isset($param[0])) {
+            return '';
+        }
+
+        disable_php_memory_limit();
+
+        $page_link = $param[0];
+
+        $valid_node_types = empty($param[1]) ? null : explode(',', $param[1]);
+        $skip = empty($param[2]) ? array() : explode(',', $param[2]);
+
+        global $SITEMAP_SET;
+
+        // Read from cache
+        $set = has_caching_for('block') ? get_cache_entry('set_' . $page_link, serialize($param), CACHE_AGAINST_PERMISSIVE_GROUPS, 60 * 24) : null;
+
+        if ($set === null) {
+            // Not in cache
+            require_code('sitemap');
+            $node = retrieve_sitemap_node($page_link);
+            $page_links = array();
+            $titles = array();
+            $this->get_page_links($node, $skip, $valid_node_types, $page_links, $titles);
+            $SITEMAP_SET = array('index' => array_flip($page_links), 'page_links' => $page_links, 'titles' => $titles);
+
+            // Cache
+            require_code('caches2');
+            put_into_cache('set_' . $page_link, 60 * 24, serialize($param), null, null, permissive_groups_cache_signature(), null, '', $SITEMAP_SET);
+        } else {
+            $SITEMAP_SET = $set;
+        }
+
+        return '';
+    }
+
+    protected function get_page_links($node, $skip, $valid_node_types, &$page_links, &$titles)
+    {
+        if ((!in_array($node['page_link'], $skip)) && (($valid_node_types === null) || (in_array($node['content_type'], $valid_node_types)))) {
+            $page_links[] = $node['page_link'];
+            $titles[] = $node['title'];
+        }
+        if (isset($node['children'])) {
+            foreach ($node['children'] as $subnode) {
+                $this->get_page_links($subnode, $skip, $valid_node_types, $page_links, $titles);
+            }
+        }
+    }
+}
diff --git a/sources_custom/hooks/systems/symbols/SET_NEXT_PAGE_LINK.php b/sources_custom/hooks/systems/symbols/SET_NEXT_PAGE_LINK.php
new file mode 100644
index 0000000..e836c83
--- /dev/null
+++ b/sources_custom/hooks/systems/symbols/SET_NEXT_PAGE_LINK.php
@@ -0,0 +1,12 @@
+<?php
+
+class Hook_symbol_SET_NEXT_PAGE_LINK
+{
+    public function run($param)
+    {
+        $page_link_current = isset($param[0]) ? $param[0] : null;
+
+        require_code('sets');
+        return set_navigation($page_link_current, 1, 'page_links');
+    }
+}
diff --git a/sources_custom/hooks/systems/symbols/SET_NEXT_TITLE.php b/sources_custom/hooks/systems/symbols/SET_NEXT_TITLE.php
new file mode 100644
index 0000000..074350c
--- /dev/null
+++ b/sources_custom/hooks/systems/symbols/SET_NEXT_TITLE.php
@@ -0,0 +1,12 @@
+<?php
+
+class Hook_symbol_SET_NEXT_TITLE
+{
+    public function run($param)
+    {
+        $page_link_current = isset($param[0]) ? $param[0] : null;
+
+        require_code('sets');
+        return set_navigation($page_link_current, 1, 'titles');
+    }
+}
diff --git a/sources_custom/hooks/systems/symbols/SET_PREV_PAGE_LINK.php b/sources_custom/hooks/systems/symbols/SET_PREV_PAGE_LINK.php
new file mode 100644
index 0000000..d5fee56
--- /dev/null
+++ b/sources_custom/hooks/systems/symbols/SET_PREV_PAGE_LINK.php
@@ -0,0 +1,12 @@
+<?php
+
+class Hook_symbol_SET_PREV_PAGE_LINK
+{
+    public function run($param)
+    {
+        $page_link_current = isset($param[0]) ? $param[0] : null;
+
+        require_code('sets');
+        return set_navigation($page_link_current, -1, 'page_links');
+    }
+}
diff --git a/sources_custom/hooks/systems/symbols/SET_PREV_TITLE.php b/sources_custom/hooks/systems/symbols/SET_PREV_TITLE.php
new file mode 100644
index 0000000..79f09f1
--- /dev/null
+++ b/sources_custom/hooks/systems/symbols/SET_PREV_TITLE.php
@@ -0,0 +1,12 @@
+<?php
+
+class Hook_symbol_SET_PREV_TITLE
+{
+    public function run($param)
+    {
+        $page_link_current = isset($param[0]) ? $param[0] : null;
+
+        require_code('sets');
+        return set_navigation($page_link_current, -1, 'titles');
+    }
+}
diff --git a/sources_custom/hooks/systems/decache/sets.php b/sources_custom/hooks/systems/decache/sets.php
new file mode 100644
index 0000000..a26c7aa
--- /dev/null
+++ b/sources_custom/hooks/systems/decache/sets.php
@@ -0,0 +1,30 @@
+<?php
+
+class Hook_decache_sets
+{
+    function decache($cached_for, $identifier)
+    {
+        switch ($cached_for) {
+            case 'main_news':
+                decache('set__site:news');
+                break;
+
+            case 'main_image_slider':
+                decache('set__site:galleries');
+                break;
+
+            case 'side_cns_private_topics':
+                decache('set__forum:topicview');
+                break;
+
+            case 'side_calendar':
+                decache('set__site:calendar');
+                break;
+
+            case 'main_comcode_page_children':
+                decache('set__');
+                break;
+        }
+    }
+}
+
