diff --git a/.gitignore b/.gitignore
index f69db87ad5a5226535559b6965e771d975ded103..a89ffdab632a2cd881f66cb20e1f154c17cc9387 100644
--- a/.gitignore
+++ b/.gitignore
@@ -26,3 +26,4 @@ xerr.log
 conf/ext
 test/ext
 src/ext
+*~
diff --git a/CHANGELOG.md b/CHANGELOG.md
index aaace618e1ba063ee7ddd13148c96da71607f9f3..41f569a3647f8d428c5b95674a4ee9050a8e6317 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -11,12 +11,13 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
 - `ext_applicable` module for building fancy features which append
   functionality to entities (EXPERIMENTAL).
 - `ext_cosmetics` module which converts http(s) uris in property values into
-  clickable links.
+  clickable links (with tests)
 - Added a menu/toc for the tour
 - Added a previous and next buttons for pages in the tour
 - Added warnings to inform about minimum width when accessing tour and
   edit mode on small screens.
 - Added a tutorial for the edit mode to the documentation
+- Documentation on how to customize reference resolving
 
 ### Changed (for changes in existing functionality)
 
@@ -26,6 +27,8 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
   dropped entirely (e.g. "jumbotron"). Please have a look at
     * https://getbootstrap.com/docs/5.0/migration/
     * https://getbootstrap.com/docs/4.6/migration/
+- Moved the resolving of references to Person Records to separate
+  example which can be disabled
 
 ### Deprecated (for soon-to-be removed features)
 
diff --git a/build.properties.d/00_default.properties b/build.properties.d/00_default.properties
index 58d20c521100bbd43e094c2da402abc38ea555bc..386eb8bd22bd3f32b94d9b9ea4714b80e355ea8e 100644
--- a/build.properties.d/00_default.properties
+++ b/build.properties.d/00_default.properties
@@ -42,7 +42,6 @@
 # Modules enabled/disabled by default
 ##############################################################################
 BUILD_MODULE_EXT_PREVIEW=ENABLED
-BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED
 BUILD_MODULE_EXT_SSS_MARKDOWN=DISABLED
 BUILD_MODULE_EXT_TRIGGER_CRAWLER_FORM=DISABLED
 BUILD_MODULE_EXT_AUTOCOMPLETE=ENABLED
@@ -51,10 +50,14 @@ BUILD_MODULE_EXT_BOTTOM_LINE_TABLE_PREVIEW=DISABLED
 BUILD_MODULE_EXT_BOTTOM_LINE_TIFF_PREVIEW=DISABLED
 BUILD_MODULE_EXT_BOOKMARKS=ENABLED
 BUILD_MODULE_EXT_ANNOTATION=ENABLED
+BUILD_MODULE_EXT_COSMETICS_LINKIFY=DISABLED
 
 BUILD_MODULE_USER_MANAGEMENT=ENABLED
 BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM=CaosDB
 
+BUILD_MODULE_EXT_RESOLVE_REFERENCES=ENABLED
+BUILD_EXT_REFERENCES_CUSTOM_RESOLVER=person_reference
+
 ##############################################################################
 # Navbar properties
 ##############################################################################
@@ -148,4 +151,5 @@ MODULE_DEPENDENCIES=(
     ext_sss_markdown.js
     ext_trigger_crawler_form.js
     ext_bookmarks.js
+    ext_cosmetics.js
 )
diff --git a/misc/ext_cosmetics_test_data.py b/misc/ext_cosmetics_test_data.py
new file mode 100755
index 0000000000000000000000000000000000000000..786f46c2d6bcd1d55488a15e2c4f50085f331950
--- /dev/null
+++ b/misc/ext_cosmetics_test_data.py
@@ -0,0 +1,67 @@
+#!/usr/bin/env python3
+# -*- coding: utf-8 -*-
+#
+# ** header v3.0
+# This file is a part of the CaosDB Project.
+#
+# Copyright (C) 2020 IndiScale GmbH <info@indiscale.com>
+# Copyright (C) 2020 Timm Fitschen <t.fitschen@indiscale.com>
+#
+# This program is free software: you can redistribute it and/or modify
+# it under the terms of the GNU Affero General Public License as
+# published by the Free Software Foundation, either version 3 of the
+# License, or (at your option) any later version.
+#
+# This program is distributed in the hope that it will be useful,
+# but WITHOUT ANY WARRANTY; without even the implied warranty of
+# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+# GNU Affero General Public License for more details.
+#
+# You should have received a copy of the GNU Affero General Public License
+# along with this program. If not, see <https://www.gnu.org/licenses/>.
+#
+# ** end header
+
+import caosdb as db
+
+# clean
+old = db.execute_query("FIND Test*")
+if len(old):
+    old.delete()
+
+# data model
+datamodel = db.Container()
+datamodel.extend([
+    db.Property("TestProp", datatype=db.TEXT),
+    db.RecordType("TestRecordType"),
+])
+
+datamodel.insert()
+
+
+# test data
+testdata = db.Container()
+
+test_cases = [
+    "no link",
+    "https://example.com",
+    "https://example.com and http://example.com",
+    "this is text https://example.com",
+    "this is text https://example.com and this as well",
+    "this is text https://example.com and another linke https://example.com",
+    "this is text https://example.com and another linke https://example.com and more text",
+    ("this is a lot of text with links in it Lorem ipsum dolor sit amet, "
+     "consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore "
+     "et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud "
+     "exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. "
+     "Duis aute irure dolor in reprehenderit in voluptate velit esse cillum "
+     "dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non "
+     "proident, sunt in culpa qui officia deserunt mollit anim id est "
+     "laborum.https://example.com and another linke https://example.com and "
+     "more text and here comes a very long link: "
+     "https://example.com/this/has/certainly/more/than/40/characters/just/count/if/you/dont/believe/it.html"),
+]
+for test_case in test_cases:
+    testdata.append(db.Record().add_parent("TestRecordType").add_property("TestProp",
+                                                                          test_case))
+testdata.insert()
diff --git a/src/core/js/ext_cosmetics.js b/src/core/js/ext_cosmetics.js
index 4d935a2a5afecd699fee1a24416c06b30d1adc46..f4f281123b39a87b7ef6848db4e84a81b5e30d9c 100644
--- a/src/core/js/ext_cosmetics.js
+++ b/src/core/js/ext_cosmetics.js
@@ -28,22 +28,33 @@
  */
 var cosmetics = new function () {
 
+    /**
+     * Cut-off length of links. When linkify processes the links any href
+     * longer than this will be cut off at character 25 and "[...]" will be
+     * appended for the link text.
+     */
+    var _link_cut_off_length = 40;
+
     var _linkify = function () {
         $('.caosdb-f-property-text-value').each(function (index) {
-            // TODO also extract and convert links surrounded by other text
-            if (/^https?:\/\//.test(this.innerText)) {
-                var uri = this.innerText;
-                var text = uri
+            if (/https?:\/\//.test(this.innerText)) {
+                var result = this.innerText.replace(/https?:\/\/[^\s]*/g, function (href, index) {
+                    var link_text = href;
+                    if (_link_cut_off_length > 4 && link_text.length > _link_cut_off_length) {
+                        link_text = link_text.substring(0, _link_cut_off_length - 5) + "[...]";
+                    }
+
+                    return `<a title="Open ${href} in a new tab." target="_blank" class="caosdb-v-property-href-value" href="${href}">${link_text} <i class="bi bi-box-arrow-up-right"></i></a>`;
+                });
 
-                $(this).parent().css("overflow", "hidden");
-                $(this).parent().css("text-overflow", "ellipsis");
-                $(this).html(`<a class="caosdb-v-property-href-value" href="${uri}">${text} <i class="bi bi-box-arrow-up-right"></i></a>`);
+                $(this).html(result);
             }
         });
     }
 
     /**
-     * Convert any text-value beginning with 'http(s)://' into a link.
+     * Convert any substring of a text-value beginning with 'http(s)://' into a
+     * link.
      *
      * A listener detects edit-mode changes and previews
      */
@@ -57,6 +68,7 @@ var cosmetics = new function () {
     }
 
     this.init = function () {
+        this.linkify = linkify;
         if ("${BUILD_MODULE_EXT_COSMETICS_LINKIFY}" == "ENABLED") {
             linkify();
         }
diff --git a/src/core/js/ext_references.js b/src/core/js/ext_references.js
index 7cd597e128e8c09da9134f42f542898fb84a4e53..fe4d618c752490400e501116470cce0f28a909ad 100644
--- a/src/core/js/ext_references.js
+++ b/src/core/js/ext_references.js
@@ -43,13 +43,13 @@ var isOutOfViewport = function (elem) {
     out.top = bounding.top < 0;
     out.left = bounding.left < 0;
     out.bottom = bounding.bottom > (window.innerHeight ||
-        document.documentElement.clientHeight);
+  document.documentElement.clientHeight);
     out.right = bounding.right >
-        (window.innerWidth || document.documentElement.clientWidth);
+  (window.innerWidth || document.documentElement.clientWidth);
     out.any =
-        out.top || out.left || out.bottom || out.right;
+  out.top || out.left || out.bottom || out.right;
     out.all = out.top &&
-        out.left && out.bottom && out.right;
+  out.left && out.bottom && out.right;
     return out;
 };
 
@@ -90,56 +90,56 @@ var reference_list_summary = new function () {
      * array.
      */
     this.simplify_integer_numbers = function (array) {
-        logger.trace("enter simplify_integer_numbers", array);
-        var set = Array.from(new Set(array));
-
-        if (set.length === 0) {
-            return ""
-        } else if (set.length === 1) {
-            return `${set[0]}`;
-        }
-
-        // sort numerically
-        set.sort((a, b) => a - b);
-
-        if (set.length === 2) {
-            return `${set[0]}, ${set[1]}`;
-        }
-
-
-        var ret = `${set[0]}`;
-        var last = undefined;
-        // set[0];
-
-        // e.g. [1,2,3,4,5,8,9,10];
-        for (const next of set) {
-            // append '-' to summarize consecutive numbers
-            if (next - last === 1 && !ret.endsWith("-")) {
-                ret += "-";
-            }
-
-            if (next - last > 1) {
-
-                if (ret.endsWith("-")) {
-                    // close previous interval and start new
-                    ret += `${last}, ${next}`;
-                } else {
-                    // no previous interval, start interval.
-                    ret += `, ${next}`;
-                }
-            } else if (next === set[set.length - 1]) {
-                // finish interval if next is last item
-                ret += next;
-                break;
-            }
+  logger.trace("enter simplify_integer_numbers", array);
+  var set = Array.from(new Set(array));
+
+  if (set.length === 0) {
+      return ""
+  } else if (set.length === 1) {
+      return `${set[0]}`;
+  }
+
+  // sort numerically
+  set.sort((a, b) => a - b);
+
+  if (set.length === 2) {
+      return `${set[0]}, ${set[1]}`;
+  }
+
+
+  var ret = `${set[0]}`;
+  var last = undefined;
+  // set[0];
+
+  // e.g. [1,2,3,4,5,8,9,10];
+  for (const next of set) {
+      // append '-' to summarize consecutive numbers
+      if (next - last === 1 && !ret.endsWith("-")) {
+    ret += "-";
+      }
+
+      if (next - last > 1) {
+
+    if (ret.endsWith("-")) {
+        // close previous interval and start new
+        ret += `${last}, ${next}`;
+    } else {
+        // no previous interval, start interval.
+        ret += `, ${next}`;
+    }
+      } else if (next === set[set.length - 1]) {
+    // finish interval if next is last item
+    ret += next;
+    break;
+      }
 
 
-            last = next;
+      last = next;
 
-        }
+  }
 
-        // e.g. "1-5, 8-10"
-        return ret;
+  // e.g. "1-5, 8-10"
+  return ret;
     }
 
     /**
@@ -158,19 +158,19 @@ var reference_list_summary = new function () {
      * @return {HTMLElement|string} generated summary
      */
     this.generate = function (ref_infos, summary_container) {
-        logger.trace("enter generate", ref_infos);
-        if (ref_infos.length > 0 &&
-            typeof ref_infos[0].callback === "function") {
-            const summary =
-                ref_infos[0].callback(ref_infos);
-            if (summary && summary_container) {
-                $(summary_container).append(summary);
-            }
-            logger.trace("leave generate", summary);
-            return summary;
-        }
-        logger.trace("leave generate, return undefined");
-        return undefined;
+  logger.trace("enter generate", ref_infos);
+  if (ref_infos.length > 0 &&
+      typeof ref_infos[0].callback === "function") {
+      const summary =
+    ref_infos[0].callback(ref_infos);
+      if (summary && summary_container) {
+    $(summary_container).append(summary);
+      }
+      logger.trace("leave generate", summary);
+      return summary;
+  }
+  logger.trace("leave generate, return undefined");
+  return undefined;
     }
 }
 
@@ -205,12 +205,12 @@ var resolve_references = new function () {
      * last scroll event.
      */
     var scroll_listener = () => {
-        if (_scroll_timeout) {
-            clearTimeout(_scroll_timeout);
-        }
-        _scroll_timeout = setTimeout(function () {
-            resolve_references.update_visible_references();
-        }, 500);
+  if (_scroll_timeout) {
+      clearTimeout(_scroll_timeout);
+  }
+  _scroll_timeout = setTimeout(function () {
+      resolve_references.update_visible_references();
+  }, 500);
     };
 
 
@@ -220,15 +220,15 @@ var resolve_references = new function () {
      * visible references.
      */
     this.init = function () {
-        if ("${BUILD_MODULE_EXT_RESOLVE_REFERENCES}" === "ENABLED") {
-            scroll_listener();
+  if ("${BUILD_MODULE_EXT_RESOLVE_REFERENCES}" === "ENABLED") {
+      scroll_listener();
 
-            // mainly for vertical scrolling
-            $(window).scroll(scroll_listener);
+      // mainly for vertical scrolling
+      $(window).scroll(scroll_listener);
 
-            // for horizontal scrolling.
-            $(".caosdb-value-list").scroll(scroll_listener);
-        }
+      // for horizontal scrolling.
+      $(".caosdb-value-list").scroll(scroll_listener);
+  }
     }
 
     /**
@@ -241,9 +241,9 @@ var resolve_references = new function () {
      *
      */
     this.is_in_viewport_vertically = function (elem) {
-        var out =
-            isOutOfViewport(elem);
-        return !(out.top || out.bottom);
+  var out =
+      isOutOfViewport(elem);
+  return !(out.top || out.bottom);
     }
 
     /** Check if an element is inside of the viewport on the horizontal axis.
@@ -257,35 +257,19 @@ var resolve_references = new function () {
      *
      */
     this.is_in_viewport_horizontally = function (elem) {
-        var scrollbox = elem.parentElement.parentElement;
-        // Check this condition only if the grand parent is a list and return true
-        // otherwise.
-        if (scrollbox.classList.contains("caosdb-value-list") ==
-            true) {
-            var boundel = elem.getBoundingClientRect();
-            var boundscroll = scrollbox.getBoundingClientRect();
-            var leftcrit = boundel.right > boundscroll.left;
-            var rightcrit = boundel.left < boundscroll.right;
-            return leftcrit && rightcrit;
-        } else {
-            return true;
-        }
-    }
-
-
-    /**
-     * Return the name of a person as firstname + lastname
-     */
-    this.get_person_str = function (el) {
-        var valpr = getProperties(el);
-        if (valpr == undefined) {
-            return;
-        }
-        return valpr.filter(valprel =>
-                valprel.name.toLowerCase() == "firstname")[0].value +
-            " " +
-            valpr.filter(valprel => valprel.name.toLowerCase() ==
-                "lastname")[0].value;
+  var scrollbox = elem.parentElement.parentElement;
+  // Check this condition only if the grand parent is a list and return true
+  // otherwise.
+  if (scrollbox.classList.contains("caosdb-value-list") ==
+      true) {
+      var boundel = elem.getBoundingClientRect();
+      var boundscroll = scrollbox.getBoundingClientRect();
+      var leftcrit = boundel.right > boundscroll.left;
+      var rightcrit = boundel.left < boundscroll.right;
+      return leftcrit && rightcrit;
+  } else {
+      return true;
+  }
     }
 
 
@@ -296,13 +280,13 @@ var resolve_references = new function () {
      * {string} par - parent name.  @return {boolean}
      */
     this.is_child = function (entity, par) {
-        var pars = resolve_references.getParents(entity);
-        for (const thispar of pars) {
-            if (thispar.name === par) {
-                return true;
-            }
-        }
-        return false;
+  var pars = resolve_references.getParents(entity);
+  for (const thispar of pars) {
+      if (thispar.name === par) {
+    return true;
+      }
+  }
+  return false;
     }
 
     /**
@@ -318,46 +302,56 @@ var resolve_references = new function () {
     /**
      * Return a reference_info for an entity.
      *
+     * You may add your own custom resolver by specifying a JS module
+     * via the `BUILD_EXT_REFERENCES_CUSTOM_RESOLVER` build
+     * variable. The custom resolver has to be a JS module (typically
+     * located at caosdb-webui/src/ext/js), the name of which is given
+     * as the value of `BUILD_EXT_REFERENCES_CUSTOM_RESOLVER`. It has
+     * to provide a `resolve` function that takes the entity id to be
+     * resolved as a string and returns a `reference_info` object with
+     * the resolved custom reference as a `text` property.
+     *
+     * See caosdb-webui/src/ext/js/person_reference_resolver.js for an
+     * example.
+     *
      * TODO refactor to be configurable.  @async @param {string} id - the id of
      * the entity which is to be resolved.  @return {reference_info}
      */
     this.resolve_reference = async function (id) {
-        const custom_reference_resolver = window["${BUILD_EXT_REFERENCES_CUSTOM_RESOLVER}"];
-        if (custom_reference_resolver && typeof custom_reference_resolver.resolve === "function") {
-            // try custom_reference_resolver and fall-back to standard implementation
-            var ret = await custom_reference_resolver.resolve(id);
-            if (ret) {
-              return ret;
-            }
-        }
-
-        const entity = (await resolve_references.retrieve(id))[0];
-
-        // TODO handle multiple parents
-        const par = resolve_references.getParents(entity)[0] || {};
-
-        var ret = {
-            "text": id
-        };
-        if (getEntityHeadingAttribute(entity, "path") !==
-            undefined || par.name == "Image") {
-            // show file name
-            var pths = getEntityHeadingAttribute(entity, "path")
-                .split("/");
-            ret["text"] = pths[pths.length - 1];
-        } else if (par.name === "Person") {
-            ret["text"] = this.get_person_str(entity);
-        } else if (par.name === "TestReferenced" && typeof resolve_references.test_resolver === "function") {
-            // this is a test case, initialized by the test suite.
-            ret = resolve_references.test_resolver(entity);
-        } else {
-            var name = getEntityName(entity);
-            if (typeof name !== "undefined" && name.length > 0) {
-                ret["text"] = name;
-            }
-        }
-
+  const custom_reference_resolver = window["${BUILD_EXT_REFERENCES_CUSTOM_RESOLVER}"];
+  if (custom_reference_resolver && typeof custom_reference_resolver.resolve === "function") {
+      // try custom_reference_resolver and fall-back to standard implementation
+      var ret = await custom_reference_resolver.resolve(id);
+      if (ret) {
         return ret;
+      }
+  }
+
+  const entity = (await resolve_references.retrieve(id))[0];
+
+  // TODO handle multiple parents
+  const par = resolve_references.getParents(entity)[0] || {};
+
+  var ret = {
+      "text": id
+  };
+  if (getEntityHeadingAttribute(entity, "path") !==
+      undefined || par.name == "Image") {
+      // show file name
+      var pths = getEntityHeadingAttribute(entity, "path")
+    .split("/");
+      ret["text"] = pths[pths.length - 1];
+  } else if (par.name === "TestReferenced" && typeof resolve_references.test_resolver === "function") {
+      // this is a test case, initialized by the test suite.
+      ret = resolve_references.test_resolver(entity);
+  } else {
+      var name = getEntityName(entity);
+      if (typeof name !== "undefined" && name.length > 0) {
+    ret["text"] = name;
+      }
+  }
+
+  return ret;
     }
 
 
@@ -372,12 +366,12 @@ var resolve_references = new function () {
      * @return {HTMLElement} the new/existing target element.
      */
     this.add_target = function (element) {
-        if(element.getElementsByClassName(this._target_class).length > 0){
-            return element.getElementsByClassName(this._target_class);
-        } else {
-            return $(`<span class="${this._target_class}"/>`)
-                .appendTo(element)[0];
-        }
+  if(element.getElementsByClassName(this._target_class).length > 0){
+      return element.getElementsByClassName(this._target_class);
+  } else {
+      return $(`<span class="${this._target_class}"/>`)
+    .appendTo(element)[0];
+  }
     }
 
     /**
@@ -388,14 +382,14 @@ var resolve_references = new function () {
      * @return {reference_info} the resolved reference information
      */
     this.update_single_resolvable_reference = async function (rs) {
-        $(rs).find(".caosdb-id-button").hide();
-        const target = resolve_references.add_target(rs);
-        const id = getEntityID(rs);
-        target.textContent = id;
-        const resolved_entity_info = (
-            await resolve_references.resolve_reference(id));
-        target.textContent = resolved_entity_info.text;
-        return resolved_entity_info;
+  $(rs).find(".caosdb-id-button").hide();
+  const target = resolve_references.add_target(rs);
+  const id = getEntityID(rs);
+  target.textContent = id;
+  const resolved_entity_info = (
+      await resolve_references.resolve_reference(id));
+  target.textContent = resolved_entity_info.text;
+  return resolved_entity_info;
     }
 
 
@@ -411,10 +405,10 @@ var resolve_references = new function () {
      * @return {HTMLElement} a summary field.
      */
     this.add_summary_field = function (list_values) {
-        const summary = $(
-            `<div class="${resolve_references._summary_class}"/>`);
-        $(list_values).prepend(summary);
-        return summary[0];
+  const summary = $(
+      `<div class="${resolve_references._summary_class}"/>`);
+  $(list_values).prepend(summary);
+  return summary[0];
     }
 
     this._summary_class = "caosdb-resolve-reference-summary";
@@ -426,9 +420,9 @@ var resolve_references = new function () {
     this._unresolved_class_name = "caosdb-resolvable-reference";
 
     this.get_resolvable_properties = function (container) {
-        const _unresolved_class_name = this._unresolved_class_name;
-        return $(container).find(".caosdb-f-property-value").has(
-            `.${_unresolved_class_name}`).toArray();
+  const _unresolved_class_name = this._unresolved_class_name;
+  return $(container).find(".caosdb-f-property-value").has(
+      `.${_unresolved_class_name}`).toArray();
     }
 
 
@@ -442,115 +436,115 @@ var resolve_references = new function () {
      * @param {HTMLElement} container
      */
     this.update_visible_references = async function (container) {
-        const property_values = resolve_references
-            .get_resolvable_properties(container || document.body);
-
-        const _unresolved_class_name = resolve_references
-            ._unresolved_class_name;
-
-        // Loop over all property values in the container element. Note: each property_value can be a single reference or a list of references.
-        for (const property_value of property_values) {
-            var lists = findElementByConditions(
-                property_value, 
-                x => x.classList.contains("caosdb-value-list"), 
-                x => x.classList.contains("caosdb-preview-container"))
-            lists = $(lists).has(`.${_unresolved_class_name}`);
-
-            if (lists.length > 0) {
-                logger.debug("processing list of references", lists);
-
-                for (var i = 0; i < lists.length; i++) {
-                    const list = lists[i];
-                    if (resolve_references
-                            .is_in_viewport_vertically(list)) {
-                        const rs = $(list).find(
-                                `.${_unresolved_class_name}`)
-                            .toggleClass(_unresolved_class_name, false);
-
-                        // First resolve only one reference. If the `ref_info`
-                        // indicates that a summary is to be generated from the
-                        // list of references, retrieve all other other
-                        // references. Otherwise retrieve only those which are
-                        // visible in the viewport horizontally and trigger the
-                        // retrieval of the others when they are scrolled into
-                        // the view port.
-                        const first_ref_info = await resolve_references
-                            .update_single_resolvable_reference(rs[0]);
-
-                        first_ref_info["index"] = 0;
-
-                        if (typeof first_ref_info.callback === "function") {
-                            // there is a callback function, hence we need to
-                            // generate a summary.
-                            logger.debug("loading all references for summary",
-                                rs);
-                            const summary_field = resolve_references
-                                .add_summary_field(property_value);
-
-                            // collect ref infos for the summary
-                            const ref_infos = [first_ref_info];
-                            for (var j = 1; j < rs.length; j++) {
-                                const ref_info = resolve_references
-                                    .update_single_resolvable_reference(rs[j]);
-                                ref_info["index"] = j;
-                                ref_infos.push(ref_info);
-                            }
-
-                            // wait for resolution of references,
-                            // then generate the summary,
-                            // dispatch event when ready.
-                            Promise.all(ref_infos)
-                                .then(_ref_infos => {reference_list_summary
-                                    .generate(_ref_infos, summary_field);})
-                                .then(() => {
-                                    summary_field.dispatchEvent(
-                                        resolve_references
-                                            .summary_ready_event
-                                    );})
-                                .catch((err) => {
-                                    logger.error(err);
-                                })
-
-                        } else {
-                            // no summary to be generated
-
-                            logger.debug("lazy loading references", rs);
-                            for (var j = 1; j < rs.length; j++) {
-                                // mark others to be loaded later and only if
-                                // visible
-                                $(rs[j]).toggleClass(_unresolved_class_name, true);
-                            }
-                        }
-                    }
-                }
-            }
-
-            // Load all remaining references. These are single reference values
-            // and those references from lists which are left for lazy loading.
-            const rs = findElementByConditions(
-                property_value, 
-                x => x.classList.contains(`${_unresolved_class_name}`), 
-                x => x.classList.contains("caosdb-preview-container"));
-            for (var i = 0; i < rs.length; i++) {
-                if (resolve_references.is_in_viewport_vertically(
-                        rs[i]) &&
-                    resolve_references.is_in_viewport_horizontally(
-                        rs[i])) {
-                    logger.debug("processing single references", rs);
-                    $(rs[i]).toggleClass(_unresolved_class_name, false);
-
-                    // discard return value as it is not needed for any summary
-                    // generation as above.
-                    resolve_references.update_single_resolvable_reference(rs[i]);
-                }
-            }
+  const property_values = resolve_references
+      .get_resolvable_properties(container || document.body);
+
+  const _unresolved_class_name = resolve_references
+      ._unresolved_class_name;
+
+  // Loop over all property values in the container element. Note: each property_value can be a single reference or a list of references.
+  for (const property_value of property_values) {
+      var lists = findElementByConditions(
+    property_value,
+    x => x.classList.contains("caosdb-value-list"),
+    x => x.classList.contains("caosdb-preview-container"))
+      lists = $(lists).has(`.${_unresolved_class_name}`);
+
+      if (lists.length > 0) {
+    logger.debug("processing list of references", lists);
+
+    for (var i = 0; i < lists.length; i++) {
+        const list = lists[i];
+        if (resolve_references
+          .is_in_viewport_vertically(list)) {
+      const rs = $(list).find(
+        `.${_unresolved_class_name}`)
+          .toggleClass(_unresolved_class_name, false);
+
+      // First resolve only one reference. If the `ref_info`
+      // indicates that a summary is to be generated from the
+      // list of references, retrieve all other other
+      // references. Otherwise retrieve only those which are
+      // visible in the viewport horizontally and trigger the
+      // retrieval of the others when they are scrolled into
+      // the view port.
+      const first_ref_info = await resolve_references
+          .update_single_resolvable_reference(rs[0]);
+
+      first_ref_info["index"] = 0;
+
+      if (typeof first_ref_info.callback === "function") {
+          // there is a callback function, hence we need to
+          // generate a summary.
+          logger.debug("loading all references for summary",
+        rs);
+          const summary_field = resolve_references
+        .add_summary_field(property_value);
+
+          // collect ref infos for the summary
+          const ref_infos = [first_ref_info];
+          for (var j = 1; j < rs.length; j++) {
+        const ref_info = resolve_references
+            .update_single_resolvable_reference(rs[j]);
+        ref_info["index"] = j;
+        ref_infos.push(ref_info);
+          }
+
+          // wait for resolution of references,
+          // then generate the summary,
+          // dispatch event when ready.
+          Promise.all(ref_infos)
+        .then(_ref_infos => {reference_list_summary
+            .generate(_ref_infos, summary_field);})
+        .then(() => {
+            summary_field.dispatchEvent(
+          resolve_references
+              .summary_ready_event
+            );})
+        .catch((err) => {
+            logger.error(err);
+        })
+
+      } else {
+          // no summary to be generated
+
+          logger.debug("lazy loading references", rs);
+          for (var j = 1; j < rs.length; j++) {
+        // mark others to be loaded later and only if
+        // visible
+        $(rs[j]).toggleClass(_unresolved_class_name, true);
+          }
+      }
         }
     }
+      }
+
+      // Load all remaining references. These are single reference values
+      // and those references from lists which are left for lazy loading.
+      const rs = findElementByConditions(
+    property_value,
+    x => x.classList.contains(`${_unresolved_class_name}`),
+    x => x.classList.contains("caosdb-preview-container"));
+      for (var i = 0; i < rs.length; i++) {
+    if (resolve_references.is_in_viewport_vertically(
+      rs[i]) &&
+        resolve_references.is_in_viewport_horizontally(
+      rs[i])) {
+        logger.debug("processing single references", rs);
+        $(rs[i]).toggleClass(_unresolved_class_name, false);
+
+        // discard return value as it is not needed for any summary
+        // generation as above.
+        resolve_references.update_single_resolvable_reference(rs[i]);
+    }
+      }
+  }
+    }
 }
 
 
 $(document).ready(function () {
     if ("${BUILD_MODULE_EXT_RESOLVE_REFERENCES}" === "ENABLED") {
-        caosdb_modules.register(resolve_references);
+  caosdb_modules.register(resolve_references);
     }
 });
diff --git a/src/core/xsl/navbar.xsl b/src/core/xsl/navbar.xsl
index ee4df81be60558e6b6aa2e558096d7420636349f..529945d7c37a95a0687473dce34f0269e2942c92 100644
--- a/src/core/xsl/navbar.xsl
+++ b/src/core/xsl/navbar.xsl
@@ -135,7 +135,7 @@
                 <span id="caosdb-f-bookmarks-collection-counter" class="badge bg-secondary">0</span>
                   Bookmarks
                 </a>
-              <ul class="dropdown-menu dropdown-menu-light" aria-labelledby="navbarBookmarkMenuLink">
+              <ul class="dropdown-menu dropdown-menu-end dropdown-menu-light" aria-labelledby="navbarBookmarkMenuLink">
                 <li class="disabled" id="caosdb-f-bookmarks-collection-link"
                     title="Show all bookmarked entities.">
                   <a class="dropdown-item">Show all</a></li>
@@ -234,10 +234,10 @@
             <i class="bi-person-fill"></i>
             <span class="caret"></span>
           </a>
-          <ul class="dropdown-menu dropdown-menu-light">
+          <ul class="dropdown-menu dropdown-menu-end dropdown-menu-light">
             <xsl:if test="/Response/@realm='${BUILD_MODULE_USER_MANAGEMENT_CHANGE_OWN_PASSWORD_REALM}'">
               <li>
-                <a title="Change your password." href="#" data-toggle="modal" data-target="#caosdb-f-change-password-form">Change Password</a>
+                <a class="dropdown-item" title="Change your password." href="#" data-bs-toggle="modal" data-bs-target="#caosdb-f-change-password-form">Change Password</a>
               </li>
             </xsl:if>
             <li>
diff --git a/src/doc/extension/references.rst b/src/doc/extension/references.rst
new file mode 100644
index 0000000000000000000000000000000000000000..63c551612e5e9d807846595b6c5e458bc5096615
--- /dev/null
+++ b/src/doc/extension/references.rst
@@ -0,0 +1,38 @@
+Customizing the display of referenced entities
+=============================================
+
+CaosDB WebUI supports the customized display of referenced entities
+using the :doc:`ext_references <../api/module-resolve_references>`
+module. The ``BUILD_MODULE_EXT_RESOLVE_REFERENCES`` build variable has
+to be set to ``ENABLED`` (see :doc:`/getting_started`) in order to use
+this module.
+
+You may then define your own JavaScript module to define how
+references to specific Records should be resolved. The module has to
+be located at a directory which is known to CaosDB WebUI; we recommend
+``caosdb-webui/src/ext/js``. Set the value of the
+``BUILD_EXT_REFERENCES_CUSTOM_RESOLVER`` build variable to the name of
+this module. The module has to have a ``resolve`` function which takes
+an entity id as its only parameter and returns a ``reference_info``
+object with the resolved custom reference as a ``text`` property. So
+the basic structure of the module should look like
+
+.. code-block:: javascript
+
+   var my_reference_resolver = new function () {
+       // Has to be called ``resolve`` and has to take exactly one
+       // string parameter: the id of the referenced entity.
+       this.resolve = async function (id) {
+	   /*
+	    * find the string that the reference should be resolved to,
+	    * e.g., from the value of the entity's properties.
+	    */
+	   return {"text": new_reference_text}
+       }
+   }
+
+An example is located in
+``caosdb-webui/src/ext/js/person_reference_resolver.js``. It resolves
+any reference to a ``Person`` Record to the value of its ``firstname``
+and ``lastname`` properties separated by a space and is active by
+default.
diff --git a/src/ext/js/person_reference_resover.js b/src/ext/js/person_reference_resover.js
new file mode 100644
index 0000000000000000000000000000000000000000..393557354904787f04472585bca0883d64200d86
--- /dev/null
+++ b/src/ext/js/person_reference_resover.js
@@ -0,0 +1,65 @@
+/*
+ * ** header v3.0
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH (info@indiscale.com)
+ * Copyright (C) 2021 Florian Spreckelsen (f.spreckelsen@indiscale.com)
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ *
+ * ** end header
+ */
+
+/**
+ * @module person_reference
+ *
+ * Replace the reference to a Person Record by the values of that
+ * Record's firstname and lastname properties.
+ *
+ * TODO: Make name(s) of person RecordType(s) and names of firstname
+ * and lastname properties configurable.
+ */
+var person_reference = new function () {
+
+    var logger = log.getLogger("person_reference");
+
+    const lastname_prop_name = "lastname"
+    const firstname_prop_name = "firstname"
+    const person_rt_name = "Person"
+
+    /**
+     * Return the name of a person as firstname + lastname
+     */
+    this.get_person_str = function (el) {
+  var valpr = getProperties(el);
+  if (valpr == undefined) {
+      return;
+  }
+  return valpr.filter(valprel =>
+      valprel.name.toLowerCase().trim() ==
+    firstname_prop_name.toLowerCase())[0].value +
+      " " +
+      valpr.filter(valprel => valprel.name.toLowerCase().trim() ==
+       lastname_prop_name.toLowerCase())[0].value;
+    }
+
+    this.resolve = async function (id) {
+
+  const entity = (await resolve_references.retrieve(id))[0];
+
+  if (resolve_references.is_child(entity, person_rt_name)) {
+      return {"text": person_reference.get_person_str(entity)};
+  }
+    }
+}
diff --git a/test/core/js/modules/ext_cosmetics.js.js b/test/core/js/modules/ext_cosmetics.js.js
new file mode 100644
index 0000000000000000000000000000000000000000..d5d4df7f10a2859bcd7318680d4f6720aedc6127
--- /dev/null
+++ b/test/core/js/modules/ext_cosmetics.js.js
@@ -0,0 +1,87 @@
+/*
+ * This file is a part of the CaosDB Project.
+ *
+ * Copyright (C) 2021 IndiScale GmbH <info@indiscale.com>
+ * Copyright (C) 2021 Timm Fitschen <t.fitschen@indiscale.com>
+ *
+ * This program is free software: you can redistribute it and/or modify
+ * it under the terms of the GNU Affero General Public License as
+ * published by the Free Software Foundation, either version 3 of the
+ * License, or (at your option) any later version.
+ *
+ * This program is distributed in the hope that it will be useful,
+ * but WITHOUT ANY WARRANTY; without even the implied warranty of
+ * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
+ * GNU Affero General Public License for more details.
+ *
+ * You should have received a copy of the GNU Affero General Public License
+ * along with this program. If not, see <https://www.gnu.org/licenses/>.
+ */
+
+'use strict';
+
+QUnit.module("ext_cosmetics.js", {
+    before: function (assert) {
+        cosmetics.init();
+        // setup before module
+    },
+    beforeEach: function (assert) {
+        // setup before each test
+    },
+    afterEach: function (assert) {
+        // teardown after each test
+    },
+    after: function (assert) {
+        // teardown after module
+    }
+});
+
+QUnit.test("linkify - https", function (assert) {
+    assert.ok(cosmetics.linkify, "linkify available");
+    var test_cases = [
+        ["https://link", 1],
+        ["this is other text https://link", 1],
+        ["https://link this is other text", 1],
+        ["this is other text https://link and this as well", 1],
+        ["this is other text https://link", 1],
+        ["this is other text https://link and here comes another link https://link and more text", 2],
+    ];
+    for (let test_case of test_cases) {
+        var text_value = $(`<div class="caosdb-f-property-text-value">${test_case[0]}</div>`);
+        $(document.body).append(text_value);
+        assert.equal($(text_value).find("a[href='https://link']").length, 0, "no link present");
+        cosmetics.linkify();
+        assert.equal($(text_value).find("a[href='https://link']").length, test_case[1], "link is present");
+        text_value.remove();
+    }
+});
+
+QUnit.test("linkify - http", function (assert) {
+    var test_cases = [
+        ["http://link", 1],
+        ["this is other text http://link", 1],
+        ["http://link this is other text", 1],
+        ["this is other text http://link and this as well", 1],
+        ["this is other text http://link", 1],
+        ["this is other text http://link and here comes another link http://link and more text", 2],
+    ];
+    for (let test_case of test_cases) {
+        var text_value = $(`<div class="caosdb-f-property-text-value">${test_case[0]}</div>`);
+        $(document.body).append(text_value);
+        assert.equal($(text_value).find("a[href='http://link']").length, 0, "no link present");
+        cosmetics.linkify();
+        assert.equal($(text_value).find("a[href='http://link']").length, test_case[1], "link is present");
+        text_value.remove();
+    }
+});
+
+QUnit.test("linkify cut-off (40)", function (assert) {
+    var test_case = "here is some text https://this.is.a.link/with/more/than/40/characters/ this is more text";
+    var text_value = $(`<div class="caosdb-f-property-text-value">${test_case}</div>`);
+    $(document.body).append(text_value);
+    assert.equal($(text_value).find("a").length, 0, "no link present");
+    cosmetics.linkify();
+    assert.equal($(text_value).find("a").length, 1, "link is present");
+    assert.equal($(text_value).find("a").text(), "https://this.is.a.link/with/more/th[...] ", "link text has been cut off");
+    text_value.remove();
+});
\ No newline at end of file
diff --git a/test/core/js/modules/ext_references.js.js b/test/core/js/modules/ext_references.js.js
index 43cc1ddd742d6702232b740bbfd96411f41b08f5..54e06d33d5f1c33781efe11802a7fbfc5ba44d89 100644
--- a/test/core/js/modules/ext_references.js.js
+++ b/test/core/js/modules/ext_references.js.js
@@ -104,7 +104,7 @@ QUnit.test("is_child", function(assert){
 });
 
 QUnit.test("get_person_str", function(assert){
-    assert.ok(resolve_references.get_person_str);
+    assert.ok(person_reference.get_person_str);
 });
 
 QUnit.test("update_visible_references_without_summary", async function(assert){