Kirby CMS: automatically open external links in new tab

Published by on

It is widely known that you can open links in a new tab using target="_blank".

For external links it often makes sense to add rel="noopener noreferrer" to secure the content of the current page. More information and a demo on this topic can be found in Mathias Bynens' blog post on this topic.

You can easily add this functionality in Kirby's WYSIWYG markdown editor using the (link) tag. However in longer texts with many links, there is a chance that one might forget adding the rel attribute.

It would be much nicer if Kirby would add target="_blank" and rel="noopener noreferrer" to external links automatically when rendering a page.

For this functionality I'm using feature called hooks: It makes it possible to add custom logic in different steps of the page rendering process.

To get started I first added a new entry called kirbytext:after in confg.php under hooks:

<?php

return [
	'hooks': [
		'kirbytext:after' => function ( $text ) {
			return $text;
		}
	]
];	

This function is executed for each field with the type kirbytext. The HTML string generated from the markdown is passed as an argument. This function is expected to modify the HTML string and return it at the end.

To find and modify all anchor elements in this HTML string one could use a complicated regular expression.

However it is much more convienient to use the PHP DOM library instead:

<?php

return [
	'hooks' => [
		'kirbytext:after' => function ( $text ) {            
			if ( strlen( $text ) > 0 ) {
				// Get current page host
				// (he attributes will only be set for external links)
				$site_host = parse_url( site()->url() )['host'];

				// Convert $text to DOM tree
				$dom = new DomDocument();
				$dom->loadHTML( $text );

				// Loop over all anchor elements
				foreach ( $dom->getElementsByTagName( 'a' ) as $link_el ) {
					// Parse link address
					$link_href = $link_el->getAttribute( 'href' );
					$link_parts = parse_url( $link_href );

					if (
						$link_parts &&
						isset( $link_parts['host'] ) &&
						isset( $link_parts['scheme'] )
					) {
						$link_host = $link_parts['host'];
						$link_scheme = $link_parts['scheme'];

						// Only continue if the link is external
						if (
							in_array( $link_scheme, [ 'http', 'https' ] ) &&
							$link_host !== $site_host
						) {
							// Create string of old link (to find and replace it later)
							$link_str = $dom->saveHTML( $link_el );

							// Add link attributes
							$link_el->setAttribute( 'rel', 'noopener noreferrer' );
							$link_el->setAttribute( 'target', '_blank' );

							// Create new link string
							$new_link_str = $dom->saveHTML( $link_el );

							// replace old link with new link in $text
							$text = str_replace( $link_str, $new_link_str, $text );
						}
					}
				}
			}       
			
			return $text;
		}
	]
];	

Automatic bookmarks

Similarly, you can add a link with an automatically generated ID to all headings to jump easily between sections:

<?php

return [
	'hooks' => [
		'kirbytext:after' => function ( $text ) {
		if ( strlen( $text ) > 0 ) {
			$dom = new DomDocument();
			$dom->loadHTML( $text );
			
			// Tags to be linked
			$tags_with_id = [ 'h1', 'h2', 'h3' ];

			foreach ( $tags_with_id as $tag_name ) {
				foreach ( $dom->getElementsByTagName( $tag_name ) as $item ) {
					$item_content = $item->nodeValue;

					if ( $item_content ) {
						// Create string for old element (to find and replace later)
						$item_str = $dom->saveHTML( $item );

						// Generate element ID from element text content
						$id = Str::slug( $item_content . '' );

						// Set element ID
						$item->setAttribute( 'id', $id );

						// Create a new anchor element and
						// move all element childNodes to the new anchor.
						// Attach insert anchor into element
						$link_el = $dom->createElement( 'a' );
						$link_el->setAttribute( 'href', '#' . $id );
						$link_el->appendChild( $item->firstChild );
						$item->insertBefore( $link_el, $item->firstChild );

						// Generate new HTML string of element
						$new_item_str = $dom->saveHTML( $item );

						// In replace old element with new element in $text
						$text = str_replace( $item_str, $new_item_str, $text );
					}
				}
			}

			return $text;
		}
	]
];	

Leave a comment

Available formatting commands

Use Markdown commands or their HTML equivalents to add simple formatting to your comment:

Text markup
*italic*, **bold**, ~~strikethrough~~, `code` and <mark>marked text</mark>.
Lists
- Unordered item 1
- Unordered list item 2
1. Ordered list item 1
2. Ordered list item 2
Quotations
> Quoted text
Code blocks
```
// A simple code block
```
```php
// Some PHP code
phpinfo();
```
Links
[Link text](https://example.com)
Full URLs are automatically converted into links.

Replied on your own website? Send a Webmention!