diff --git a/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java b/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java
new file mode 100644
index 0000000000000000000000000000000000000000..67fde44b8117296f13defc599adc6dcd0cd970a3
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/utils/Sites.java
@@ -0,0 +1,14 @@
+package fr.inra.urgi.faidare.utils;
+
+import java.nio.charset.StandardCharsets;
+import java.util.Base64;
+
+/**
+ * Utilities for sites
+ * @author JB Nizet
+ */
+public class Sites {
+    public static String siteIdToLocationId(String siteId) {
+        return Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII));
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
index cf3430490eb3b9cba9803c41ab775b7a2558ce14..cd24d06dc61698b217db1422c9585ed4b7b97b05 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmController.java
@@ -1,5 +1,6 @@
 package fr.inra.urgi.faidare.web.germplasm;
 
+import java.util.ArrayList;
 import java.util.Arrays;
 import java.util.Collections;
 import java.util.Comparator;
@@ -105,7 +106,6 @@ public class GermplasmController {
             createXref("bazbing")
         );
 
-
         sortDonors(germplasm);
         sortPopulations(germplasm);
         sortCollections(germplasm);
@@ -117,8 +117,7 @@ public class GermplasmController {
                                     faidareProperties.getByUri(germplasm.getSourceUri()),
                                     attributes,
                                     pedigree,
-                                    crossReferences
-                                )
+                                    crossReferences)
         );
     }
 
@@ -205,8 +204,23 @@ public class GermplasmController {
         SiteVO originSite = new SiteVO();
         originSite.setSiteId("1234");
         originSite.setSiteName("Le Moulon");
+        originSite.setSiteType("Origin site");
+        originSite.setLatitude(47.0F);
+        originSite.setLongitude(12.0F);
         result.setOriginSite(originSite);
 
+        List<SiteVO> evaluationSites = new ArrayList<>();
+        for (int i = 0; i < 5; i++) {
+            SiteVO evaluationSite = new SiteVO();
+            evaluationSite.setSiteId(Integer.toString(12347 + i));
+            evaluationSite.setSiteType("Evaluation site");
+            evaluationSite.setSiteName("Site " + i);
+            evaluationSite.setLatitude(46.0F + i);
+            evaluationSite.setLongitude(13.0F + i);
+            evaluationSites.add(evaluationSite);
+        }
+        result.setEvaluationSites(evaluationSites);
+
         result.setGenus("Genus 1");
         result.setSpecies("Species 1");
         result.setSpeciesAuthority("Species Auth");
@@ -241,7 +255,13 @@ public class GermplasmController {
         collector.setAccessionNumber("567");
         result.setCollector(collector);
 
-        result.setCollectingSite(originSite);
+        SiteVO collectingSite = new SiteVO();
+        collectingSite.setSiteId("1235");
+        collectingSite.setSiteName("St Just");
+        collectingSite.setSiteType("Collecting site");
+        collectingSite.setLatitude(48.0F);
+        collectingSite.setLongitude(13.0F);
+        result.setCollectingSite(collectingSite);
         result.setAcquisitionDate("In the summer");
 
         GermplasmInstituteVO breeder = new GermplasmInstituteVO();
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
index 8acdf78fc5f36d5427dba77b1abf57463f388f74..ffa24336f395b2d00817babacc09b38981318178 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/germplasm/GermplasmModel.java
@@ -1,13 +1,16 @@
 package fr.inra.urgi.faidare.web.germplasm;
 
+import java.util.ArrayList;
 import java.util.List;
 
 import fr.inra.urgi.faidare.domain.brapi.v1.data.BrapiGermplasmAttributeValue;
 import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmInstituteVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.PedigreeVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO;
 import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
 import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.web.site.MapLocation;
 import org.apache.logging.log4j.util.Strings;
 
 /**
@@ -136,4 +139,19 @@ public final class GermplasmModel {
             || Strings.isNotBlank(this.pedigree.getCrossingYear())
             || Strings.isNotBlank(this.pedigree.getFamilyCode()));
     }
+
+    public List<MapLocation> getMapLocations() {
+        List<SiteVO> sites = new ArrayList<>();
+        if (germplasm.getCollectingSite() != null) {
+            sites.add(germplasm.getCollectingSite());
+        }
+        if (germplasm.getOriginSite() != null) {
+            sites.add(germplasm.getOriginSite());
+        }
+        if (germplasm.getEvaluationSites() != null) {
+            sites.addAll(germplasm.getEvaluationSites());
+        }
+
+        return MapLocation.sitesToDisplayableMapLocations(sites);
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java
new file mode 100644
index 0000000000000000000000000000000000000000..3b096853cfff4a467d7a277f9fb4b41e65b2ad01
--- /dev/null
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/MapLocation.java
@@ -0,0 +1,82 @@
+package fr.inra.urgi.faidare.web.site;
+
+import java.util.List;
+import java.util.stream.Collectors;
+
+import fr.inra.urgi.faidare.domain.data.LocationVO;
+import fr.inra.urgi.faidare.domain.data.germplasm.SiteVO;
+import fr.inra.urgi.faidare.utils.Sites;
+
+/**
+ * An object that can be serialized to JSON to serve as a map marker.
+ * @author JB Nizet
+ */
+public final class MapLocation {
+    private final String locationDbId;
+    private final String locationType;
+    private final String locationName;
+    private final double latitude;
+    private final double longitude;
+
+    public MapLocation(String locationDbId,
+                       String locationType,
+                       String locationName,
+                       double latitude,
+                       double longitude) {
+        this.locationDbId = locationDbId;
+        this.locationType = locationType;
+        this.locationName = locationName;
+        this.latitude = latitude;
+        this.longitude = longitude;
+    }
+
+    public MapLocation(LocationVO site) {
+        this(site.getLocationDbId(),
+             site.getLocationType(),
+             site.getLocationName(),
+             site.getLatitude(),
+             site.getLongitude());
+    }
+
+    public MapLocation(SiteVO site) {
+        this(Sites.siteIdToLocationId(site.getSiteId()),
+             site.getSiteType(),
+             site.getSiteName(),
+             site.getLatitude(),
+             site.getLongitude());
+    }
+
+    public static List<MapLocation> locationsToDisplayableMapLocations(List<LocationVO> locations) {
+        return locations.stream()
+                        .filter(location -> location.getLatitude() != null && location.getLongitude() != null)
+                        .map(MapLocation::new)
+                        .collect(Collectors.toList());
+    }
+
+    public static List<MapLocation> sitesToDisplayableMapLocations(List<SiteVO> sites) {
+        return sites.stream()
+                    .filter(site -> site.getLatitude() != null && site.getLongitude() != null)
+                    .map(MapLocation::new)
+                    .collect(Collectors.toList());
+    }
+
+    public String getLocationDbId() {
+        return locationDbId;
+    }
+
+    public String getLocationType() {
+        return locationType;
+    }
+
+    public String getLocationName() {
+        return locationName;
+    }
+
+    public double getLatitude() {
+        return latitude;
+    }
+
+    public double getLongitude() {
+        return longitude;
+    }
+}
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
index 61102ce0cc0af9ed2b8394cfd5df9ee3ac1deb58..cd8f7bb80bbb6cad8d29a9c76c3b6e1a39dce06f 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/site/SiteModel.java
@@ -111,4 +111,8 @@ public final class SiteModel {
     public List<XRefDocumentVO> getCrossReferences() {
         return crossReferences;
     }
+
+    public List<MapLocation> getMapLocations() {
+        return MapLocation.locationsToDisplayableMapLocations(Collections.singletonList(this.site));
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
index ff6aee3bfb5e322b4385cbd491ad9ee9f336b5cd..833c45471061a5ef02d7f5fc57319fc9afc8a12b 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyController.java
@@ -12,16 +12,20 @@ import com.google.common.collect.Lists;
 import fr.inra.urgi.faidare.api.NotFoundException;
 import fr.inra.urgi.faidare.config.FaidareProperties;
 import fr.inra.urgi.faidare.domain.criteria.GermplasmPOSTSearchCriteria;
+import fr.inra.urgi.faidare.domain.data.LocationVO;
 import fr.inra.urgi.faidare.domain.data.TrialVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.GermplasmVO;
 import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
 import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
 import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
 import fr.inra.urgi.faidare.repository.es.GermplasmRepository;
+import fr.inra.urgi.faidare.repository.es.LocationRepository;
 import fr.inra.urgi.faidare.repository.es.StudyRepository;
 import fr.inra.urgi.faidare.repository.es.TrialRepository;
 import fr.inra.urgi.faidare.repository.es.XRefDocumentRepository;
 import fr.inra.urgi.faidare.repository.file.CropOntologyRepository;
+import fr.inra.urgi.faidare.web.site.MapLocation;
+import org.apache.logging.log4j.util.Strings;
 import org.springframework.stereotype.Controller;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -42,19 +46,22 @@ public class StudyController {
     private final GermplasmRepository germplasmRepository;
     private final CropOntologyRepository cropOntologyRepository;
     private final TrialRepository trialRepository;
+    private final LocationRepository locationRepository;
 
     public StudyController(StudyRepository studyRepository,
                            FaidareProperties faidareProperties,
                            XRefDocumentRepository xRefDocumentRepository,
                            GermplasmRepository germplasmRepository,
                            CropOntologyRepository cropOntologyRepository,
-                           TrialRepository trialRepository) {
+                           TrialRepository trialRepository,
+                           LocationRepository locationRepository) {
         this.studyRepository = studyRepository;
         this.faidareProperties = faidareProperties;
         this.xRefDocumentRepository = xRefDocumentRepository;
         this.germplasmRepository = germplasmRepository;
         this.cropOntologyRepository = cropOntologyRepository;
         this.trialRepository = trialRepository;
+        this.locationRepository = locationRepository;
     }
 
     @GetMapping("/{studyId}")
@@ -77,6 +84,11 @@ public class StudyController {
         List<GermplasmVO> germplasms = getGermplasms(study);
         List<ObservationVariableVO>variables = getVariables(study);
         List<TrialVO> trials = getTrials(study);
+        LocationVO location = getLocation(study);
+
+        // TODO remove this
+        location.setLatitude(34.0);
+        location.setLongitude(14.0);
 
         return new ModelAndView("study",
                                 "model",
@@ -86,11 +98,19 @@ public class StudyController {
                                     germplasms,
                                     variables,
                                     trials,
-                                    crossReferences
+                                    crossReferences,
+                                    location
                                 )
         );
     }
 
+    private LocationVO getLocation(StudyDetailVO study) {
+        if (Strings.isBlank(study.getLocationDbId())) {
+            return null;
+        }
+        return locationRepository.getById(study.getLocationDbId());
+    }
+
     private List<GermplasmVO> getGermplasms(StudyDetailVO study) {
         if (study.getGermplasmDbIds() == null || study.getGermplasmDbIds().isEmpty()) {
             return Collections.emptyList();
@@ -125,6 +145,8 @@ public class StudyController {
                     .collect(Collectors.toList());
     }
 
+
+
     private XRefDocumentVO createXref(String name) {
         XRefDocumentVO xref = new XRefDocumentVO();
         xref.setName(name);
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
index 980c78d43d3dcae233c2371de2f80add8f11d421..bc77dfc3d9b9fdb7da63dd7b47afb15ab1e3bff6 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/study/StudyModel.java
@@ -1,11 +1,8 @@
 package fr.inra.urgi.faidare.web.study;
 
-import java.util.Arrays;
 import java.util.Collections;
-import java.util.HashSet;
 import java.util.List;
 import java.util.Map;
-import java.util.Set;
 import java.util.stream.Collectors;
 
 import fr.inra.urgi.faidare.domain.data.LocationVO;
@@ -15,6 +12,7 @@ import fr.inra.urgi.faidare.domain.data.study.StudyDetailVO;
 import fr.inra.urgi.faidare.domain.data.variable.ObservationVariableVO;
 import fr.inra.urgi.faidare.domain.datadiscovery.data.DataSource;
 import fr.inra.urgi.faidare.domain.xref.XRefDocumentVO;
+import fr.inra.urgi.faidare.web.site.MapLocation;
 
 /**
  * The model used by the study page
@@ -27,6 +25,7 @@ public final class StudyModel {
     private final List<ObservationVariableVO> variables;
     private final List<TrialVO> trials;
     private final List<XRefDocumentVO> crossReferences;
+    private final LocationVO location;
     private final List<Map.Entry<String, Object>> additionalInfoProperties;
 
     public StudyModel(StudyDetailVO study,
@@ -34,13 +33,15 @@ public final class StudyModel {
                       List<GermplasmVO> germplasms,
                       List<ObservationVariableVO> variables,
                       List<TrialVO> trials,
-                      List<XRefDocumentVO> crossReferences) {
+                      List<XRefDocumentVO> crossReferences,
+                      LocationVO location) {
         this.study = study;
         this.source = source;
         this.germplasms = germplasms;
         this.variables = variables;
         this.trials = trials;
         this.crossReferences = crossReferences;
+        this.location = location;
 
         Map<String, Object> additionalInfo =
             study.getAdditionalInfo() == null ? Collections.emptyMap() : study.getAdditionalInfo().getProperties();
@@ -79,4 +80,11 @@ public final class StudyModel {
     public List<Map.Entry<String, Object>> getAdditionalInfoProperties() {
         return additionalInfoProperties;
     }
+
+    public List<MapLocation> getMapLocations() {
+        if (this.location == null) {
+            return Collections.emptyList();
+        }
+        return MapLocation.locationsToDisplayableMapLocations(Collections.singletonList(this.location));
+    }
 }
diff --git a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
index a9f699de3dc80e7bc1b3bc4a7af0bc7bbb5d5da3..9ba16d342d94e9097e720c5b3ad2e370d564fe6d 100644
--- a/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
+++ b/backend/src/main/java/fr/inra/urgi/faidare/web/thymeleaf/FaidareExpressions.java
@@ -9,8 +9,11 @@ import java.util.Locale;
 import java.util.Map;
 import java.util.function.Function;
 
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import fr.inra.urgi.faidare.domain.data.germplasm.CollPopVO;
 import fr.inra.urgi.faidare.domain.data.germplasm.TaxonSourceVO;
+import fr.inra.urgi.faidare.utils.Sites;
 import org.apache.logging.log4j.util.Strings;
 
 /**
@@ -21,6 +24,7 @@ public class FaidareExpressions {
 
     private static final Map<String, Function<String, String>> TAXON_ID_URL_FACTORIES_BY_SOURCE_NAME =
         createTaxonIdUrlFactories();
+    private static final ObjectMapper objectMapper = new ObjectMapper();
 
     private static Map<String, Function<String, String>> createTaxonIdUrlFactories() {
         Map<String, Function<String, String>> result = new HashMap<>();
@@ -38,7 +42,7 @@ public class FaidareExpressions {
     }
 
     public String toSiteParam(String siteId) {
-        return Base64.getUrlEncoder().encodeToString(("urn:URGI/location/" + siteId).getBytes(StandardCharsets.US_ASCII));
+        return Sites.siteIdToLocationId(siteId);
     }
 
     public String collPopTitle(CollPopVO collPopVO) {
@@ -55,6 +59,15 @@ public class FaidareExpressions {
         return urlFactory != null ? urlFactory.apply(taxonSource.getTaxonId()) : null;
     }
 
+    public String toJson(Object value) {
+        try {
+            return objectMapper.writeValueAsString(value);
+        }
+        catch (JsonProcessingException e) {
+            throw new IllegalArgumentException(e);
+        }
+    }
+
     private String collPopTitle(CollPopVO collPopVO, Function<String, String> nameTransformer) {
         if (Strings.isBlank(collPopVO.getType())) {
             return nameTransformer.apply(collPopVO.getName());
diff --git a/backend/src/main/resources/static/assets/images/marker-icon-blue.png b/backend/src/main/resources/static/assets/images/marker-icon-blue.png
new file mode 100644
index 0000000000000000000000000000000000000000..e2e9f757f515ded172e6f72c3ce55bbe15579649
Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-blue.png differ
diff --git a/backend/src/main/resources/static/assets/images/marker-icon-green.png b/backend/src/main/resources/static/assets/images/marker-icon-green.png
new file mode 100644
index 0000000000000000000000000000000000000000..6b4fb278f611f802d0c4e9cf88edad80e1e1c975
Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-green.png differ
diff --git a/backend/src/main/resources/static/assets/images/marker-icon-purple.png b/backend/src/main/resources/static/assets/images/marker-icon-purple.png
new file mode 100644
index 0000000000000000000000000000000000000000..63e423d250842ad5c9666507a4dd843a8f6a2b93
Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-purple.png differ
diff --git a/backend/src/main/resources/static/assets/images/marker-icon-red.png b/backend/src/main/resources/static/assets/images/marker-icon-red.png
new file mode 100644
index 0000000000000000000000000000000000000000..e3c0026ef246271f89aad81b6cf47b2ff63596d9
Binary files /dev/null and b/backend/src/main/resources/static/assets/images/marker-icon-red.png differ
diff --git a/backend/src/main/resources/static/assets/script.js b/backend/src/main/resources/static/assets/script.js
new file mode 100644
index 0000000000000000000000000000000000000000..a01603cc5ae086a8d60178594fae420dd5fe6580
--- /dev/null
+++ b/backend/src/main/resources/static/assets/script.js
@@ -0,0 +1,100 @@
+const faidare = (() => {
+    function initializePopovers() {
+        const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
+        popoverTriggerList.forEach(popoverTriggerEl => {
+            const options = {};
+            const contentSelector = popoverTriggerEl.dataset.bsElement;
+            if (contentSelector) {
+                const content = document.querySelector(contentSelector);
+                if (content) {
+                    options.content = () => {
+                        const element = document.createElement('div');
+                        element.innerHTML = content.innerHTML;
+                        return element;
+                    };
+                    options.html = true;
+                } else {
+                    throw new Error('element with selector ' + contentSelector + ' not found');
+                }
+            }
+            return new bootstrap.Popover(popoverTriggerEl, options);
+        });
+    }
+
+    function markerColor(location) {
+        switch (location.locationType) {
+            case 'Origin site':
+                return 'red';
+            case 'Collecting site':
+                return 'blue';
+            case 'Evaluation site':
+                return 'green';
+        }
+        return 'purple';
+    }
+
+    function markerIconUrl(contextPath, location) {
+        return `${contextPath}/assets/images/marker-icon-${markerColor(location)}.png`;
+    }
+
+    function initializeMap(options) {
+        if (!options.locations.length) {
+            return;
+        }
+
+        const mapContainerElement = document.querySelector('#map-container');
+        mapContainerElement.classList.remove("d-none");
+        const mapElement = document.querySelector('#map');
+        const map = L.map(mapElement);
+        L.tileLayer('https://server.arcgisonline.com/ArcGIS/rest/services/World_Street_Map/MapServer/tile/{z}/{y}/{x}', {
+            attribution: 'Tiles &copy; Esri &mdash; Source: Esri, DeLorme, NAVTEQ, USGS, Intermap, iPC, NRCAN, Esri Japan, METI, ' +
+                'Esri China (Hong Kong), Esri (Thailand), TomTom, 2012'
+        }).addTo(map);
+
+        const firstLocation = options.locations[0];
+        map.setView([firstLocation.latitude, firstLocation.longitude], 5);
+
+        const markers = L.markerClusterGroup();
+        const mapMarkers = [];
+        for (const location of options.locations) {
+            const icon = L.icon({
+                iconUrl: markerIconUrl(options.contextPath, location),
+                iconAnchor: [12, 41], // point of the icon which will correspond to marker's location
+            });
+            const popupElement = document.createElement('div');
+            const titleElement = document.createElement('strong');
+            titleElement.innerText = location.locationName;
+            const typeElement = document.createElement('div');
+            typeElement.innerText = location.locationType;
+            const linkElement = document.createElement('a');
+            linkElement.innerText = 'Details';
+            linkElement.href = `${options.contextPath}/sites/${location.locationDbId}`;
+            popupElement.appendChild(titleElement);
+            popupElement.appendChild(typeElement);
+            popupElement.appendChild(linkElement);
+
+            const marker = L.marker(
+                [location.latitude, location.longitude],
+                { icon: icon }
+            );
+            markers.addLayer(marker.bindPopup(popupElement));
+            mapMarkers.push(marker);
+        }
+        const initialZoom = map.getZoom();
+
+        map.fitBounds(L.featureGroup(mapMarkers).getBounds());
+        const markerZoom = map.getZoom();
+
+        setTimeout(() => {
+            map.setZoom(Math.min(initialZoom, markerZoom));
+            map.addLayer(markers);
+        }, 100);
+    }
+
+    return {
+        initializePopovers,
+        initializeMap
+    };
+})();
+
+
diff --git a/backend/src/main/resources/static/assets/style.css b/backend/src/main/resources/static/assets/style.css
index 340b22eae82d8f58d76250fdb37ded071695b6c5..87c396caa7c37cfe7a8fe274146bb9ce1a3f070f 100644
--- a/backend/src/main/resources/static/assets/style.css
+++ b/backend/src/main/resources/static/assets/style.css
@@ -5,3 +5,11 @@
 .popover {
     max-width: min(80vw, 600px);
 }
+
+#map {
+    height: min(400px, 60vh);
+}
+
+.map-legend img {
+    height: 1.5rem;
+}
diff --git a/backend/src/main/resources/templates/fragments/map.html b/backend/src/main/resources/templates/fragments/map.html
new file mode 100644
index 0000000000000000000000000000000000000000..7fce9d1ec4155e167d3faf5572ee1c9968613750
--- /dev/null
+++ b/backend/src/main/resources/templates/fragments/map.html
@@ -0,0 +1,23 @@
+<!DOCTYPE html>
+
+<html xmlns:th="http://www.thymeleaf.org">
+
+<body>
+<!--
+Reusable fragment displaying a map and its legend.
+The map is initially hidden. The JavaScript displays it if there are locations
+to display
+-->
+<div th:fragment="map" id="map-container" class="d-none">
+  <div id="map" class="border rounded"></div>
+  <div class="map-legend mt-1">
+    <img th:src="@{/assets/images/marker-icon-red.png}" id="red"/>
+    <label for="red" class="me-2">Origin site</label>
+    <img th:src="@{/assets/images/marker-icon-blue.png}" id="blue"/>
+    <label for="blue" class="me-2">Collecting site</label>
+    <img th:src="@{/assets/images/marker-icon-green.png}" id="green"/>
+    <label for="green" class="me-2">Evaluation site</label>
+    <img th:src="@{/assets/images/marker-icon-purple.png}" id="purple"/>
+    <label for="purple">Multi-purpose site</label>
+  </div>
+</div>
diff --git a/backend/src/main/resources/templates/germplasm.html b/backend/src/main/resources/templates/germplasm.html
index 1c51d43887a55f7e125dc46a85ae961de9d5b7bd..15ccd615a62c962ec5047510bef07a4cdd6a2652 100644
--- a/backend/src/main/resources/templates/germplasm.html
+++ b/backend/src/main/resources/templates/germplasm.html
@@ -2,7 +2,7 @@
 
 <html
   xmlns:th="http://www.thymeleaf.org"
-  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
 >
 <head>
   <title>Germplasm: <th:block th:text="${model.germplasm.germplasmName}" /></title>
@@ -18,6 +18,8 @@
     </div>
   </div>
 
+  <div th:replace="fragments/map::map"></div>
+
   <div class="row align-items-center justify-content-center">
     <div class="col-auto field" th:if="${model.germplasm.photo != null && model.germplasm.photo.thumbnailFile != null}">
       <template id="photo-popover">
@@ -414,5 +416,13 @@
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
+
+<script th:inline="javascript">
+  faidare.initializePopovers();
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /* TODO [[${model.mapLocations}]]*/ []
+  });
+</script>
 </body>
 </html>
diff --git a/backend/src/main/resources/templates/layout/main.html b/backend/src/main/resources/templates/layout/main.html
index 4cd33f7022d4752767011dba6f59cf6391bd8c4c..b428faaf870ff160e99a1db5c296c19e4240cf16 100644
--- a/backend/src/main/resources/templates/layout/main.html
+++ b/backend/src/main/resources/templates/layout/main.html
@@ -1,5 +1,5 @@
 <!DOCTYPE html>
-<html lang="fr" th:fragment="layout (title, content)" xmlns:th="http://www.thymeleaf.org">
+<html lang="fr" th:fragment="layout (title, content, script)" xmlns:th="http://www.thymeleaf.org">
   <head>
     <title th:replace="${title}">Layout Title</title>
 
@@ -8,6 +8,11 @@
 
     <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/css/bootstrap.min.css" rel="stylesheet" integrity="sha384-KyZXEAg3QhqLMpG8r+8fhAXLRk2vvoC2f3B09zVXn8CA5QIVfZOJ3BCsw2P0p/We" crossorigin="anonymous">
     <link th:href="@{/assets/style.css}" rel="stylesheet">
+    <link rel="stylesheet" href="https://unpkg.com/leaflet@1.7.1/dist/leaflet.css"
+          integrity="sha512-xodZBNTC5n17Xt2atTPuE1HxjVMSvLVW9ocqUKLsCC5CXdbqCmblAshOMAS6/keqq/sMZMZ19scR4PsZChSR7A=="
+          crossorigin=""/>
+    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.css" />
+    <link rel="stylesheet" href="https://unpkg.com/leaflet.markercluster@1.1.0/dist/MarkerCluster.Default.css" />
 
     <link rel="shortcut icon" th:href="@{/static/assets/images/favicon.ico}" type="image/x-icon" />
   </head>
@@ -27,26 +32,11 @@
       </footer>
     </div>
     <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.1.0/dist/js/bootstrap.bundle.min.js" integrity="sha384-U1DAWAznBHeqEIlVSCgzq+c9gqGAJn5c/t99JyeKa9xxaYpSvHU5awsuZVVFIhvj" crossorigin="anonymous"></script>
-    <script type="text/javascript">
-      const popoverTriggerList = [].slice.call(document.querySelectorAll('[data-bs-toggle="popover"]'))
-      popoverTriggerList.forEach(popoverTriggerEl => {
-        const options = {};
-        const contentSelector = popoverTriggerEl.dataset.bsElement;
-        if (contentSelector) {
-          const content = document.querySelector(contentSelector);
-          if (content) {
-            options.content = () => {
-              const element = document.createElement('div');
-              element.innerHTML = content.innerHTML;
-              return element;
-            };
-            options.html = true;
-          } else {
-            throw new Error('element with selector ' + contentSelector + ' not found');
-          }
-        }
-        return new bootstrap.Popover(popoverTriggerEl, options);
-      });
-    </script>
+    <script src="https://unpkg.com/leaflet@1.7.1/dist/leaflet.js"
+            integrity="sha512-XQoYMqMTK8LvdxXYG3nZ448hOEQiglfqkJs1NOQV44cWnUrBc8PkAOcXy20w0vlaXaVUearIOBhiXZ5V3ynxwA=="
+            crossorigin=""></script>
+    <script src="https://unpkg.com/leaflet.markercluster@1.1.0/dist/leaflet.markercluster.js"></script>
+    <script type="text/javascript" th:src="@{/assets/script.js}"></script>
+    <script type="text/javascript" th:replace="${script}"></script>
   </body>
 </html>
diff --git a/backend/src/main/resources/templates/site.html b/backend/src/main/resources/templates/site.html
index d5d65e5c598f80106ef4bc910b7184114ec40d5c..d859f4f29e5d6ec3a2f0803235d2922a286ee2bd 100644
--- a/backend/src/main/resources/templates/site.html
+++ b/backend/src/main/resources/templates/site.html
@@ -2,7 +2,7 @@
 
 <html
   xmlns:th="http://www.thymeleaf.org"
-  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
 >
 <head>
   <title>Site <th:block th:text="${model.site.locationName}" /></title>
@@ -13,6 +13,8 @@
 <main>
   <h1>Site <th:block th:text="${model.site.locationName}" /></h1>
 
+  <div th:replace="fragments/map::map"></div>
+
   <th:block th:if="${model.site.uri != null && !model.site.uri.startsWith('urn:')}">
     <div th:replace="fragments/row::text-row(label='Permanent unique identifier', text=${model.site.uri})"></div>
   </th:block>
@@ -58,5 +60,12 @@
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
+
+<script th:inline="javascript">
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /* TODO [[${model.mapLocations}]]*/ []
+  });
+</script>
 </body>
 </html>
diff --git a/backend/src/main/resources/templates/study.html b/backend/src/main/resources/templates/study.html
index 7bd5bbbd64fb18a2bff615b3f76e23e5153b3031..c2ee9a3ba0ec73b15de54a5861f15a288b4b174a 100644
--- a/backend/src/main/resources/templates/study.html
+++ b/backend/src/main/resources/templates/study.html
@@ -2,7 +2,7 @@
 
 <html
   xmlns:th="http://www.thymeleaf.org"
-  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main})}"
+  th:replace="~{layout/main :: layout(title=~{::title}, content=~{::main}, script=~{::script})}"
 >
 <head>
   <title>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></title>
@@ -13,6 +13,8 @@
 <main>
   <h1>Study <th:block th:text="${model.study.studyType}" />: <th:block th:text="${model.study.studyName}" /></h1>
 
+  <div th:replace="fragments/map::map"></div>
+
   <h2>Identification</h2>
 
   <div th:replace="fragments/row::text-row(label='Name', text=${model.study.studyName})"></div>
@@ -188,5 +190,12 @@
 
   <div th:replace="fragments/xrefs::xrefs(crossReferences=${model.crossReferences})"></div>
 </main>
+
+<script th:inline="javascript">
+  faidare.initializeMap({
+    contextPath: [[${#request.getContextPath()}]],
+    locations: /* TODO [[${model.mapLocations}]]*/ []
+  });
+</script>
 </body>
 </html>