In this article, I will describe how to create navigation using SVG map where each map region links to a specific entry. Links won't be hardcoded - the map will have an interface in the control panel that will allow admins to assign entries to specific regions.
SVG map #
We will use this map of USA as an example. You can find many SVG maps on wikipedia, and even more on amcharts website. Keep in mind however that these from amcharts are free for non-commercial projects only.
Each region of the SVG map is represented by path
HTML element. It's important that these paths have some kind of HTML attribute representing their region - our example map uses id
for that. For example, Texas path
element has id TX
, which corresponds to Texas ISO code - US-TX
.
Control panel interface #
We will add links to the map using matrix field. Create a matrix field with handle mapLinks
and define a single block. It will have two sub-fields, both set to required:
- Entry field with handle
linkEntry
. Map region will link to this entry. - Text field with handle
linkRegion
. This field will hold map region code.
Using a text field for setting region code is the easiest solution. As an alternative, we might use dropdown field with regions list, or even Country select Field - if we are dealing with a map of the whole world.
We might also use another strategy. Instead of keeping all links in one place (within matrix field assigned to some entry or global), we can assign a field that will hold region code to specific entries. In our example, we will however use a matrix field approach.
Templating #
To add links to SVG map, we need to manipulate its HTML code - wrap path
elements with links. We can achieve this by using Retcon plugin.
Here's Twig code that generates our map. Remember to also place map.svg
file in proper directory so it can be included within our Twig component.
{# settings #}
{% set map = include('usa.svg') %}
{% set mapLinks = entry.mapLinks.all() %}
{# logic #}
{% set map = map|retconRemove('style') %}
{% set regionsUsed = [] %}
{% for link in mapLinks %}
{% if link.linkRegion is not empty and link.linkEntry.exists() and link.linkRegion not in regionsUsed %}
{% set regionsUsed = regionsUsed|merge([link.linkRegion]) %}
{% set linkRegion = link.linkRegion|upper %}
{% set linkUrl = link.linkEntry.one().url %}
{% set map = map|retconWrap( 'path#'~linkRegion, 'a#link-'~linkRegion ) %}
{% set map = map|retconAttr( 'a#link-'~linkRegion, { 'href' : linkUrl } ) %}
{% endif %}
{% endfor %}
{# output #}
{{map|raw}}
How does it work? #
First, we load SVG file using include
. SVG is just markup instead of a "real" image, so it can be treated like a template file. I decided to use include
instead of svg funcition, because svg()
namespaces all ID and class attributes within the markup, which could pose problems if we wanted to style our map. After loading map, we also set a collection of links defined in the control panel to mapLinks
variable.
Most SVGs have their own styles defined within their own style
tag. I decided to remove them using Retcon retconRemove
filter - because I would rather keep all styles in the separate CSS file. Example CSS that would let us distinguish paths that are links from others, might look like this:
svg a path{
fill: red;
}
After that, we define regionsUsed
array and loop through the matrix blocks holding our map links. For each link, we wrap path
element that has ID matching linkRegion
region code set in the control panel. Region code is transformed into uppercase using upper
filter, because IDs in our map are in capital letters. This might however not be the case for other maps.
For each for
loop, we check if region code is defined, is entry that region is supposed to link to exists, and most importantly - if a region wasn't already used. If it wasn't, we insert region code into regionsUsed
array, to make sure it won't be used again. It's important to perform this check - with duplicate regions, we would end up with links wrapped in other links. We cannot easily block usage of duplicate regions in the control panel, so make sure to add a warning in matrix field description: that in case of duplicate regions, only the first occurrence of the region will be used to create link.
Wrapping path
with links is done using Retcon Wrap filter. Each link will have its own ID - the second param of Wrap
filter is set to a#link-'~linkRegion
, which generates a link with ID like link-TX
. This ID of a link is used as a selector in the second Retcon filter, retconAttr
- which adds proper href
attribute to our generated link.
Finally, SVG map is passed through raw filter and outputted into the template. Without this filter, HTML code would be escaped and displayed as regular text. Using Retcon filters also works like using raw
, but in case if matrix field being empty (and if you decided to not use retconRemove
filter to remove styles), code inside for
loop would not run and no Retcon filter would be used on SVG. That's why I decided to use raw
filter - to ensure that even map without links would display correctly.