Preface
Do you want to show points of interest, like store locations, real estate listings, and events around a city, on a WordPress website, without using Google Maps? For this article, I have prepared a tutorial for the tech-savvy and fellow developers on how to do so.
Note: You can also visit the Related Repository and Demo Site to get an idea of what we will be covering and what is the end result.
Let’s go through why it’s important to use Maps, especially if you’re a local business, why would you like to stick with Google Maps or choose a free alternative like OpenStreetMaps.
Then we will go through step by step on some sample code, that lives in a custom theme for this example, to showcase what is possible using:
- The WordPress CMS and its REST API,
- The famous ACF Plugin, you can download and install for free,
- The Leaflet Javascript Library,
- Javascript and,
- PHP
Let’s get this rolling.
Why would you want to use Maps on your website?
Many websites can benefit from a map feature, the store locator case being the most obvious. It helps with discoverability, drives conversions, and builds trust and brand.
There are many other cases where you would want to implement such a feature, for instance, a website or app for a Conference may display related events or a website of a Real Estate Agent would display positions of the properties. The possibilities are endless.
The Google Maps API is capable of all the above with a major drawback. It’s not free, although it used to be.
In this tutorial we will try to relate events to their respective locations on a map, using a free alternative.
Free Maps with the Leaflet JavaScript Library and OpenStreetMaps
Leaflet is one of those open-source API alternatives for Google Maps that has become quite popular because of its modern JavaScript data library and complete freedom to customize. Of course, Leaflet is not an actual mapping service, but it works great with OpenStreetMaps and it is the favorite solution for developers who prefer open-source products.
Leaflet Pros
- Open-source and free API with a JavaScript data library
- Fully customizable
- Compatible with other mapping API services
Leaflet Cons
- It requires a developer (or at least some developing skills) to get started and use it
- It needs third-party services to support some configurations.
An example implementation
An example implementation using the famous WordPress ACF Plugin and the Leaflet Library in a Custom Post Type.
To showcase the procedure of implementing such a feature with the Leaflet library in our WordPress website we will use a WordPress Child Theme, with custom post types, where we want to show locations on a map, either on the archive page or the single post page. We will make the scripts and settings flexible, so it can be used also in the default post type, or even a page.
In this setting, we will work on our custom theme, rather than building a dedicated plugin, which is out of the scope of this current article.
Step 1: Prepare your WordPress Installation
To start, locate the theme you are going to work on within your folders. It should be something like this: /app/public/wp-content/themes/your-theme-name
, in my settings it’s /app/public/wp-content/themes/twentytwenty-child
.
Now, if we are going to use our script in a custom post type, we better have something set up. You can create custom post types in numerous ways, but in my case, I will write code and place it under /app/public/wp-content/mu-plugins
.
In this folder create a file named custom-post-types.php
(or name it accordingly) and insert this sample code:
Contents of /app/public/wp-content/mu-plugins/custom-post-types.php
:
<?php
function wordpress_acf_leaflet_create_custom_post_types()
{
//Event Post Type
$event_labels = array(
'name' => __('Events'),
'singular_name' => __('Event'),
'all_items' => __('All Events'),
'add_new' => _x('Add new Event', 'Event'),
'add_new_item' => __('Add new Event'),
'edit_item' => __('Edit Event'),
'new_item' => __('New Event'),
'view_item' => __('View Event'),
'search_items' => __('Search in Events'),
'not_found' => __('No Events found'),
'not_found_in_trash' => __('No Events found in trash'),
'parent_item_colon' => ''
);
$event_args = array(
'labels' => $event_labels,
'public' => true,
'show_in_rest' => true,
'has_archive' => true,
'menu_icon' => 'dashicons-calendar',
'rewrite' => array('slug' => 'events'),
'query_var' => true,
'menu_position' => 5,
'supports' => array('excerpt', 'title', 'editor', 'thumbnail')
);
register_post_type('event', $event_args);
}
add_action('init', 'wordpress_acf_leaflet_create_custom_post_types');
Step 2: Advanced Custom Fields
Install and activate Advanced Custom Fields for WordPress Developers in your WordPress installation. For what we will use it, you can use the free version.
Important Note: You should update the permalinks structure after this step. Go to Settings — Permalinks and click on Save Changes
Now in your theme folder create an inc
folder and, inside that, a php file, let’s say location-acf.php
.
Contents of app/public/wp-content/themes/twentytwenty-child/inc/location-acf.php
:
<?php
if( function_exists('acf_add_local_field_group') ):
acf_add_local_field_group(array(
'key' => 'group_631ee0fc4e8ec',
'title' => 'Location',
'fields' => array(
array(
'key' => 'field_631ee108ff573',
'label' => 'Location Address',
'name' => 'location_address',
'type' => 'text',
'instructions' => '',
'required' => 0,
'conditional_logic' => 0,
'wrapper' => array(
'width' => '',
'class' => '',
'id' => '',
),
'default_value' => '',
'placeholder' => '',
'prepend' => '',
'append' => '',
'maxlength' => '',
),
array(
'key' => 'field_631ee1ceff574',
'label' => 'Location Latitude',
'name' => 'location_latitude',
'type' => 'number',
'instructions' => '',
'required' => 0,
'conditional_logic' => 0,
'wrapper' => array(
'width' => '',
'class' => '',
'id' => '',
),
'default_value' => '',
'placeholder' => '',
'prepend' => '',
'append' => '',
'min' => -90,
'max' => 90,
'step' => '0.01',
),
array(
'key' => 'field_631ee278ff575',
'label' => 'Location Longitude',
'name' => 'location_longitude',
'type' => 'number',
'instructions' => '',
'required' => 0,
'conditional_logic' => 0,
'wrapper' => array(
'width' => '',
'class' => '',
'id' => '',
),
'default_value' => '',
'placeholder' => '',
'prepend' => '',
'append' => '',
'min' => -180,
'max' => 180,
'step' => '0.01',
),
),
'location' => array(
array(
array(
'param' => 'post_type',
'operator' => '==',
'value' => 'event',
),
),
array(
array(
'param' => 'post_type',
'operator' => '==',
'value' => 'post',
),
),
array(
array(
'param' => 'page_template',
'operator' => '==',
'value' => 'default',
),
),
),
'menu_order' => 0,
'position' => 'normal',
'style' => 'default',
'label_placement' => 'top',
'instruction_placement' => 'label',
'hide_on_screen' => '',
'active' => true,
'description' => '',
'show_in_rest' => 1,
));
endif;
The last step is to require the above file in your functions.php
, doing like so:
<?php
//Require Custom Location Files
require_once( get_stylesheet_directory(). '/inc/location-acf.php' );
Now you should be able to, not only see the custom post type event, but also insert Location information, like address and the Latitude and Longitude, or in other words the coordinates at the geographic coordinate system. As per the above settings the Pages and Posts will also have this option. I have done this intentionally, so you can experiment with the scripts.
Step 3: Create a Custom API Route
For this example, we will use an optimized custom endpoint to get the information to build our maps. Learn more about Adding Custom Endpoints on the official documentation.
Again, in the inc
folder create a php file named maps-route.php
.
Contents of app/public/wp-content/themes/twentytwenty-child/inc/maps-route.php
:
<?php
add_action('rest_api_init', 'demoRegisterMaps');
function demoRegisterMaps()
{
register_rest_route('demo/v1', 'maps', array(
'methods' => WP_REST_Server::READABLE,
'callback' => 'query_posts_and_pages_with_params'
));
};
function query_posts_and_pages_with_params(WP_REST_Request $request)
{
$args = array();
$args['post_type'] = $request->get_param('type');
$args['id'] = $request->get_param('id');
return query_posts_and_pages($args);
}
function query_posts_and_pages($args)
{
if ($args['post_type'] == 'page' && !$args['id']) {
$query_args = array(
'page_id' => $args['id'],
'post_type' => $args['post_type'],
'nopaging' => true,
'order' => 'ASC',
'orderby' => 'title',
);
} elseif ($args['post_type'] == 'page' && $args['id']) {
$query_args = array(
'page_id' => $args['id'],
'nopaging' => true,
'order' => 'ASC',
'orderby' => 'title',
);
} elseif ($args['post_type'] != 'page' && !$args['id']) {
$query_args = array(
'post_type' => $args['post_type'],
'nopaging' => true,
'order' => 'ASC',
'orderby' => 'title',
);
} elseif ($args['post_type'] != 'page' && $args['id']) {
$query_args = array(
'p' => $args['id'],
'post_type' => $args['post_type'],
'nopaging' => true,
'order' => 'ASC',
'orderby' => 'title',
);
}
return fetchByPostTypeAndID($query_args);
}
function fetchByPostTypeAndID($query_args)
{
// Run a custom query
$meta_query = new WP_Query($query_args);
if ($meta_query->have_posts()) {
//Define an empty array
$data = array();
// Store each post's data in the array
while ($meta_query->have_posts()) {
$meta_query->the_post();
if (get_field('location_latitude') && get_field('location_longitude')) {
$post_object = (object) [
'id' => get_the_ID(),
'title' => (object) ['rendered' => get_the_title()],
'link' => get_the_permalink(),
'location' => (object) [
'latitude' => get_field('location_latitude'),
'longitude' => get_field('location_longitude'),
'address' => get_field('location_address')
]
];
$data[] = $post_object;
}
}
// Return the data
return $data;
} else {
// If there is no post
return new WP_Error('rest_not_found', esc_html__('Error fetching data.', 'twentytwentytwochild'), array('status' => 404));
}
}
The last step is to require the above file in your functions.php
, doing like so:
<?php
//Require Custom Location Files
require_once( get_stylesheet_directory(). '/inc/location-acf.php' );
require_once( get_stylesheet_directory(). '/inc/maps-route.php' );
Now you should be able to query and experiment with this endpoint using software like PostMan.
For instance, the response for my setup, using dummy data, for this route https://wordpress-acf-leaflet-demo.elissavet.me/wp-json/demo/v1/maps/?type=event&id=8
(choosing a GET) is
[
{
"id": 8,
"title": {
"rendered": "Athens Art Book Fair 2022"
},
"link": "https://wordpress-acf-leaflet-demo.elissavet.me/events/athens-art-book-fair-2022/",
"location": {
"latitude": "37.973256",
"longitude": "23.743341",
"address": "The Athens Conservatory, Rigillis & 17-19 Vasileos Georgiou B, Pangrati, 106 75"
}
}
]
and the JSON returned for the route https://wordpress-acf-leaflet-demo.elissavet.me/wp-json/demo/v1/maps/?type=event
is the whole set of events:
[
{
"id": 8,
"title": {
"rendered": "Athens Art Book Fair 2022"
},
"link": "https://wordpress-acf-leaflet-demo.elissavet.me/events/athens-art-book-fair-2022/",
"location": {
"latitude": "37.973256",
"longitude": "23.743341",
"address": "The Athens Conservatory, Rigillis & 17-19 Vasileos Georgiou B, Pangrati, 106 75"
}
},
{
"id": 7,
"title": {
"rendered": "Illustradays 2022"
},
"link": "https://wordpress-acf-leaflet-demo.elissavet.me/events/illustradays-2022%ef%bf%bc/",
"location": {
"latitude": "37.9732091",
"longitude": "23.7009461",
"address": "Serafeio Athletics & Community Complex, 160 Pireos, Rouf, 118 54"
}
},
{
"id": 9,
"title": {
"rendered": "Technopolis Vinyl Market"
},
"link": "https://wordpress-acf-leaflet-demo.elissavet.me/events/technopolis-vinyl-market/",
"location": {
"latitude": "37.97",
"longitude": "23.71",
"address": "Technopolis, 100 Pireos, Gazi, 118 54"
}
}
]
Step 4: Send related variables from WordPress to the Front-End
To be more flexible in our setup, or maybe even call custom pin graphics from our theme’s images folder, we need to pass some variables from the backend to the frontend.
This can be accomplished using the wp_localize_script.
Now add this in functions.php
:
function custom_demo_files()
{
wp_enqueue_script('main-demo-js', get_theme_file_uri('/build/index.js'), array(), '1.0', true);
wp_localize_script('main-demo-js', 'php_to_js', [
'data' => array(
'theme_uri' => get_theme_file_uri(),
'root_url' => get_site_url()
)
]);
wp_enqueue_style('demo_main_style', get_theme_file_uri('/build/style-index.css'));
wp_enqueue_style('demo_extra_style', get_theme_file_uri('/build/index.css'));
};
add_action('wp_enqueue_scripts', 'custom_demo_files');
Step 5: Prepare your Javascript Setup
To start working with the javascript source javascript files, (located in your theme folder under src) use the below code in the file package.json
(you can edit the name to match your preference), located in the root of your theme’s folder.
Note: You should have Node.js and npm installed. If you haven’t already you could visit this guide.
Contents of app/public/wp-content/themes/twentytwenty-child/package.json
:
{
"name": "fictional-demo-theme",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "wp-scripts build",
"start": "wp-scripts start",
"dev": "wp-scripts start",
"test": "echo \"Error: no test specified\" && exit 1"
},
"author": "Elissavet Triantafyllopoulou",
"license": "ISC",
"dependencies": {
"@wordpress/scripts": "^24.0.0",
"axios": "^0.27.2",
"leaflet": "^1.8.0"
}
}
Open up the terminal and run npm-install
After that, you should try to start the development server with npm run dev
or perform a one-time operation with npm run build
Step 6: Front-end - Your Javascript and CSS Files
Contents of app/public/wp-content/themes/twentytwenty-child/src/modules/LeafletMap.js
:
import L from "leaflet";
import axios from "axios"
class LeafletMap {
constructor() {
document.querySelectorAll(".leaflet-map").forEach(el => {
el.post_type = !('template' in el.dataset) ? 'post' : el.getAttribute("data-template") //template keeps the post-type
el.post_id = !('id' in el.dataset) ? '' : el.getAttribute("data-id")
this.new_map(el)
})
}
new_map(el) {
let get_url = `${php_to_js.data.root_url}/wp-json/demo/v1/maps?type=${el.post_type}${el.post_id ? `&id=${el.post_id}` : ''}`
let that = this
let arrayOfLatLngs = []
let showLink = false
const args = {
zoom: 14,
centerLat: 0.0,
centerLng: 0.0,
}
const map = L.map(el).setView([args.centerLng, args.centerLng], args.zoom);
L.tileLayer('https://tile.openstreetmap.org/{z}/{x}/{y}.png', {
maxZoom: 19,
attribution: '© OpenStreetMap'
}).addTo(map)
axios.get(get_url)
.then(function (response) {
// handle success
console.log(response); //left intentionally here
if (Array.isArray(response.data)) {
if (response.data.length > 1) {
showLink = true
}else {
showLink = false
}
response.data.forEach(element => {
if (element.location.latitude !== '' && element.location.longitude !== '') {
arrayOfLatLngs.push([element.location.latitude, element.location.longitude])
that.add_marker(map, element, showLink)
}
});
}
// center map
that.center_map(map, arrayOfLatLngs)
})
.catch(function (error) {
// handle error
//Show a custom error Map
//Console error
console.log(error);
})
} // end new_map
add_marker(map, element, showLink) {
const templateUrl = php_to_js.data.theme_uri
const myIcon = L.icon({
iconUrl: templateUrl + "/images/maps/icon.png",
iconRetinaUrl: templateUrl + '/images/maps/icon-retina.png',
})
const { latitude, longitude, address } = element.location
const marker = new L.Marker([latitude, longitude], { icon: myIcon })
marker.addTo(map)
marker.bindPopup(`
${`<p class="popup-title">${showLink? `<a href="${element.link}">` : ''}${element.title.rendered}${showLink? '</a>' : ''}</p>`}
${address ? `<p class="popup-address">${address}</p>` : ''}
`)
} // end add_marker
center_map(map, arrayOfLatLngs) {
var bounds = new L.LatLngBounds(arrayOfLatLngs);
// only 1 marker?
if (arrayOfLatLngs.length == 1) {
// set center of map
map.setView(arrayOfLatLngs[0], 16, { animation: true });
} else {
// fit to bounds
map.fitBounds(bounds).setZoom(map.getZoom() - 1);
}
} // end center_map
}
export default LeafletMap
Contents of wp-content/themes/twentytwenty-child/src/index.js
:
import "../css/style.scss"
// Our modules / classes
import LeafletMap from "./modules/LeafletMap"
// Instantiate a new object using our modules/classes
var leafletMap = new LeafletMap()
Contents of /wp-content/themes/twentytwenty-child/css/style.scss
:
@import "../node_modules/leaflet/dist/leaflet.css";
.acf-map {
width: 100%;
height: 500px;
border: #ccc solid 1px;
font-size: 18px;
margin: 3rem auto;
@media (max-width: 640px) {
height: 320px;
}
p {
font-size: 1.5rem;
}
.popup-title {
color: #cd2653;
}
.popup-address {
font-weight: bolder;
}
}
Step 7: Call the Map’s HTML in your theme PHP files
Now that you hopefully have everything set up, you will need to output a div with certain characteristics to your theme files, to show the map and the related marker or markers.
In general, the script expects the div to have a class of “leaflet-map” and 2 data attributes:
- data-template that should hold our post-type
- data-id which should be used to query only the related location of our post
If you want to display only one marker, in our case a single event site, this code would go on single-event.php
:
<!-- Example usage on pages -->
<?php if (get_field('location_latitude') && get_field('location_longitude')) : ?>
<!-- example usage on page or custom post-type / single point of interest marker -->
<hr class="section-break">
<h2 class="headline headline--small">Location of <?php the_title(); ?></h2>
<div class="acf-map leaflet-map" data-id=<?php the_ID(); ?> data-template=<?php echo get_post_type() ?>></div>
<?php endif; ?>
Or if you want to show the list of all events in archive-event.php
you would:
<!-- example usage on custom post-type multiple points of interest markers -->
<hr class="section-break">
<h2 class="headline headline--small">Find Sites On The Map</h2>
<div class="acf-map leaflet-map" data-template=<?php echo get_post_type() ?>></div>
or you could even hard-code it like so:
<div class="acf-map leaflet-map" data-template="event"></div>
Conclusion
In this article, we saw how maps can be used in a website and their importance on the User Experience.
Then we experimented with an actual implementation, with the free and open-source Leaflet Javascript Library implementing it in a custom WordPress Theme.
Visit the Related Repository and Demo Site, to see what we’ve covered and the end result.