How to Build an Impressive Web GIS Portfolio using Leaflet and GitHub Pages
Web GIS Tutorial · HTML · CSS · JavaScript · Leaflet.js · GitHub Pages
Why Build a Web GIS Portfolio using Leaflet and GitHub Pages
A strong Web GIS portfolio demonstrates more than your ability to produce a good-looking map. It shows that you can transform spatial data into an interactive application that people can explore through a web browser.
Many GIS students showcase static maps exported from QGIS or ArcGIS Pro. These are useful, but an interactive map demonstrates additional skills: web development, spatial data handling, user interface design, and publishing applications online.
With Leaflet and GitHub Pages, you can build and host a working Web GIS portfolio without paying for a web server. Your project can include interactive markers, pop-ups, layer controls, a custom legend, and a descriptive project page.
This Web GIS Portfolio Leaflet GitHub Pages tutorial will guide you through building a complete example: a public facilities map that displays sample locations, allows users to explore features, and can be shared through a live URL.
By the end, you will have a reusable foundation for showcasing geospatial projects to employers, clients, lecturers, and other GIS professionals.
1. Tools needed
You do not need an expensive development setup to get started.
HTML, CSS, and JavaScript
HTML structures the page, CSS controls its appearance, and JavaScript powers map interactions. These three technologies form the foundation of the application.
Leaflet.js
An open-source JavaScript library for interactive maps, markers, pop-ups, raster tile layers, and layer controls.

GitHub account
GitHub stores your code and provides version control. GitHub Pages can publish a static website directly from a repository.
QGIS (recommended)
Use QGIS to inspect spatial datasets, select relevant attributes, and export vector layers into GeoJSON for use in your web map.
You can write and test the application in a code editor such as VS Code. A modern web browser is sufficient for testing the initial project locally.
2. Step 1 — Create your project and HTML boilerplate
Start with a simple project directory containing the files needed to build the map.
web-gis-portfolio/
├── index.html
├── style.css
├── script.js
└── data/
└── facilities.geojson
The files have distinct responsibilities:
-
index.htmldefines the page structure and loads the required libraries. -
style.csscontrols the layout, typography, legend, and responsive design. -
script.jscreates the map and implements its interactions. -
facilities.geojsonstores the spatial features displayed on the map.
2.1 Create index.html
Create a new file named index.html and insert the following complete HTML boilerplate.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Public Facilities | Web GIS Portfolio</title>
<!-- Leaflet stylesheet -->
<link
rel="stylesheet"
href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
>
<!-- Your custom styles -->
<link rel="stylesheet" href="style.css">
</head>
<body>
<header class="site-header">
<div>
<p class="eyebrow">GEOSPATIAL PORTFOLIO PROJECT</p>
<h1>Public Facilities Explorer</h1>
<p>Explore facility locations through an interactive web map.</p>
</div>
</header>
<main>
<section class="map-section" aria-label="Interactive map">
<div id="map"></div>
<div id="map-status" class="map-status" role="status">
Loading map data...
</div>
</section>
<section class="project-info">
<h2>About this project</h2>
<p>
This Web GIS application demonstrates spatial data
visualization, interactive pop-ups, and layer controls
using Leaflet and GeoJSON.
</p>
<p class="tech-stack">
Built with HTML, CSS, JavaScript, Leaflet, and GeoJSON.
</p>
</section>
</main>
<!-- Leaflet JavaScript -->
<script
src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
></script>
<!-- Your map logic -->
<script src="script.js"></script>
</body>
</html>
Why this structure matters: The #map element is the container Leaflet will turn into an interactive map. The stylesheet and JavaScript library must load before your custom map code runs.
The example uses a pinned Leaflet version so the dependencies remain predictable during development.
3. Step 2 — Initialize the map and add basemaps
A basemap provides geographical context for your data. For example, a public facilities map becomes easier to interpret when users can see nearby roads, settlements, and other reference features.
Leaflet supports raster tile layers from compatible map providers. In this tutorial, we will use OpenStreetMap as the default basemap and CARTO’s light basemap as an alternative.
3.1 Initialize Leaflet
Open script.js and add the following code.
const map = L.map("map").setView([-1.2864, 36.8172], 11);
// OpenStreetMap basemap
const osmLayer = L.tileLayer(
"https://tile.openstreetmap.org/{z}/{x}/{y}.png",
{
maxZoom: 19,
attribution:
'© <a href="https://www.openstreetmap.org/copyright">' +
"OpenStreetMap</a> contributors"
}
).addTo(map);
// CARTO light basemap
const cartoLayer = L.tileLayer(
"https://basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png",
{
maxZoom: 20,
attribution:
'© OpenStreetMap contributors © <a href="https://carto.com/attributions">CARTO</a>'
}
);
// Allow users to switch basemaps
const baseMaps = {
"OpenStreetMap": osmLayer,
"CARTO Light": cartoLayer
};
L.control.layers(baseMaps, null, {
collapsed: false
}).addTo(map);
The initial coordinates point to Nairobi, Kenya. You can change the coordinates and zoom level to match your study area.
The key components are:
-
L.map("map")creates the map inside the HTML element with IDmap. -
setView()sets the initial centre and zoom. -
L.tileLayer()loads map tiles from a provider. -
L.control.layers()creates an interactive basemap selector.
Tile usage reminder: OpenStreetMap’s standard tile service has a usage policy. Keep its required attribution visible, avoid bulk downloading or prefetching tiles, and use a provider appropriate for your expected traffic. Review OpenStreetMap’s tile usage policy before publishing. Check the terms of any alternative basemap provider as well.
4. Step 3 — Convert spatial data to GeoJSON
GeoJSON is a convenient format for web mapping because it represents geographic features and their attributes in a JSON structure that JavaScript can read directly.
You can export points, lines, or polygons from QGIS and display them as interactive Leaflet layers.
4.1 Prepare the data in QGIS
For this example, prepare a point layer containing public facilities. Your attribute table could include fields such as:
| Field | Example |
|---|---|
name |
Community Health Centre |
type |
Health |
ward |
Example Ward |
status |
Operational |
These are example values, not verified facility records. Use genuine, appropriately licensed data when creating a real portfolio project.
In QGIS:
-
Load your vector dataset.
-
Check the geometry and attribute fields.
-
Right-click the layer in the Layers panel.
-
Select Export → Save Features As.
-
Set the format to GeoJSON.
-
Select EPSG:4326 — WGS 84 as the CRS for this example.
-
Save the file as
facilities.geojsoninside your project’sdatadirectory.
GeoJSON coordinates use longitude first and latitude second. When converting a layer from a projected CRS, reproject it during export rather than simply changing the CRS label.
4.2 Understand the GeoJSON structure
A GeoJSON file containing point features has a structure similar to this:
{
"type": "FeatureCollection",
"features": [
{
"type": "Feature",
"properties": {
"name": "Sample Health Centre",
"type": "Health",
"ward": "Example Ward",
"status": "Operational"
},
"geometry": {
"type": "Point",
"coordinates": [36.8172, -1.2864]
}
}
]
}
The coordinates represent longitude and latitude in that order. The record is illustrative and should not be treated as a real facility location.
4.3 Load GeoJSON into Leaflet
Add the following to script.js, after your basemap code.
let facilitiesLayer;
fetch("data/facilities.geojson")
.then(response => {
if (!response.ok) {
throw new Error(
`Could not load GeoJSON: ${response.status}`
);
}
return response.json();
})
.then(data => {
facilitiesLayer = L.geoJSON(data, {
pointToLayer: (feature, latlng) => {
return L.circleMarker(latlng, {
radius: 7,
fillColor: "#0f766e",
color: "#ffffff",
weight: 2,
opacity: 1,
fillOpacity: 0.9
});
},
onEachFeature: (feature, layer) => {
const properties = feature.properties || {};
const popupContent = document.createElement("div");
const heading = document.createElement("h3");
heading.textContent = properties.name || "Unnamed facility";
const type = document.createElement("p");
type.textContent = `Type: ${properties.type || "Not specified"}`;
const ward = document.createElement("p");
ward.textContent = `Ward: ${properties.ward || "Not specified"}`;
const status = document.createElement("p");
status.textContent = `Status: ${properties.status || "Not specified"}`;
popupContent.append(heading, type, ward, status);
layer.bindPopup(popupContent);
}
}).addTo(map);
const bounds = facilitiesLayer.getBounds();
if (bounds.isValid()) {
map.fitBounds(bounds, { padding: [30, 30] });
}
document.getElementById("map-status").textContent =
`${data.features.length} features loaded`;
})
.catch(error => {
console.error("GeoJSON loading failed:", error);
document.getElementById("map-status").textContent =
"Map data could not be loaded. Check the file path and browser console.";
});
This implementation creates styled point markers, builds a pop-up from each feature’s attributes, zooms to the data extent, and reports loading errors.
Using DOM elements and textContent for attribute values is preferable to inserting untrusted data directly into HTML strings. This helps prevent data-driven HTML injection.
Important for local testing: fetch() may fail if you open index.html directly using a file:// URL. Run the project through a local web server instead. For example, if Python is installed, open a terminal in your project folder and run:
python -m http.server 8000
Then open http://localhost:8000 in your browser. If the Python command is unavailable, use a local development server provided by your code editor.
For line and polygon datasets, L.geoJSON() can render the geometry directly. You can also style each feature based on its properties using a style function.
5. Step 4 — Design a professional map interface
A portfolio project should feel like a small, finished application rather than a default map embedded on a blank webpage.
Use CSS to create a consistent title banner, clear map boundaries, a compact legend, readable typography, and a layout that adapts to mobile screens.
5.1 Style the page using CSS
Open style.css and add the following complete stylesheet.
:root {
--navy: #102a43;
--teal: #0f766e;
--background: #f3f6f8;
--text: #243b53;
--muted: #627d98;
--white: #ffffff;
}
* {
box-sizing: border-box;
}
body {
margin: 0;
font-family: Arial, Helvetica, sans-serif;
background: var(--background);
color: var(--text);
line-height: 1.6;
}
.site-header {
background: var(--navy);
color: var(--white);
padding: 30px 6%;
}
.site-header h1 {
margin: 4px 0 8px;
font-size: clamp(1.7rem, 4vw, 2.7rem);
line-height: 1.2;
}
.site-header p {
margin: 0;
color: #d9e8f2;
}
.eyebrow {
font-size: 0.75rem;
letter-spacing: 0.13em;
font-weight: bold;
}
main {
width: min(1200px, 92%);
margin: 28px auto;
}
.map-section {
position: relative;
background: var(--white);
padding: 12px;
border-radius: 14px;
box-shadow: 0 5px 22px rgba(16, 42, 67, 0.08);
}
#map {
height: 560px;
width: 100%;
border-radius: 9px;
}
.map-status {
padding: 8px 4px 0;
color: var(--muted);
font-size: 0.85rem;
}
.project-info {
background: var(--white);
margin-top: 22px;
padding: 24px;
border-radius: 14px;
}
.project-info h2 {
margin-top: 0;
color: var(--navy);
}
.tech-stack {
color: var(--muted);
font-size: 0.9rem;
}
.leaflet-popup-content h3 {
margin: 0 0 10px;
color: var(--navy);
}
.leaflet-popup-content p {
margin: 5px 0;
}
@media (max-width: 600px) {
.site-header {
padding: 24px 5%;
}
main {
width: 94%;
margin: 16px auto;
}
#map {
height: 420px;
}
.project-info {
padding: 18px;
}
}
This stylesheet establishes a dark title banner, a clean map card, a readable project description, and responsive spacing. The clamp() function scales the title according to screen width, while the media query adjusts the map height on smaller screens.
5.2 Add a custom legend
A legend explains the meaning of symbols on the map. This becomes especially important when your project contains multiple facility types or categories.
Add this HTML inside the .map-section element, immediately after the #map element:
<div class="map-legend">
<h3>Facility Types</h3>
<div class="legend-item">
<span class="legend-dot health"></span>
Health facility
</div>
<div class="legend-item">
<span class="legend-dot education"></span>
Education facility
</div>
<div class="legend-item">
<span class="legend-dot other"></span>
Other facility
</div>
</div>
Add these rules to style.css:
.map-legend {
position: absolute;
z-index: 1000;
bottom: 42px;
left: 22px;
width: 190px;
padding: 14px;
border-radius: 9px;
background: rgba(255, 255, 255, 0.96);
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.15);
font-size: 0.85rem;
}
.map-legend h3 {
margin: 0 0 10px;
font-size: 0.95rem;
}
.legend-item {
display: flex;
align-items: center;
gap: 9px;
margin: 7px 0;
}
.legend-dot {
width: 12px;
height: 12px;
display: inline-block;
flex-shrink: 0;
border-radius: 50%;
border: 1px solid #ffffff;
}
.health {
background: #0f766e;
}
.education {
background: #2563eb;
}
.other {
background: #d97706;
}
@media (max-width: 600px) {
.map-legend {
width: 160px;
font-size: 0.78rem;
}
}
The legend currently describes three categories, while the earlier marker code gives every facility the same colour. To make the legend accurate, update pointToLayer to use the facility’s type attribute.
Replace the existing pointToLayer function with this version:
pointToLayer: (feature, latlng) => {
const type = (feature.properties?.type || "")
.toLowerCase();
let color = "#d97706";
if (type === "health") {
color = "#0f766e";
} else if (type === "education") {
color = "#2563eb";
}
return L.circleMarker(latlng, {
radius: 7,
fillColor: color,
color: "#ffffff",
weight: 2,
fillOpacity: 0.9
});
},
Now the marker colours correspond to the legend categories. Ensure that the actual values in your GeoJSON match the categories used in your code.
5.3 Improve layer controls and usability
The basemap selector is already functional. You can also add thematic overlays as additional entries in the second argument to L.control.layers().
For example, if your JavaScript defines an additional layer named facilitiesLayer, you can register it like this after the GeoJSON layer has loaded:
L.control.layers(
{
"OpenStreetMap": osmLayer,
"CARTO Light": cartoLayer
},
{
"Public facilities": facilitiesLayer
},
{
collapsed: true
}
).addTo(map);
Avoid adding this second control if you already have a control with the same overlays registered. Instead, modify your existing control or create one after the GeoJSON layer has loaded.
A polished interface should also:
-
Use readable pop-ups and consistent marker sizes.
-
Keep controls from covering important map features.
-
Display an informative loading or error message.
-
Work on both desktop and mobile screens.
-
Include a short explanation of the dataset, methods, and limitations.
6. Step 5 — Deploy your Web GIS portfolio using GitHub Pages
GitHub Pages can host your HTML, CSS, JavaScript, and GeoJSON files as a static website. For a small student project, this is one of the easiest ways to publish a working interactive map for free.
6.1 Create a GitHub repository
-
Open GitHub and sign in or create an account.
-
Click the + icon and select New repository.
-
Enter a repository name such as
web-gis-portfolio. -
Select Public if you want employers to view the source code easily.
-
Create the repository.
If you want to host multiple portfolio projects, you can create a main portfolio website and keep individual mapping projects in separate repositories.
6.2 Upload your project files
For a beginner, uploading through the GitHub website is sufficient.
-
Open your new repository.
-
Select Add file → Upload files.
-
Upload
index.html,style.css, andscript.js. -
Upload the
datafolder and itsfacilities.geojsonfile, preserving the directory structure. -
Add a
README.mddescribing the project. -
Commit the files to the repository.
Your repository should look like this:
web-gis-portfolio/
├── index.html
├── style.css
├── script.js
├── README.md
└── data/
└── facilities.geojson
Critical: GitHub Pages serves files at a URL path, so your relative file references must match the repository structure exactly. A file saved as Facilities.geojson will not necessarily load when your code requests facilities.geojson, especially on a case-sensitive hosting environment.
6.3 Enable GitHub Pages
Follow these steps:
-
Open the repository’s Settings.
-
Select Pages from the navigation menu.
-
Under Build and deployment, choose Deploy from a branch.
-
Select the
mainbranch and the/ (root)folder. -
Click Save.
GitHub will build and publish the site. It may take a few minutes for the first deployment to become available, so the setup is quick but not guaranteed to finish in under two minutes.
Your site will normally be available at a URL similar to:
https://YOUR-USERNAME.github.io/web-gis-portfolio/
Replace YOUR-USERNAME with your GitHub username.
For exact instructions, consult the official GitHub Pages setup guide.
6.4 Test the published map
Open the live URL in a private or incognito browser window and check:
-
The map loads and displays the expected study area.
-
Both basemaps work.
-
The GeoJSON data loads without a 404 error.
-
Markers and pop-ups work correctly.
-
The legend matches the marker colours.
-
The layout remains usable on a mobile screen.
-
Attribution is visible and readable.
If the map loads but the data does not, open the browser’s Developer Tools and inspect the Console and Network tabs. Look for missing files, incorrect paths, JavaScript errors, or blocked requests.
GitHub Pages is a static hosting service. It does not run Python scripts or provide a database backend directly. If a future project requires authentication, server-side processing, or a live database connection, you will need an appropriate additional service.
7. Checklist — What employers look for in a student Web GIS portfolio
Publishing a live map is a great first step. A competitive portfolio also explains what problem you solved, how you handled the data, and why your results matter.
Portfolio readiness
0/9
Live project URL
The website opens reliably and works outside your development computer.
Clear problem statement
The project identifies a real spatial question or decision-making need.
Data provenance
Datasets, sources, licenses, dates, and limitations are documented.
GIS methodology
The workflow explains data cleaning, spatial analysis, CRS choices, and assumptions.
Interactive functionality
Pop-ups, legends, layer controls, and navigation work as intended.
Professional interface
The design is readable, responsive, accessible, and consistent.
Readable source code
Files are organized, variables are descriptive, and the repository includes a README.
Evidence of analytical thinking
Maps are accompanied by findings, interpretation, and appropriate caveats.
Project ownership
The portfolio distinguishes your own work from external datasets, libraries, and contributions.
Portfolio projects worth building next
Once the facilities map works, consider expanding your portfolio with projects that demonstrate different GIS skills:
1. Public service accessibility map
Map health facilities or schools and analyze which communities have limited access. This demonstrates spatial analysis and communication.
2. Land-use change explorer
Display land-cover categories or change between two dates, with a legend and an explanation of the methodology.
3. Environmental risk dashboard
Combine spatial layers to communicate flood exposure, drainage conditions, or another locally relevant environmental issue.
Choose projects that you can explain confidently in an interview. One well-documented application with a clear analytical purpose is often more valuable than several unfinished maps.
Summary
Building a Web GIS portfolio with Leaflet and GitHub Pages gives you a practical way to demonstrate both GIS knowledge and web development skills without paying for hosting.
The complete workflow is:
-
Create the HTML, CSS, and JavaScript files.
-
Initialize a Leaflet map and configure basemaps.
-
Export spatial data from QGIS into GeoJSON.
-
Add interactive markers, pop-ups, legends, and layer controls.
-
Publish the project using GitHub Pages.
-
Document the project, test the live site, and explain the spatial insights it provides.
Career advice: Treat every portfolio project as a demonstration of your problem-solving ability, not merely your ability to display a map. Explain the question you investigated, where your data came from, which GIS methods you used, what you discovered, and what limitations remain.
Your first goal should be to publish one complete, working map. You can then build on the same foundation to learn spatial analysis, advanced JavaScript, web mapping APIs, and eventually full-stack Web GIS development.