diff --git a/sources/hooks/systems/preview/comcode_page.php b/sources/hooks/systems/preview/comcode_page.php
index e6ebe7fc..b5481b17 100644
--- a/sources/hooks/systems/preview/comcode_page.php
+++ b/sources/hooks/systems/preview/comcode_page.php
@@ -104,6 +104,7 @@ class Hook_preview_comcode_page
             'SUBMITTER' => strval(get_member()),
             'TAGS' => '',
             'WARNING_DETAILS' => '',
+            'ADD_DATE_RAW' => strval(time()),
             'EDIT_DATE_RAW' => strval(time()),
             'SHOW_AS_EDIT' => (get_param_integer('show_as_edit', 0) == 1),
             'CONTENT' => $post_html,
diff --git a/sources/site.php b/sources/site.php
index 35ac2f48..5d902ae7 100644
--- a/sources/site.php
+++ b/sources/site.php
@@ -1977,6 +1977,7 @@ function load_comcode_page($string, $zone, $codename, $file_base = null, $being_
         'TAGS' => (get_option('show_content_tagging') == '0') ? /*optimisation, can be intensive with many page includes*/
             new Tempcode() : get_loaded_tags('comcode_pages'),
         'WARNING_DETAILS' => $warning_details,
+        'ADD_DATE_RAW' => strval($comcode_page_row['p_add_date']),
         'EDIT_DATE_RAW' => ($comcode_page_row['p_edit_date'] === null) ? '' : strval($comcode_page_row['p_edit_date']),
         'SHOW_AS_EDIT' => $comcode_page_row['p_show_as_edit'] == 1,
         'CONTENT' => $html,
diff --git a/sources_custom/hooks/systems/symbols/FIRST_IMAGE_EXTRACTOR.php b/sources_custom/hooks/systems/symbols/FIRST_IMAGE_EXTRACTOR.php
new file mode 100644
index 00000000..e4e68de1
--- /dev/null
+++ b/sources_custom/hooks/systems/symbols/FIRST_IMAGE_EXTRACTOR.php
@@ -0,0 +1,10 @@
+<?php
+
+class Hook_symbol_FIRST_IMAGE_EXTRACTOR
+{
+    function run($param)
+    {
+        require_code('images');
+        return first_image_extractor($param);
+    }
+}
diff --git a/sources_custom/images.php b/sources_custom/images.php
new file mode 100644
index 00000000..a0183dfe
--- /dev/null
+++ b/sources_custom/images.php
@@ -0,0 +1,28 @@
+<?php
+
+function first_image_extractor($image_scan_sources)
+{
+    foreach ($image_scan_sources as $x) {
+        if (is_object($x)) {
+            $x = $x->evaluate();
+        }
+
+        if (trim($x) == '') {
+            continue;
+        }
+
+        if (looks_like_url($x)) {
+            return $x;
+        }
+        $matches = array();
+        if (preg_match('#<img[^<>]*\ssrc="([^"]*)"#', $x, $matches) != 0) {
+            $x = $matches[1];
+
+            if (strpos($x, 'emoticon') === false) {
+                return $x;
+            }
+        }
+    }
+
+    return '';
+}
diff --git a/sources_custom/miniblocks/related_content.php b/sources_custom/miniblocks/related_content.php
new file mode 100644
index 00000000..dccbc658
--- /dev/null
+++ b/sources_custom/miniblocks/related_content.php
@@ -0,0 +1,42 @@
+<?php
+
+require_code('related_content');
+
+$content_type = $map['content_type'];
+$id = $map['content_id'];
+
+$include_reverse = !empty($map['include_reverse']);
+
+$allow_fallback = (!isset($map['allow_fallback']) || $map['allow_fallback'] == '1');
+
+$rows = load_related_content($content_type, $id, $include_reverse, true, RELATED_CONTENT__DEFINED);
+
+if ((empty($rows)) && ($allow_fallback)) {
+    // Fallback
+    if (!empty($map['timestamp'])) {
+        $timestamp = intval($map['timestamp']);
+        $rows = load_related_content($content_type, $id, $include_reverse, true, RELATED_CONTENT__LATEST);
+    }
+}
+
+$all_content_types = get_content_type_labels();
+
+$content = array();
+foreach ($rows as $row) {
+    $content[] = array(
+        'URL' => $row['url'],
+        'LABEL' => $row['label'],
+        'CONTENT' => $row['content'],
+        'CONTENT_TYPE' => $row['content_type'],
+        'SUBMITTER' => strval($row['submitter']),
+        'ADD_DATE' => strval($row['add_date']),
+        'CONTENT_TYPE_LABEL' => $all_content_types[$row['content_type']],
+        'REP_IMAGE' => $row['rep_image'],
+        'IMAGE' => $row['image'],
+        'AUTHOR' => $row['author'],
+        '_CATEGORY' => $row['_category'],
+    );
+}
+
+$out = do_template('RELATED_CONTENT', array('CONTENT' => $content));
+$out->evaluate_echo();
diff --git a/sources_custom/related_content.php b/sources_custom/related_content.php
new file mode 100644
index 00000000..8d058bd2
--- /dev/null
+++ b/sources_custom/related_content.php
@@ -0,0 +1,391 @@
+<?php
+
+function init__related_content()
+{
+    define('MAX_WITH_REVERSE_ASSOCIATIONS', 8);
+
+    define('RELATED_CONTENT__DEFINED', 1);
+    define('RELATED_CONTENT__LATEST', 2);
+}
+
+function install_related_content_table()
+{
+    $GLOBALS['SITE_DB']->create_table('related_content', array(
+        'from_content_type' => '*ID_TEXT',
+        'from_content_id' => '*ID_TEXT',
+        'to_content_type' => '*ID_TEXT',
+        'to_content_id' => '*ID_TEXT',
+    ), false, false, true);
+
+    $GLOBALS['SITE_DB']->create_index('related_content', 'search_from', array('from_content_type', 'from_content_id'));
+    $GLOBALS['SITE_DB']->create_index('related_content', 'search_to', array('to_content_type', 'to_content_id'));
+}
+
+function has_related_content_spec_access()
+{
+    if ($GLOBALS['FORUM_DRIVER']->is_staff(get_member())) {
+        return true;
+    }
+
+    return false;
+}
+
+function form_input_related_content($content_type, $id = null)
+{
+    if (!has_related_content_spec_access()) {
+        return new Tempcode();
+    }
+
+    require_code('form_templates');
+
+    if ($id !== null) {
+        $rows = load_related_content($content_type, $id, false, false);
+
+        $content = array();
+        foreach ($rows as $row) {
+            $content[] = array(
+                'CONTENT_TYPE' => $row['content_type'],
+                'CONTENT_ID' => $row['content_id'],
+                'LABEL' => $row['label'],
+            );
+        }
+    } else {
+        $content = array();
+    }
+
+    $input = do_template('RELATED_CONTENT_INPUT', array('RELATED_CONTENT' => $content));
+
+    return _form_input('related_content[]', 'Related content', 'Select other content to relate to this. You can select recent content, or type to do a search and select content from the results.', $input, false);
+}
+
+function get_content_type_labels_plural()
+{
+    return array(
+        'news' => 'News Articles',
+        'comcode_page' => 'Pages',
+    );
+}
+
+function get_content_type_labels()
+{
+    return array(
+        'news' => 'News Article',
+        'comcode_page' => 'Page',
+    );
+}
+
+function related_content_search_script()
+{
+    require_code('character_sets');
+
+    header("Cache-Control: no-cache, must-revalidate"); // HTTP/1.1
+    header("Expires: Mon, 26 Jul 1997 05:00:00 GMT"); // Date in the past
+
+    header('Content-type:  application/json; charset='.get_charset());
+
+    $search = get_param_string('term', '');
+    if ($search == '') {
+        $search = null;
+    }
+
+    $max = 20;
+    $start = 0;
+
+    if (get_param_string('_type') == 'query_append') {
+        $start = (get_param_integer('page') - 1) * $max;
+    }
+
+    $all_content_types = get_content_type_labels_plural();
+
+    $content_groups = array();
+    foreach ($all_content_types as $content_type => $group_label) {
+        if ($search === null) {
+            $group_label = 'Recent ' . $group_label;
+        } else {
+            $group_label = 'Search Results from ' . $group_label;
+        }
+        $results = find_matching_content($content_type, $max, $start, $search, null, null, true, true);
+        if (!empty($results)) {
+            $_results = array();
+            foreach ($results as $i => $result) {
+                $result['text'] = convert_to_internal_encoding($result['text'], get_charset(), 'utf-8');
+                $_results[] = array(
+                    'id' => $result['id'],
+                    'text' => $result['text'],
+                    'image' => $result['image'],
+                );
+            }
+
+            $content_groups[] = array(
+                'text' => $group_label,
+                'children' => $_results,
+            );
+        }
+    }
+
+    echo json_encode($content_groups, defined('JSON_INVALID_UTF8_SUBSTITUTE') ? JSON_INVALID_UTF8_SUBSTITUTE : 0);
+}
+
+function load_related_content($content_type, $id, $include_reverse = false, $consider_validation = true, $mode = 1)
+{
+    $rows = array();
+
+    if (($id === null) || ($mode == RELATED_CONTENT__LATEST)) {
+        $rows_filtered = find_matching_content($content_type, 4, 0, null, null, $id, true, true, true);
+    } else {
+        $_rows = $GLOBALS['SITE_DB']->query_select('related_content', array('to_content_type AS content_type', 'to_content_id AS content_id'), array('from_content_type' => $content_type, 'from_content_id' => $id));
+        foreach ($_rows as $row) {
+            $key = $row['content_type'] . ':' . $row['content_id'];
+            $rows[$key] = $row;
+        }
+
+        if ($include_reverse) {
+            if (count($rows) < MAX_WITH_REVERSE_ASSOCIATIONS) {
+                $max = MAX_WITH_REVERSE_ASSOCIATIONS - count($rows);
+                $_rows = $GLOBALS['SITE_DB']->query_select('related_content', array('from_content_type AS content_type', 'from_content_id AS content_id'), array('to_content_type' => $content_type, 'to_content_id' => $id), '', $max);
+                foreach ($_rows as $row) {
+                    $key = $row['content_type'] . ':' . $row['content_id'];
+                    if (!array_key_exists($key, $rows)) {
+                        $rows[$key] = $row;
+                    }
+                }
+            }
+        }
+
+        $rows_filtered = array();
+
+        foreach ($rows as $row) {
+            $details = find_content_details($row['content_type'], $row['content_id'], $consider_validation, true, true);
+            if ($details !== null) {
+                $row += $details;
+                $rows_filtered[] = $row;
+            }
+        }
+    }
+
+    sort_maps_by($rows_filtered, '!add_date');
+
+    return $rows_filtered;
+}
+
+function delete_related_content($content_type, $id)
+{
+    $GLOBALS['SITE_DB']->query_delete('related_content', array('from_content_type' => $content_type, 'from_content_id' => $id));
+}
+
+function save_related_content($content_type, $id)
+{
+    if (!has_related_content_spec_access()) {
+        return;
+    }
+
+    $to = array();
+    if (isset($_POST['related_content'])) {
+        foreach ($_POST['related_content'] as $r) {
+            if (strpos($r, ':') !== false) {
+                list($to_content_type, $to_content_id) = explode(':', $r, 2);
+                $to[] = array(
+                    'to_content_type' => $to_content_type,
+                    'to_content_id' => $to_content_id,
+                );
+            }
+        }
+    }
+    _save_related_content($content_type, $id, $to);
+}
+
+function _save_related_content($content_type, $id, $to)
+{
+    $GLOBALS['SITE_DB']->query_delete('related_content', array('from_content_type' => $content_type, 'from_content_id' => $id));
+    foreach ($to as $row) {
+        $GLOBALS['SITE_DB']->query_insert('related_content', $row + array('from_content_type' => $content_type, 'from_content_id' => $id));
+    }
+}
+
+function find_content_details($content_type, $id, $consider_validation = true)
+{
+    $results = find_matching_content($content_type, 1, 0, null, $id, null, $consider_validation, true, true);
+    if (!empty($results)) {
+        return $results[0];
+    }
+    return null;
+}
+
+function find_matching_content($content_type, $limit, $start = 0, $search = null, $id_search = null, $id_skip = null, $consider_validation = true, $get_content_field = false, $get_url = false)
+{
+    $db = $GLOBALS['SITE_DB'];
+    $prefix = $db->get_table_prefix();
+    $lang_fields = null;
+    $where = null;
+    $order_by = null;
+
+    $url = null;
+    $content = '';
+
+    switch ($content_type) {
+        case 'news':
+            $select = 'r.id,r.title,r.submitter,r.date_and_time AS add_date,author,news_category AS _category';
+            if ($get_content_field) {
+                $select .= ',r.news_article AS content';
+            }
+            $query = "SELECT " . $select . " FROM {$prefix}news r";
+            if ($consider_validation) {
+                $where = "WHERE validated=1";
+            } else {
+                $where = "WHERE 1=1";
+            }
+            if ($search !== null) {
+                $query .= " JOIN {$prefix}translate t ON t.id=r.title";
+                $where .= " AND (MATCH(t.text_original) AGAINST('" . addslashes($search) . "')";
+                if (preg_match('#^\d+$#', $search) != 0) {
+                    $where .= ' OR r.id=' . $search;
+                }
+                $where .= ')';
+            } else {
+                $order_by = "ORDER BY r.date_and_time DESC";
+            }
+            if ($id_search !== null) {
+                $where .= ' AND r.id=' . strval(intval($id_search));
+            }
+            if ($id_skip !== null) {
+                $where .= ' AND r.id<>' . strval(intval($id_skip));
+            }
+            $lang_fields = array('title' => 'SHORT_TRANS');
+
+            break;
+
+        case 'comcode_page':
+            $select = 'r.the_zone,r.the_page,c.cc_page_title AS title,r.p_submitter AS submitter,r.p_add_date AS add_date';
+            if ($get_content_field) {
+                $select .= ',c.string_index AS content';
+            }
+            $query = "SELECT " . $select . " FROM {$prefix}comcode_pages r LEFT JOIN {$prefix}cached_comcode_pages c ON r.the_zone=c.the_zone AND r.the_page=c.the_page";
+            if ($consider_validation) {
+                $where = "WHERE p_validated=1";
+            } else {
+                $where = "WHERE 1=1";
+            }
+            if ($search !== null) {
+                $query .= " JOIN {$prefix}translate t ON t.id=c.cc_page_title";
+                $where .= " AND (MATCH(t.text_original) AGAINST('" . addslashes($search) . "')";
+                if (preg_match('#^\d+$#', $search) != 0) {
+                    $where .= ' OR ' . db_string_equal_to('r.the_page', $search);
+                }
+                $where .= ')';
+            } else {
+                $order_by = "ORDER BY r.p_add_date DESC";
+            }
+            if ($id_search !== null) {
+                list($id_search_zone, $id_search_page) = explode(':', $id_search, 2);
+                $where .= ' AND ' . db_string_equal_to('r.the_zone', $id_search_zone) . ' AND ' . db_string_equal_to('r.the_page', $id_search_page);
+            }
+            if ($id_skip !== null) {
+                list($id_skip_zone, $id_skip_page) = explode(':', $id_skip, 2);
+                $where .= ' AND ' . db_string_not_equal_to('r.the_zone', $id_skip_zone) . ' AND ' . db_string_not_equal_to('r.the_page', $id_skip_page);
+            }
+            $lang_fields = array('cc_page_title' => 'SHORT_TRANS');
+
+            break;
+
+        default:
+            exit('Error: Unrecognised content type ' . $content_type);
+    }
+
+    if ($where !== null) {
+        $query .= ' ' . $where;
+    }
+    if ($order_by !== null) {
+        $query .= ' ' . $order_by;
+    }
+
+    $raw_results = $db->query($query, $limit, 0, false, true, $lang_fields);
+    $results = array();
+    foreach ($raw_results as $result) {
+        switch ($content_type) {
+            case 'news':
+                $id = strval($result['id']);
+
+                if ($get_url) {
+                    $url = build_url(array('page' => 'news', 'type' => 'view', 'id' => $id), 'site');
+                }
+
+                $label = get_translated_text($result['title'], $db);
+
+                if ($get_content_field) {
+                    $content = get_translated_tempcode('news', $result, 'content', $db);
+                }
+
+                break;
+
+            case 'comcode_page':
+                $id = $result['the_zone'] . ':' . $result['the_page'];
+
+                if ($get_url) {
+                    $url = build_url(array('page' => $result['the_page']), $result['the_zone']);
+                }
+
+                if (isset($result['title'])) {
+                    $label = get_translated_text($result['title'], $db);
+
+                    if ($get_content_field) {
+                        $content = get_translated_tempcode('cached_comcode_pages', $result, 'content', $db);
+                    }
+                } else {
+                    list(, , $path) = find_comcode_page(user_lang(), $result['the_page'], $result['the_zone']);
+                    require_code('zones2');
+                    $label = get_comcode_page_title_from_disk($path);
+
+                    if ($get_content_field) {
+                        push_output_state();
+                        $content = request_page($result['the_zone'], false, $result['the_page'], 'comcode_custom', true);
+                        restore_output_state();
+                    }
+                }
+
+                break;
+        }
+
+        if ((trim($label) != '') || ($id_search !== null)) {
+            $text = $label . ' -- ' . $content_type . ':' . $id;
+            if (isset($result['add_date'])) {
+                $text .= ' -- ' . get_timezoned_date($result['add_date']);
+            }
+
+            $rep_image = '';
+            if (!empty($result['rep_image'])) {
+                $rep_image = $result['rep_image'];
+            }
+            if (!empty($result['rep_image_2'])) {
+                $rep_image = $result['rep_image_2'];
+            }
+            if (($rep_image != '') && (url_is_local($rep_image))) {
+                $rep_image = get_custom_base_url() . '/' . $rep_image;
+            }
+
+            require_code('images');
+            $image_scan_sources = array($rep_image, $content, find_theme_image('no_image'));
+            $image = first_image_extractor($image_scan_sources);
+
+            $author = isset($result['author']) ? $result['author'] : $GLOBALS['FORUM_DRIVER']->get_username($result['submitter']);
+
+            $_category = isset($result['_category']) ? strval($result['_category']) : null;
+
+            $results[] = array(
+                'id' => $content_type . ':' . $id,
+                'text' => $text,
+                'label' => $label,
+                'content_type' => $content_type,
+                'content_id' => $id,
+                'content' => $content,
+                'add_date' => $result['add_date'],
+                'submitter' => $result['submitter'],
+                'url' => $url,
+                'rep_image' => $rep_image,
+                'image' => $image,
+                'author' => $author,
+                '_category' => $_category,
+            );
+        }
+    }
+    return $results;
+}
diff --git a/themes/default/css_custom/forms.css b/themes/default/css_custom/forms.css
index feb95481..e7c089f0 100644
--- a/themes/default/css_custom/forms.css
+++ b/themes/default/css_custom/forms.css
@@ -184,7 +184,7 @@ th.form_table_field_name a, /* Extra specificity to take precedence over th.de_t
 	display: inline;
 }
 .form_table_field_input .wide_field {
-	width: calc(100% - 40px);
+	width: calc(100% - 40px) !important;
 }
 
 /* Tone it all down for forms inside tabs */
diff --git a/themes/default/templates_custom/RELATED_CONTENT_INPUT.tpl b/themes/default/templates_custom/RELATED_CONTENT_INPUT.tpl
new file mode 100644
index 00000000..b1788db4
--- /dev/null
+++ b/themes/default/templates_custom/RELATED_CONTENT_INPUT.tpl
@@ -0,0 +1,88 @@
+{$,Parser hint: .innerHTML okay}
+
+{$REQUIRE_CSS,widget_select2}
+{$REQUIRE_JAVASCRIPT,jquery}
+{$REQUIRE_JAVASCRIPT,select2}
+
+<select multiple="multiple" id="related_content" name="related_content[]" class="input_list wide_field">
+	{+START,LOOP,RELATED_CONTENT}
+		<option selected="selected" value="{CONTENT_TYPE*}:{CONTENT_ID*}">{LABEL*}</option>
+	{+END}
+</select>
+
+<script>// <![CDATA[
+	function escapeHTML(str)
+	{
+		var p = document.createElement("p");
+		p.appendChild(document.createTextNode(str));
+		return p.innerHTML;
+	}
+
+	function getIconFor(id_path, float_dir, image)
+	{
+		var icon = '';
+		/*if (id_path.indexOf('news:') == 0) {
+			icon = '<img class="' + float_dir + ' float_separation" width="24" src="{$IMG*,icons/24x24/menu/rich_content/news}" alt="News article" />';
+		}
+		else if (id_path.indexOf('comcode_page:') == 0) {
+			icon = '<img class="' + float_dir + ' float_separation" width="24" src="{$IMG*,24x24/menu/cms/comcode_page_edit}" alt="Comcode page" />';
+		}*/
+
+		if (id_path.indexOf('news:') == 0) {
+			icon = '<img class="' + float_dir + ' float_separation" width="24" src="' + image + '" alt="News article" />';
+		}
+		else if (id_path.indexOf('comcode_page:') == 0) {
+			icon = '<img class="' + float_dir + ' float_separation" width="24" src="' + image + '" alt="Comcode page" />';
+		}
+
+		return icon;
+	}
+
+	add_event_listener_abstract(window,'load',function() {
+		var $relatedContent = $("#related_content");
+		$relatedContent.select2({
+			ajax: {
+				dropdownAutoWidth: true,
+				containerCssClass: 'wide_field',
+				url: '{$FIND_SCRIPT;/,related_content_search}' + keep_stub(true),
+				dataType: 'json',
+				processResults: function (data) {
+					return {
+						results: data,
+					};
+				}
+			},
+			templateResult: function(state) {
+				var parts = state.text.split(/ -- /, 3);
+				if (parts.length < 3) {
+					return state.text;
+				}
+
+				var icon = getIconFor(parts[1], 'left', state.image);
+
+				return $(
+					'<div title="' + escapeHTML(parts[1]) + '" class="float_surrounder vertical_alignment"> \
+						' + icon + ' \
+						<strong class="left">' + escapeHTML(parts[0]) + '</strong> \
+						<em class="right">' + escapeHTML(parts[2]) + '<\/em> \
+					</div>'
+				);
+			},
+			templateSelection: function(state) {
+				var parts = state.text.split(/ -- /, 3);
+				if (parts.length < 3) {
+					return state.text;
+				}
+
+				var icon = getIconFor(parts[1], 'right', state.image);
+
+				return $(
+					'<span title="' + escapeHTML(parts[1]) + '" class="vertical_alignment"> \
+						' + icon + ' \
+						<span>' + escapeHTML(parts[0]) + '</span> \
+					</span>'
+				);
+			},
+		});
+	});
+//]]></script>
