Open Wagtail Rich Text Block Links in a New Tab

You'd think being able to set external link targets in rich text blocks to open in a new tab would be something offered out of the box. I put this in as a feature request with Wagtail that was promptly shut down with the explanation that "it's against UX best practice" and that is should be up to the visitor to decide if they want to open in a new tab.

Personally, I find it sloppy when a link to an external site isn't opened in a new tab by default. I've lost my place in the site I was on, and now have to navigate back or reload from history. If the opened page is something I want to keep open and save for later while I continue on the original site then the new tab is the only route for this. I'm more likely to just not return back to the referring page.

Much more importantly, leaving your site to visit another is extremely bad practice from a business/marketing/retention viewpoint where the very aim is to retain the visitor, not to send them elsewhere.

Clarification

This applies only to links created using the External Link option in Wagtail's built-in rich text block editor (DraftTail). Links that you create in custom blocks or any other way are entirely up to the developer. Since linking from text is such a common feature, this requires extra consideration.

Workaround

There are two routes that I've come across to circumvent this: adding a site-wide policy via wagtail hooks or dropping in a block of JQuery into the relevant page types.

I'll take the latter first as this is the simplest, less intrusive way.

Opening Links in New Tabs via JQuery

This is a very simple override that amends the attributes of your href a tags if they point to an address using http or https protocols. There are a couple of benefits to using this over a site-wide policy that affects every 'external link':

  1. In a site where there are Django pages alongside Wagtail pages (such as this one), linking to your internal Django pages has to be done using the External Link option since they don't feature in the Page Chooser. These should be opened in the same tab, only opening truly external links in new tabs. The JQuery method allows this provided you use relative paths for internal url's. You may also be using the External Link option to point to aliased domains or routable pages, all of which you'd want in the same tab.
  2. You can apply this method only to the page types you want, rather than the entire site.
  3. It doesn't involve messing with Wagtail internals which can potentially break with future updates.

The code:

$(document).ready(function(){
    $('a[href^="http://"]').attr('target', '_blank');
    $('a[href^="http://"]').attr('rel', 'nofollow noopener');
    $('a[href^="https://"]').attr('target', '_blank');
    $('a[href^="https://"]').attr('rel', 'nofollow noopener');
    });

That's all there is to it. I've dropped this into my site-wide js as I don't have any special requirements. It doesn't mess with the OAuth links and leaves my "internal external" Django links alone as well.

A note about the 'rel' attribute lines

These are optional and included for the following reasons:

noopener - this is included for older browsers where target="_blank" used an opener which was vulnerable to exploit. It's since been shut in all major browsers. See here for a discussion on this.

nofollow - instructs web crawlers to not follow the link. Couple of reasons to do this: it avoids circular references (a link to a page that links back to the current page) which will score negative points on your SEO; avoids giving SEO to your competitors (not sure why you'd want to link to your competitors but it's here for completeness).

 

Opening Links in New Tabs via Wagtail Hooks

Doing this will apply a site-wide policy on all 'External Links', even those that point to internal links.

Starting with Wagtail 2.5 (release date = ???) the following will be possible in your wagtail_hooks.py:

from django.utils.html import escape
from wagtail.core import hooks
from wagtail.core.rich_text import LinkHandler

class NewWindowExternalLinkHandler(LinkHandler):
    # This specifies to do this override for external links only.
    # Other identifiers are available for other types of links.
    identifier = 'external'

    @classmethod
    def expand_db_attributes(cls, attrs):
        href = attrs["href"]
        # Let's add the target attr, and also rel="noopener" + noreferrer fallback.
        # See https://github.com/whatwg/html/issues/4078.
        return '<a href="%s" target="_blank" rel="noopener noreferrer">' % escape(href)

@hooks.register('register_rich_text_features')
def register_external_link(features):
    features.register_link_type(NewWindowExternalLinkHandler)

This is not compatible with 2.14 though (the current version at time of writing).

The unofficial method for pre-2.5 can be:

from draftjs_exporter.dom import DOM
from wagtail.core import hooks
import wagtail.admin.rich_text.editors.draftail.features as draftail_features
from wagtail.admin.rich_text.converters.html_to_contentstate import ExternalLinkElementHandler, PageLinkElementHandler

@hooks.register('register_rich_text_features')
def register_extended_link_feature(features):
    features.default_features.append('extended_link')
    feature_name = 'extended_link'
    type_ = 'LINK'

    control = {
        'type': type_,
        'icon': 'link',
        'description': 'Extended link',
        # We want to enforce constraints on which links can be pasted into rich text.
        # Keep only the attributes Wagtail needs.
        'attributes': ['url', 'id', 'parentId', 'rel', 'target'],
        'whitelist': {
        # Keep pasted links with http/https protocol, and not-pasted links (href = undefined).
        'href': "^(http:|https:|undefined$)",
        }
    }

    features.register_editor_plugin(
        'draftail', feature_name, draftail_features.EntityFeature(control)
    )

    features.register_converter_rule('contentstate', feature_name, {
        'from_database_format': {
            'a[href]': ExternalLinkElementHandler('LINK'),
            'a[linktype="page"]': PageLinkElementHandler('LINK'),
        },
        'to_database_format': {
            'entity_decorators': {'LINK': extended_link_entity}
        }
    })

def extended_link_entity(props):
    id_ = props.get('id')
    link_props = {}

    if id_ is not None:
        link_props['linktype'] = 'page'
        link_props['id'] = id_
    else:
        link_props['href'] = props.get('url')
        # if external link -- add rel="nofollow noopener" and target="_blank"
        link_props['rel'] = 'nofollow noopener'
        link_props['target'] = '_blank'
        
    return DOM.create_element('a', link_props, props['children'])

Now in your text block declaration, remove the 'link' option and add 'extended_link'.

e.g.

body = blocks.RichTextBlock(features=['h1', 'h2', 'h3', 'h4', 'bold', 'italic', 'ol', 'ul', 'hr', 'extended_link', 'image'])),
 


Summary

Well, my vote goes with the JQuery option for the reasons listed above. It's simple, non-intrusive and should it break, your links simply open in the same tab, nothing disastrous. It's the method applied to this website.

If you have any thoughts or questions on this, please feel free to leave them below.

 
Comments
Sign In to leave a comment.

 

Previous Post

Making Wagtail pages more SEO friendly with Wagtail Metadata

Making Wagtail pages more SEO friendly with Wagtail Metadata

Wagtail pages are great for creating a lot of rich content straight out of the box, but for SEO optimization, they need some tweaking.

Here, I subclass the Page model with some help from the wagtail-metadata plug-in.

This subclassed model becomes the base for all site pages and holds all the data for og metadata, twitter cards, page description etc..