{"id":1203,"date":"2026-10-20T07:32:28","date_gmt":"2026-10-20T07:32:28","guid":{"rendered":"https:\/\/helpwithmygisassignment.com\/blog\/?p=1203"},"modified":"2026-10-20T07:32:28","modified_gmt":"2026-10-20T07:32:28","slug":"publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers","status":"publish","type":"post","link":"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/","title":{"rendered":"Publishing PostGIS Layers to the Web Using GeoServer and OpenLayers"},"content":{"rendered":"<div><\/div>\n<div>\n<div id=\"ez-toc-container\" class=\"ez-toc-v2_0_89 counter-hierarchy ez-toc-counter ez-toc-grey ez-toc-container-direction\">\n<div class=\"ez-toc-title-container\">\n<p class=\"ez-toc-title\" style=\"cursor:inherit\">Table of Contents<\/p>\n<span class=\"ez-toc-title-toggle\"><a href=\"#\" class=\"ez-toc-pull-right ez-toc-btn ez-toc-btn-xs ez-toc-btn-default ez-toc-toggle\" aria-label=\"Toggle Table of Content\"><span class=\"ez-toc-js-icon-con\"><span class=\"\"><span class=\"eztoc-hide\" style=\"display:none;\">Toggle<\/span><span class=\"ez-toc-icon-toggle-span\"><svg style=\"fill: #999;color:#999\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" class=\"list-377408\" width=\"20px\" height=\"20px\" viewBox=\"0 0 24 24\" fill=\"none\"><path d=\"M6 6H4v2h2V6zm14 0H8v2h12V6zM4 11h2v2H4v-2zm16 0H8v2h12v-2zM4 16h2v2H4v-2zm16 0H8v2h12v-2z\" fill=\"currentColor\"><\/path><\/svg><svg style=\"fill: #999;color:#999\" class=\"arrow-unsorted-368013\" xmlns=\"http:\/\/www.w3.org\/2000\/svg\" width=\"10px\" height=\"10px\" viewBox=\"0 0 24 24\" version=\"1.2\" baseProfile=\"tiny\"><path d=\"M18.2 9.3l-6.2-6.3-6.2 6.3c-.2.2-.3.4-.3.7s.1.5.3.7c.2.2.4.3.7.3h11c.3 0 .5-.1.7-.3.2-.2.3-.5.3-.7s-.1-.5-.3-.7zM5.8 14.7l6.2 6.3 6.2-6.3c.2-.2.3-.5.3-.7s-.1-.5-.3-.7c-.2-.2-.4-.3-.7-.3h-11c-.3 0-.5.1-.7.3-.2.2-.3.5-.3.7s.1.5.3.7z\"\/><\/svg><\/span><\/span><\/span><\/a><\/span><\/div>\n<nav><ul class='ez-toc-list ez-toc-list-level-1 ' ><li class='ez-toc-page-1 ez-toc-heading-level-1'><a class=\"ez-toc-link ez-toc-heading-1\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Publishing_PostGIS_Layers_to_the_Web_Using_GeoServer_and_OpenLayers\" >Publishing PostGIS Layers to the Web Using GeoServer and OpenLayers<\/a><ul class='ez-toc-list-level-2' ><li class='ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-2\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Architecture_for_Publishing_PostGIS_Layers_to_the_Web_Using_GeoServer_and_OpenLayers\" >Architecture for Publishing PostGIS Layers to the Web Using GeoServer and OpenLayers<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-3\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Prerequisites_and_example_dataset\" >Prerequisites and example dataset<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-4\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Step_1_Connect_PostGIS_to_GeoServer\" >Step 1: Connect PostGIS to GeoServer<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-5\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#11_Prepare_the_spatial_table\" >1.1 Prepare the spatial table<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-6\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#12_Create_a_dedicated_database_user\" >1.2 Create a dedicated database user<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-7\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#13_Create_a_workspace_in_GeoServer\" >1.3 Create a workspace in GeoServer<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-8\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#14_Configure_the_PostGIS_Data_Store\" >1.4 Configure the PostGIS Data Store<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-9\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#15_Publish_the_vector_layer\" >1.5 Publish the vector layer<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-10\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#16_Understand_the_WMS_and_WFS_endpoints\" >1.6 Understand the WMS and WFS endpoints<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-11\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Step_2_Style_the_layer_using_SLD\" >Step 2: Style the layer using SLD<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-12\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#21_Create_an_SLD_file\" >2.1 Create an SLD file<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-13\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#22_Upload_and_apply_the_style\" >2.2 Upload and apply the style<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-14\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Step_3_Build_the_OpenLayers_front_end\" >Step 3: Build the OpenLayers front end<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-15\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#31_Create_the_OpenLayers_project\" >3.1 Create the OpenLayers project<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-16\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#32_Create_the_HTML_page\" >3.2 Create the HTML page<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-17\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#33_Add_the_CSS\" >3.3 Add the CSS<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-18\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#34_Connect_OpenLayers_to_the_published_WMS_layer\" >3.4 Connect OpenLayers to the published WMS layer<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-19\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#35_Run_and_test_the_map\" >3.5 Run and test the map<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-20\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Step_4_Add_WFS_for_client-side_vector_interaction\" >Step 4: Add WFS for client-side vector interaction<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-21\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Step_5_Troubleshooting_common_problems\" >Step 5: Troubleshooting common problems<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-22\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Step_6_Security_and_production_deployment\" >Step 6: Security and production deployment<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-23\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Configure_CORS_deliberately\" >Configure CORS deliberately<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-24\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Restrict_service_and_layer_access\" >Restrict service and layer access<\/a><\/li><li class='ez-toc-page-1 ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-25\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Limit_expensive_requests\" >Limit expensive requests<\/a><\/li><\/ul><\/li><li class='ez-toc-page-1 ez-toc-heading-level-2'><a class=\"ez-toc-link ez-toc-heading-26\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Conclusion\" >Conclusion<\/a><ul class='ez-toc-list-level-3' ><li class='ez-toc-heading-level-3'><a class=\"ez-toc-link ez-toc-heading-27\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/#Official_documentation\" >Official documentation<\/a><\/li><\/ul><\/li><\/ul><\/li><\/ul><\/nav><\/div>\n<h1><span class=\"ez-toc-section\" id=\"Publishing_PostGIS_Layers_to_the_Web_Using_GeoServer_and_OpenLayers\"><\/span>Publishing PostGIS Layers to the Web Using GeoServer and OpenLayers<img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-1347\" src=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_cmhuh6cmhuh6cmhu-1024x559.jpg\" alt=\"Publishing PostGIS Layers to the Web Using GeoServer and OpenLayers\" width=\"1024\" height=\"559\" srcset=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_cmhuh6cmhuh6cmhu-1024x559.jpg 1024w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_cmhuh6cmhuh6cmhu-300x164.jpg 300w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_cmhuh6cmhuh6cmhu-768x419.jpg 768w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_cmhuh6cmhuh6cmhu.jpg 1408w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><span class=\"ez-toc-section-end\"><\/span><\/h1>\n<p>Technical tutorial \u00b7 Web GIS architecture \u00b7 PostgreSQL\/PostGIS, GeoServer, OpenLayers<\/p>\n<p>A web map is more than a map displayed in a browser. It is a connected system in which<a href=\"https:\/\/helpwithmygisassignment.com\/blog\/idw-vs-kriging-gis\/\"> spatial data<\/a> is stored in a database, exposed through a map server, styled for presentation, and rendered interactively by a front-end application.<\/p>\n<p>This GeoServer PostGIS OpenLayers web map tutorial walks through that complete workflow using open-source tools. You will connect a PostGIS database to GeoServer, publish a spatial layer through WMS and WFS, apply a Styled Layer Descriptor (SLD), and build a browser-based map with zoom, pan, and feature pop-ups.<\/p>\n<p>By the end, you will have the foundations of a reusable Web GIS application rather than a static map image.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Architecture_for_Publishing_PostGIS_Layers_to_the_Web_Using_GeoServer_and_OpenLayers\"><\/span>Architecture for Publishing PostGIS Layers to the Web Using GeoServer and OpenLayers<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<div>\n<div>\n<p>PostgreSQL + PostGIS<\/p>\n<p>Stores spatial tables, attributes, geometries and spatial indexes<\/p>\n<\/div>\n<div>\n<p>JDBC database connection<\/p>\n<\/div>\n<div>\n<p>GeoServer<\/p>\n<p>Publishes layers, applies styles and serves OGC web services<\/p>\n<div>\n<div>WMS<\/div>\n<div>WFS<\/div>\n<\/div>\n<\/div>\n<div>\n<p>HTTP requests and responses<\/p>\n<\/div>\n<div>\n<p>OpenLayers web application<\/p>\n<p>Renders maps, handles navigation and displays feature information<\/p>\n<\/div>\n<\/div>\n<p>Each component has a distinct responsibility:<\/p>\n<ul>\n<li>\n<div>\n<div>\n<p>PostGIS: Stores authoritative spatial data and supports spatial queries.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>GeoServer: Connects to the database and exposes layers as web services. WMS produces map images; WFS can expose vector features and their attributes.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>OpenLayers: Runs in the browser and requests the layers it needs to display or interact with.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<p>This separation lets you update the database without rebuilding the front-end map, change a layer&#8217;s appearance on the server, or reuse published services in other GIS clients.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Prerequisites_and_example_dataset\"><\/span>Prerequisites and example dataset<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>You need a working PostgreSQL installation with PostGIS enabled, a GeoServer instance, and a web server or development server for the front-end files.<\/p>\n<p>For this walkthrough, assume:<\/p>\n<div>\n<table>\n<tbody>\n<tr>\n<th>Setting<\/th>\n<th>Example value<\/th>\n<\/tr>\n<tr>\n<td>Database<\/td>\n<td><code>webgis<\/code><\/td>\n<\/tr>\n<tr>\n<td>Database schema<\/td>\n<td><code>public<\/code><\/td>\n<\/tr>\n<tr>\n<td>Spatial table<\/td>\n<td><code>public.city_assets<\/code><\/td>\n<\/tr>\n<tr>\n<td>Geometry column<\/td>\n<td><code>geom<\/code><\/td>\n<\/tr>\n<tr>\n<td>GeoServer workspace<\/td>\n<td><code>city<\/code><\/td>\n<\/tr>\n<tr>\n<td>Published layer<\/td>\n<td><code>city:city_assets<\/code><\/td>\n<\/tr>\n<tr>\n<td>Front-end<\/td>\n<td>HTML, CSS and JavaScript with OpenLayers<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>These are illustrative names. Replace them with the values in your environment. The examples assume the layer contains point features and uses a known coordinate reference system.<\/p>\n<p>If you already have a suitable PostGIS table, use it. Otherwise, create a small sample table and populate it with test features before proceeding.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Step_1_Connect_PostGIS_to_GeoServer\"><\/span>Step 1: Connect PostGIS to GeoServer<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<h3><span class=\"ez-toc-section\" id=\"11_Prepare_the_spatial_table\"><\/span>1.1 Prepare the spatial table<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>GeoServer needs a database table with valid geometry metadata and a coordinate reference system (CRS).<\/p>\n<p>For a sample point layer, the following SQL creates a table of city assets. Run it in the <code>webgis<\/code> database using pgAdmin or <code>psql<\/code>.<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>CREATE EXTENSION IF NOT EXISTS postgis;\r\n\r\nCREATE TABLE public.city_assets (\r\n    asset_id SERIAL PRIMARY KEY,\r\n    asset_name VARCHAR(100) NOT NULL,\r\n    asset_type VARCHAR(50),\r\n    geom geometry(Point, 4326)\r\n);\r\n\r\nINSERT INTO public.city_assets\r\n    (asset_name, asset_type, geom)\r\nVALUES\r\n    (\r\n        'City Hall',\r\n        'Government',\r\n        ST_SetSRID(ST_MakePoint(36.8219, -1.2921), 4326)\r\n    ),\r\n    (\r\n        'Central Hospital',\r\n        'Healthcare',\r\n        ST_SetSRID(ST_MakePoint(36.8070, -1.2864), 4326)\r\n    ),\r\n    (\r\n        'Railway Station',\r\n        'Transport',\r\n        ST_SetSRID(ST_MakePoint(36.8250, -1.3010), 4326)\r\n    );\r\n\r\nCREATE INDEX city_assets_geom_idx\r\nON public.city_assets\r\nUSING GIST (geom);\r\n\r\nANALYZE public.city_assets;<\/code><\/pre>\n<\/div>\n<\/div>\n<p>The coordinates are illustrative locations in Nairobi, not a verified inventory of real facilities.<\/p>\n<p>The important parts are:<\/p>\n<ul>\n<li>\n<div>\n<div>\n<p><code>geometry(Point, 4326)<\/code> declares a point geometry in WGS 84.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><code>ST_SetSRID<\/code> assigns the CRS identifier to the supplied longitude and latitude coordinates. It does not transform coordinates.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>The GiST index helps PostgreSQL accelerate supported spatial queries.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><code>ANALYZE<\/code> updates table statistics used by the query planner.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<p>Check the result:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>SELECT\r\n    asset_id,\r\n    asset_name,\r\n    asset_type,\r\n    ST_AsText(geom) AS geometry,\r\n    ST_SRID(geom) AS srid\r\nFROM public.city_assets;<\/code><\/pre>\n<\/div>\n<\/div>\n<p>If you already have data, verify its geometry type, CRS, primary key and spatial index before publishing it.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"12_Create_a_dedicated_database_user\"><\/span>1.2 Create a dedicated database user<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Avoid connecting GeoServer using the PostgreSQL superuser account. Create a restricted login for reading the published layer:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>CREATE ROLE geoserver_reader\r\n    LOGIN PASSWORD 'replace-with-a-strong-password';\r\n\r\nGRANT CONNECT ON DATABASE webgis\r\n    TO geoserver_reader;\r\n\r\nGRANT USAGE ON SCHEMA public\r\n    TO geoserver_reader;\r\n\r\nGRANT SELECT ON public.city_assets\r\n    TO geoserver_reader;<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Replace the example password with a securely managed secret. In production, also consider default privileges for future tables and restrict database network access to the GeoServer application.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"13_Create_a_workspace_in_GeoServer\"><\/span>1.3 Create a workspace in GeoServer<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Open the GeoServer administration interface. In a local development environment, its URL is commonly:<\/p>\n<p><code>http:\/\/localhost:8080\/geoserver<\/code><\/p>\n<p>Your production URL will differ.<\/p>\n<ol>\n<li>\n<div>\n<div>\n<p>Sign in with an administrator account.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Navigate to Data \u2192 Workspaces.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Select Add new workspace.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Enter <code>city<\/code> as the workspace name.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Set a namespace URI, such as <code>https:\/\/example.org\/city<\/code>.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Save.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ol>\n<p>A workspace organizes related published resources. It also becomes part of the qualified layer name, <code>city:city_assets<\/code>.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"14_Configure_the_PostGIS_Data_Store\"><\/span>1.4 Configure the PostGIS Data Store<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>GeoServer&#8217;s PostGIS Data Store connects a workspace to the spatial database. The official GeoServer quick-start follows this same general workflow.<\/p>\n<p>Navigate to Data \u2192 Stores \u2192 Add new Store \u2192 PostGIS.<\/p>\n<p>Enter the connection settings:<\/p>\n<div>\n<table>\n<tbody>\n<tr>\n<th>Parameter<\/th>\n<th>Example value<\/th>\n<\/tr>\n<tr>\n<td>Workspace<\/td>\n<td><code>city<\/code><\/td>\n<\/tr>\n<tr>\n<td>Data source name<\/td>\n<td><code>webgis_postgis<\/code><\/td>\n<\/tr>\n<tr>\n<td>Host<\/td>\n<td>Database server hostname<\/td>\n<\/tr>\n<tr>\n<td>Port<\/td>\n<td><code>5432<\/code><\/td>\n<\/tr>\n<tr>\n<td>Database<\/td>\n<td><code>webgis<\/code><\/td>\n<\/tr>\n<tr>\n<td>Schema<\/td>\n<td><code>public<\/code><\/td>\n<\/tr>\n<tr>\n<td>User<\/td>\n<td><code>geoserver_reader<\/code><\/td>\n<\/tr>\n<tr>\n<td>Password<\/td>\n<td>Your configured secret<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>Important: If GeoServer and PostgreSQL run in separate containers, <code>localhost<\/code> inside the GeoServer container refers to that container\u2014not the database container. Use the database&#8217;s service name or reachable hostname.<\/p>\n<p>Save the store. If the connection fails, check the database credentials, PostgreSQL access rules, firewall, JDBC connectivity and server logs.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"15_Publish_the_vector_layer\"><\/span>1.5 Publish the vector layer<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Once the Data Store is saved:<\/p>\n<ol>\n<li>\n<div>\n<div>\n<p>Open the list of available layers for <code>webgis_postgis<\/code>.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Locate <code>city_assets<\/code> and choose Publish.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Set the declared and native CRS to <code>EPSG:4326<\/code> if the data genuinely uses WGS 84.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Under the bounding-box settings, use Compute from data and Compute from native bounds, where available.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Review the layer metadata and save.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ol>\n<p>The published layer should now be identified as:<\/p>\n<p><code>city:city_assets<\/code><\/p>\n<p>The layer name is case-sensitive in many practical configurations. Use the exact name shown in GeoServer when making service requests.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"16_Understand_the_WMS_and_WFS_endpoints\"><\/span>1.6 Understand the WMS and WFS endpoints<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>GeoServer commonly exposes both services through its workspace or global service URL.<\/p>\n<div>\n<div>\n<p>WMS \u2014 Web Map Service<\/p>\n<p>Returns rendered map images. Suitable for displaying styled data without sending all underlying feature geometries to the browser.<\/p>\n<p><code>\/geoserver\/city\/wms<\/code><\/p>\n<p>Example operation: <code>GetMap<\/code><\/p>\n<\/div>\n<div><\/div>\n<div>\n<p>WFS \u2014 Web Feature Service<\/p>\n<p>Returns vector features and attributes for client-side querying or analysis, subject to the server&#8217;s configuration and output-format support.<\/p>\n<p><code>\/geoserver\/city\/ows<\/code><\/p>\n<p>Example operation: <code>GetFeature<\/code><\/p>\n<\/div>\n<\/div>\n<p>You can test the services by opening these URLs in a browser, replacing the hostname and port as needed.<\/p>\n<p>WMS capabilities:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>http:\/\/localhost:8080\/geoserver\/city\/wms?service=WMS&amp;version=1.3.0&amp;request=GetCapabilities<\/code><\/pre>\n<\/div>\n<\/div>\n<p>WFS capabilities:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>http:\/\/localhost:8080\/geoserver\/city\/ows?service=WFS&amp;version=2.0.0&amp;request=GetCapabilities<\/code><\/pre>\n<\/div>\n<\/div>\n<p>A valid XML response describing available operations and layers indicates that the service endpoint is responding. It does not, by itself, prove that the individual layer is published correctly.<\/p>\n<p>Test WFS feature retrieval:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>http:\/\/localhost:8080\/geoserver\/city\/ows?service=WFS&amp;version=2.0.0&amp;request=GetFeature&amp;typeNames=city:city_assets&amp;outputFormat=application\/json<\/code><\/pre>\n<\/div>\n<\/div>\n<p>If JSON output is supported and the layer is accessible, the response should contain GeoJSON features. Avoid exposing unrestricted WFS access to large or sensitive datasets.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Step_2_Style_the_layer_using_SLD\"><\/span>Step 2: Style the layer using SLD<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>A spatial database stores geometry and attributes, not necessarily the visual design you want users to see. GeoServer&#8217;s Styled Layer Descriptor (SLD) lets you define server-side symbology, including symbol shapes, colors, outlines, labels and attribute-based rules.<\/p>\n<div>\n<div><\/div>\n<\/div>\n<h3><span class=\"ez-toc-section\" id=\"21_Create_an_SLD_file\"><\/span>2.1 Create an SLD file<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>For the sample point layer, create a file named <code>city_assets.sld<\/code>:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>&lt;?xml version=\"1.0\" encoding=\"UTF-8\"?&gt;\r\n&lt;StyledLayerDescriptor\r\n    version=\"1.0.0\"\r\n    xmlns=\"http:\/\/www.opengis.net\/sld\"\r\n    xmlns:ogc=\"http:\/\/www.opengis.net\/ogc\"\r\n    xmlns:xsi=\"http:\/\/www.w3.org\/2001\/XMLSchema-instance\"\r\n    xsi:schemaLocation=\"\r\n      http:\/\/www.opengis.net\/sld\r\n      http:\/\/schemas.opengis.net\/sld\/1.0.0\/StyledLayerDescriptor.xsd\"&gt;\r\n\r\n  &lt;NamedLayer&gt;\r\n    &lt;Name&gt;city:city_assets&lt;\/Name&gt;\r\n\r\n    &lt;UserStyle&gt;\r\n      &lt;Title&gt;City Assets&lt;\/Title&gt;\r\n      &lt;FeatureTypeStyle&gt;\r\n        &lt;Rule&gt;\r\n          &lt;Name&gt;Healthcare&lt;\/Name&gt;\r\n          &lt;ogc:Filter&gt;\r\n            &lt;ogc:PropertyIsEqualTo&gt;\r\n              &lt;ogc:PropertyName&gt;asset_type&lt;\/ogc:PropertyName&gt;\r\n              &lt;ogc:Literal&gt;Healthcare&lt;\/ogc:Literal&gt;\r\n            &lt;\/ogc:PropertyIsEqualTo&gt;\r\n          &lt;\/ogc:Filter&gt;\r\n\r\n          &lt;PointSymbolizer&gt;\r\n            &lt;Graphic&gt;\r\n              &lt;Mark&gt;\r\n                &lt;WellKnownName&gt;circle&lt;\/WellKnownName&gt;\r\n                &lt;Fill&gt;\r\n                  &lt;CssParameter name=\"fill\"&gt;#D94841&lt;\/CssParameter&gt;\r\n                &lt;\/Fill&gt;\r\n                &lt;Stroke&gt;\r\n                  &lt;CssParameter name=\"stroke\"&gt;#FFFFFF&lt;\/CssParameter&gt;\r\n                  &lt;CssParameter name=\"stroke-width\"&gt;1.5&lt;\/CssParameter&gt;\r\n                &lt;\/Stroke&gt;\r\n              &lt;\/Mark&gt;\r\n              &lt;Size&gt;12&lt;\/Size&gt;\r\n            &lt;\/Graphic&gt;\r\n          &lt;\/PointSymbolizer&gt;\r\n        &lt;\/Rule&gt;\r\n\r\n        &lt;Rule&gt;\r\n          &lt;Name&gt;Other assets&lt;\/Name&gt;\r\n          &lt;ElseFilter\/&gt;\r\n          &lt;PointSymbolizer&gt;\r\n            &lt;Graphic&gt;\r\n              &lt;Mark&gt;\r\n                &lt;WellKnownName&gt;circle&lt;\/WellKnownName&gt;\r\n                &lt;Fill&gt;\r\n                  &lt;CssParameter name=\"fill\"&gt;#247BA0&lt;\/CssParameter&gt;\r\n                &lt;\/Fill&gt;\r\n                &lt;Stroke&gt;\r\n                  &lt;CssParameter name=\"stroke\"&gt;#FFFFFF&lt;\/CssParameter&gt;\r\n                  &lt;CssParameter name=\"stroke-width\"&gt;1.5&lt;\/CssParameter&gt;\r\n                &lt;\/Stroke&gt;\r\n              &lt;\/Mark&gt;\r\n              &lt;Size&gt;9&lt;\/Size&gt;\r\n            &lt;\/Graphic&gt;\r\n          &lt;\/PointSymbolizer&gt;\r\n        &lt;\/Rule&gt;\r\n      &lt;\/FeatureTypeStyle&gt;\r\n    &lt;\/UserStyle&gt;\r\n  &lt;\/NamedLayer&gt;\r\n&lt;\/StyledLayerDescriptor&gt;<\/code><\/pre>\n<\/div>\n<\/div>\n<p>The first rule selects features whose <code>asset_type<\/code> is <code>Healthcare<\/code> and renders them as larger red circles. The <code>ElseFilter<\/code> rule styles features that did not match that rule.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"22_Upload_and_apply_the_style\"><\/span>2.2 Upload and apply the style<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>In GeoServer:<\/p>\n<ol>\n<li>\n<div>\n<div>\n<p>Open Data \u2192 Styles \u2192 Add a new style.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Name the style <code>city_assets_style<\/code>.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Upload the SLD file or paste its XML into the editor.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Validate the style and save it.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Open Data \u2192 Layers \u2192 city:city_assets.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Under the publishing or layer settings, select <code>city_assets_style<\/code> as the default style and save.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ol>\n<p>The exact placement of style controls can vary slightly by GeoServer version.<\/p>\n<p>Preview the layer using the GeoServer layer preview page. If the style fails to validate, inspect the XML namespaces, element nesting, property names and filter values.<\/p>\n<p>When should you use GeoServer CSS instead? GeoServer supports a CSS-style extension for people who prefer a shorter, more familiar syntax. It is not the same as the browser&#8217;s CSS, and it requires the appropriate GeoServer extension. SLD is the better baseline for this tutorial because it is supported by default.<\/p>\n<div>\n<div><\/div>\n<\/div>\n<h2><span class=\"ez-toc-section\" id=\"Step_3_Build_the_OpenLayers_front_end\"><\/span>Step 3: Build the OpenLayers front end<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p><img loading=\"lazy\" decoding=\"async\" class=\"aligncenter size-large wp-image-1348\" src=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_di0830di0830di08-1024x559.jpg\" alt=\"interactive web map OpenLayers GeoServer PostGIS\" width=\"1024\" height=\"559\" srcset=\"https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_di0830di0830di08-1024x559.jpg 1024w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_di0830di0830di08-300x164.jpg 300w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_di0830di0830di08-768x419.jpg 768w, https:\/\/helpwithmygisassignment.com\/blog\/wp-content\/uploads\/2026\/10\/Gemini_Generated_Image_di0830di0830di08.jpg 1408w\" sizes=\"auto, (max-width: 1024px) 100vw, 1024px\" \/><\/p>\n<p>Now that GeoServer exposes the layer, the browser needs a map application that can request and render it.<\/p>\n<p>We&#8217;ll use WMS for map rendering and WMS <code>GetFeatureInfo<\/code> for click-based pop-ups. This is a useful starting architecture because the browser receives rendered map images rather than downloading every feature geometry.<\/p>\n<p>OpenLayers supports both WMS rendering and <code>GetFeatureInfo<\/code> requests through its WMS source classes.<\/p>\n<p>&nbsp;<\/p>\n<h3><span class=\"ez-toc-section\" id=\"31_Create_the_OpenLayers_project\"><\/span>3.1 Create the OpenLayers project<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Install Node.js and npm, then run the official OpenLayers project setup:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>npm create ol-app city-webgis\r\ncd city-webgis\r\nnpm start<\/code><\/pre>\n<\/div>\n<\/div>\n<p>The starter project includes <code>index.html<\/code>, <code>main.js<\/code>, <code>style.css<\/code> and a development server.<\/p>\n<div>\n<div><\/div>\n<\/div>\n<p>Open the project in your code editor. We&#8217;ll replace the contents of those three files.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"32_Create_the_HTML_page\"><\/span>3.2 Create the HTML page<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>In <code>index.html<\/code>, provide a map container and a pop-up container:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>&lt;!doctype html&gt;\r\n&lt;html lang=\"en\"&gt;\r\n&lt;head&gt;\r\n  &lt;meta charset=\"utf-8\"&gt;\r\n  &lt;meta name=\"viewport\"\r\n        content=\"width=device-width, initial-scale=1\"&gt;\r\n  &lt;title&gt;City Assets Web GIS&lt;\/title&gt;\r\n&lt;\/head&gt;\r\n&lt;body&gt;\r\n  &lt;header class=\"app-header\"&gt;\r\n    &lt;h1&gt;City Assets Explorer&lt;\/h1&gt;\r\n    &lt;p&gt;Explore published PostGIS data&lt;\/p&gt;\r\n  &lt;\/header&gt;\r\n\r\n  &lt;main&gt;\r\n    &lt;div id=\"map\" aria-label=\"Interactive city map\"&gt;&lt;\/div&gt;\r\n\r\n    &lt;div id=\"popup\" class=\"ol-popup\" hidden&gt;\r\n      &lt;button id=\"popup-closer\"\r\n              class=\"popup-closer\"\r\n              aria-label=\"Close popup\"&gt;\u00d7&lt;\/button&gt;\r\n      &lt;div id=\"popup-content\"&gt;&lt;\/div&gt;\r\n    &lt;\/div&gt;\r\n\r\n    &lt;p id=\"status\" role=\"status\"&gt;\r\n      Loading map\u2026\r\n    &lt;\/p&gt;\r\n  &lt;\/main&gt;\r\n\r\n  &lt;script type=\"module\" src=\".\/main.js\"&gt;&lt;\/script&gt;\r\n&lt;\/body&gt;\r\n&lt;\/html&gt;<\/code><\/pre>\n<\/div>\n<\/div>\n<p>The <code>map<\/code> element is where OpenLayers renders the map. The pop-up is a separate HTML element positioned over the map when a user selects a feature.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"33_Add_the_CSS\"><\/span>3.3 Add the CSS<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>In <code>style.css<\/code>, define the page layout and pop-up appearance:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>@import \"ol\/ol.css\";\r\n\r\nhtml,\r\nbody {\r\n  margin: 0;\r\n  min-height: 100%;\r\n  font-family: Arial, sans-serif;\r\n  color: #1f2937;\r\n}\r\n\r\n.app-header {\r\n  padding: 16px 22px;\r\n  background: #17324d;\r\n  color: white;\r\n}\r\n\r\n.app-header h1 {\r\n  margin: 0 0 6px;\r\n  font-size: 1.35rem;\r\n}\r\n\r\n.app-header p {\r\n  margin: 0;\r\n  font-size: 0.9rem;\r\n}\r\n\r\n#map {\r\n  width: 100%;\r\n  height: 72vh;\r\n  min-height: 420px;\r\n}\r\n\r\n#status {\r\n  padding: 0 16px;\r\n  font-size: 0.9rem;\r\n}\r\n\r\n.ol-popup {\r\n  position: absolute;\r\n  min-width: 190px;\r\n  max-width: 280px;\r\n  padding: 14px;\r\n  background: white;\r\n  border: 1px solid #d1d5db;\r\n  border-radius: 8px;\r\n  box-shadow: 0 3px 14px #0002;\r\n  bottom: 12px;\r\n  left: -50px;\r\n}\r\n\r\n.ol-popup[hidden] {\r\n  display: none;\r\n}\r\n\r\n.popup-closer {\r\n  float: right;\r\n  border: 0;\r\n  background: transparent;\r\n  font-size: 1.3rem;\r\n  cursor: pointer;\r\n}\r\n\r\n.popup-title {\r\n  margin: 0 24px 8px 0;\r\n}\r\n\r\n.popup-row {\r\n  margin: 5px 0;\r\n  overflow-wrap: anywhere;\r\n}<\/code><\/pre>\n<\/div>\n<\/div>\n<p>The <code>ol\/ol.css<\/code> import supplies OpenLayers&#8217; built-in controls and map styling.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"34_Connect_OpenLayers_to_the_published_WMS_layer\"><\/span>3.4 Connect OpenLayers to the published WMS layer<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Replace <code>main.js<\/code> with the following code.<\/p>\n<p>This implementation:<\/p>\n<ul>\n<li>\n<div>\n<div>\n<p>Displays the published GeoServer layer.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Starts the map over Nairobi.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Supports built-in zoom and drag-to-pan navigation.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Requests feature information when a user clicks the map.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Displays selected attributes using safe DOM text insertion.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Handles failed requests without silently pretending that the map is working.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<div>\n<div>\n<pre dir=\"ltr\"><code>import Map from 'ol\/Map.js';\r\nimport View from 'ol\/View.js';\r\nimport TileLayer from 'ol\/layer\/Tile.js';\r\nimport TileWMS from 'ol\/source\/TileWMS.js';\r\nimport Overlay from 'ol\/Overlay.js';\r\nimport { fromLonLat } from 'ol\/proj.js';\r\n\r\nimport '.\/style.css';\r\n\r\n\/\/ Replace this with your accessible GeoServer URL.\r\n\/\/ Use HTTPS in production.\r\nconst geoserverUrl =\r\n  'http:\/\/localhost:8080\/geoserver\/city\/wms';\r\n\r\nconst layerName = 'city:city_assets';\r\n\r\nconst status = document.getElementById('status');\r\nconst popup = document.getElementById('popup');\r\nconst popupContent = document.getElementById('popup-content');\r\nconst popupCloser = document.getElementById('popup-closer');\r\n\r\n\/\/ Configure the WMS source.\r\nconst wmsSource = new TileWMS({\r\n  url: geoserverUrl,\r\n  params: {\r\n    LAYERS: layerName,\r\n    TILED: true,\r\n    FORMAT: 'image\/png',\r\n    TRANSPARENT: true\r\n  },\r\n  serverType: 'geoserver'\r\n});\r\n\r\n\/\/ Render the server-styled map.\r\nconst assetsLayer = new TileLayer({\r\n  source: wmsSource\r\n});\r\n\r\n\/\/ Configure the HTML popup.\r\nconst popupOverlay = new Overlay({\r\n  element: popup,\r\n  autoPan: {\r\n    animation: { duration: 200 }\r\n  },\r\n  positioning: 'bottom-center',\r\n  offset: [0, -12]\r\n});\r\n\r\n\/\/ Create the map.\r\nconst map = new Map({\r\n  target: 'map',\r\n  layers: [assetsLayer],\r\n  overlays: [popupOverlay],\r\n  view: new View({\r\n    center: fromLonLat([36.8219, -1.2921]),\r\n    zoom: 12\r\n  })\r\n});\r\n\r\n\/\/ Close the popup.\r\npopupCloser.addEventListener('click', () =&gt; {\r\n  popupOverlay.setPosition(undefined);\r\n  popup.hidden = true;\r\n});\r\n\r\n\/\/ Report that the map has initialized.\r\n\/\/ This does not guarantee that the WMS request succeeded.\r\nstatus.textContent =\r\n  'Map initialized. Click a city asset to inspect its attributes.';\r\n\r\n\/\/ Handle map clicks using WMS GetFeatureInfo.\r\nmap.on('singleclick', async (event) =&gt; {\r\n  popupOverlay.setPosition(undefined);\r\n  popup.hidden = true;\r\n  popupContent.replaceChildren();\r\n\r\n  const resolution = map.getView().getResolution();\r\n\r\n  const url = wmsSource.getFeatureInfoUrl(\r\n    event.coordinate,\r\n    resolution,\r\n    map.getView().getProjection(),\r\n    {\r\n      INFO_FORMAT: 'application\/json',\r\n      QUERY_LAYERS: layerName,\r\n      FEATURE_COUNT: 5\r\n    }\r\n  );\r\n\r\n  if (!url) {\r\n    status.textContent =\r\n      'Could not construct the feature information request.';\r\n    return;\r\n  }\r\n\r\n  status.textContent = 'Querying feature information\u2026';\r\n\r\n  try {\r\n    const response = await fetch(url);\r\n\r\n    if (!response.ok) {\r\n      throw new Error(`HTTP ${response.status}`);\r\n    }\r\n\r\n    const data = await response.json();\r\n    const feature = data.features?.[0];\r\n\r\n    if (!feature) {\r\n      status.textContent =\r\n        'No queryable feature found at this location.';\r\n      return;\r\n    }\r\n\r\n    const properties = feature.properties ?? {};\r\n\r\n    const title = document.createElement('h3');\r\n    title.className = 'popup-title';\r\n    title.textContent =\r\n      String(properties.asset_name ?? 'Selected asset');\r\n\r\n    popupContent.appendChild(title);\r\n\r\n    for (const key of ['asset_id', 'asset_type']) {\r\n      if (properties[key] === undefined) continue;\r\n\r\n      const row = document.createElement('p');\r\n      row.className = 'popup-row';\r\n      row.textContent = `${key}: ${properties[key]}`;\r\n      popupContent.appendChild(row);\r\n    }\r\n\r\n    popup.hidden = false;\r\n    popupOverlay.setPosition(event.coordinate);\r\n\r\n    status.textContent = 'Feature information loaded.';\r\n  } catch (error) {\r\n    console.error('GetFeatureInfo failed:', error);\r\n\r\n    status.textContent =\r\n      'Could not retrieve feature information. Check the service, ' +\r\n      'browser network panel and CORS configuration.';\r\n  }\r\n});<\/code><\/pre>\n<\/div>\n<\/div>\n<p>The example assumes that GeoServer is reachable at the configured URL, the layer is queryable, and the service returns JSON for <code>GetFeatureInfo<\/code>. If your server uses another host, port or workspace, change the URL accordingly.<\/p>\n<p>The click request uses the current map coordinate, map resolution and view projection to identify the clicked location. <code>QUERY_LAYERS<\/code> specifies which layer should be queried.<\/p>\n<p>Security detail: The code uses <code>textContent<\/code> rather than inserting returned feature attributes as HTML. This reduces the risk of interpreting untrusted attribute values as executable markup.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"35_Run_and_test_the_map\"><\/span>3.5 Run and test the map<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>Start the development server:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>npm start<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Open the local URL printed by the terminal, commonly <code>http:\/\/localhost:5173\/<\/code>.<\/p>\n<p>Test the following:<\/p>\n<ol>\n<li>\n<div>\n<div>\n<p>Rendering: The city asset layer appears over the map.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Navigation: The zoom controls work, and dragging pans the map.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Styling: Healthcare features are red and other assets are blue.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Feature query: Clicking a point opens a pop-up containing its attributes.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Error handling: If GeoServer is unavailable, inspect the browser&#8217;s developer tools for failed network requests.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ol>\n<p>If the page loads but the layer is blank, check the WMS request in the browser&#8217;s Network panel. Confirm the requested layer name, CRS, bounding box and server response. A successful HTTP response can still contain a GeoServer exception rather than a valid map image.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Step_4_Add_WFS_for_client-side_vector_interaction\"><\/span>Step 4: Add WFS for client-side vector interaction<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>WMS is appropriate when the server should render the layer. WFS is useful when the application needs actual vector geometries and attributes\u2014for example, to apply client-side styling, select features or perform browser-side analysis.<\/p>\n<p>However, loading an entire WFS layer can be expensive. The following optional example loads features by map extent, which is more appropriate for larger datasets than downloading everything at startup.<\/p>\n<p>Add these imports to <code>main.js<\/code>:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>import VectorLayer from 'ol\/layer\/Vector.js';\r\nimport VectorSource from 'ol\/source\/Vector.js';\r\nimport GeoJSON from 'ol\/format\/GeoJSON.js';\r\nimport { bbox as bboxStrategy } from 'ol\/loadingstrategy.js';\r\nimport { Style, Circle, Fill, Stroke } from 'ol\/style.js';<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Then create a WFS vector layer:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>const wfsUrl =\r\n  'http:\/\/localhost:8080\/geoserver\/city\/ows';\r\n\r\nconst wfsSource = new VectorSource({\r\n  format: new GeoJSON(),\r\n\r\n  url: (extent) =&gt; {\r\n    const params = new URLSearchParams({\r\n      service: 'WFS',\r\n      version: '2.0.0',\r\n      request: 'GetFeature',\r\n      typeNames: 'city:city_assets',\r\n      outputFormat: 'application\/json',\r\n      srsName: 'EPSG:3857',\r\n      bbox: `${extent.join(',')},EPSG:3857`\r\n    });\r\n\r\n    return `${wfsUrl}?${params.toString()}`;\r\n  },\r\n\r\n  strategy: bboxStrategy\r\n});\r\n\r\nconst wfsLayer = new VectorLayer({\r\n  source: wfsSource,\r\n  style: new Style({\r\n    image: new Circle({\r\n      radius: 6,\r\n      fill: new Fill({ color: '#F59E0B' }),\r\n      stroke: new Stroke({\r\n        color: '#FFFFFF',\r\n        width: 1.5\r\n      })\r\n    })\r\n  })\r\n});\r\n\r\n\/\/ Add this after the existing map has been created.\r\nmap.addLayer(wfsLayer);<\/code><\/pre>\n<\/div>\n<\/div>\n<p>This layer uses a bounding-box loading strategy and requests GeoJSON in Web Mercator (<code>EPSG:3857<\/code>), matching the default OpenLayers view projection. The server must support the requested WFS version, GeoJSON output and CRS.<\/p>\n<p>The WFS layer is deliberately styled separately in the browser, so it will not automatically inherit the SLD styling used by the WMS layer. If you add both layers simultaneously, the WFS points may overlap the WMS points; you can disable the WMS layer or use a layer switcher to compare them.<\/p>\n<p>For very large datasets, consider paging, server-side filters, scale-dependent loading or vector tiles instead of repeatedly fetching large feature collections.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Step_5_Troubleshooting_common_problems\"><\/span>Step 5: Troubleshooting common problems<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<div>\n<table>\n<tbody>\n<tr>\n<th>Symptom<\/th>\n<th>Likely cause<\/th>\n<th>What to check<\/th>\n<\/tr>\n<tr>\n<td>WMS layer is blank<\/td>\n<td>Incorrect layer name, CRS or bounds<\/td>\n<td>Inspect the WMS request and GeoServer logs<\/td>\n<\/tr>\n<tr>\n<td>Database store fails<\/td>\n<td>Incorrect credentials or unreachable host<\/td>\n<td>Test connectivity and PostgreSQL access rules<\/td>\n<\/tr>\n<tr>\n<td>Pop-up never appears<\/td>\n<td>Layer isn&#8217;t queryable or response format differs<\/td>\n<td>Test <code>GetFeatureInfo<\/code> directly and inspect the response<\/td>\n<\/tr>\n<tr>\n<td>WFS request fails<\/td>\n<td>Unsupported output format, version or CRS<\/td>\n<td>Check WFS capabilities and the request parameters<\/td>\n<\/tr>\n<tr>\n<td>Browser reports a CORS error<\/td>\n<td>Cross-origin access isn&#8217;t allowed<\/td>\n<td>Configure CORS on GeoServer or the reverse proxy<\/td>\n<\/tr>\n<tr>\n<td>Map is slow<\/td>\n<td>Too many features or expensive rendering<\/td>\n<td>Add indexes, filter requests, simplify data or cache suitable map tiles<\/td>\n<\/tr>\n<\/tbody>\n<\/table>\n<\/div>\n<p>A particularly common mistake is confusing CRS assignment with coordinate transformation. If the database coordinates are in a projected CRS but the layer is declared as <code>EPSG:4326<\/code>, GeoServer may display the features in the wrong place. Correct the source data&#8217;s CRS metadata and transform coordinates when necessary; do not simply change the label.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Step_6_Security_and_production_deployment\"><\/span>Step 6: Security and production deployment<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>A working development map is not yet a secure production Web GIS. Before exposing the stack publicly, address the following concerns.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Configure_CORS_deliberately\"><\/span>Configure CORS deliberately<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>If OpenLayers runs on a different origin from GeoServer, the browser may block cross-origin requests. GeoServer provides built-in CORS configuration under Server \u2192 Global Settings \u2192 CORS Settings in supported versions. Configure the specific application origin, required methods and headers, then restart GeoServer as required by the installation.<\/p>\n<div>\n<div><\/div>\n<\/div>\n<p>For example, a production application might allow only:<\/p>\n<div>\n<div>\n<pre dir=\"ltr\"><code>https:\/\/maps.example.org<\/code><\/pre>\n<\/div>\n<\/div>\n<p>Avoid allowing every origin by default for a private or authenticated service. CORS is a browser access policy, not an authentication or authorization mechanism.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Restrict_service_and_layer_access\"><\/span>Restrict service and layer access<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<ul>\n<li>\n<div>\n<div>\n<p>Create separate database roles for read-only access and controlled editing.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Configure GeoServer service and layer permissions to expose only intended data and operations.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Keep the GeoServer administrator interface and REST API private to trusted operators.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Do not put database passwords, GeoServer administrator credentials or long-lived privileged tokens in browser JavaScript.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>Use HTTPS and protect secrets in server-side configuration or a secrets manager.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<p>GeoServer supports service-level and layer-level security controls; use them rather than assuming an obscure endpoint URL protects the data.<\/p>\n<div>\n<div><\/div>\n<\/div>\n<h3><span class=\"ez-toc-section\" id=\"Limit_expensive_requests\"><\/span>Limit expensive requests<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<p>WFS can expose large collections, and poorly constrained requests can place substantial load on PostgreSQL and GeoServer. Set sensible request limits, apply filters where appropriate, monitor database connections and execution times, and consider caching for frequently requested map imagery.<\/p>\n<p>Use a reverse proxy or gateway to terminate HTTPS, control public exposure, apply rate limits where appropriate, and avoid exposing internal database ports to the internet. Keep GeoServer and its extensions updated, and back up both the spatial database and GeoServer configuration.<\/p>\n<h2><span class=\"ez-toc-section\" id=\"Conclusion\"><\/span>Conclusion<span class=\"ez-toc-section-end\"><\/span><\/h2>\n<p>This workflow demonstrates the core of an open-source web GIS architecture:<\/p>\n<ul>\n<li>\n<div>\n<div>\n<p>PostGIS stores structured spatial data and supports spatial queries.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>GeoServer publishes those tables through WMS and WFS, with SLD controlling server-side map styling.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p>OpenLayers builds the browser interface, rendering map layers and enabling navigation and feature interaction.<\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<p>The key architectural choice is to match the service to the task: use WMS when the browser primarily needs a rendered map, and use WFS when it needs vector features for interaction or analysis. For production systems, add access control, HTTPS, CORS configuration, database indexing and request limits before exposing the application publicly.<\/p>\n<h3><span class=\"ez-toc-section\" id=\"Official_documentation\"><\/span>Official documentation<span class=\"ez-toc-section-end\"><\/span><\/h3>\n<ul>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/docs.geoserver.org\/stable\/en\/user\/gettingstarted\/postgis-quickstart\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">GeoServer: Publishing a PostGIS table<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/docs.geoserver.org\/stable\/en\/user\/data\/database\/postgis.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">GeoServer: PostGIS Data Store<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/docs.geoserver.org\/stable\/en\/user\/styling\/sld\/introduction\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">GeoServer: Introduction to SLD<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/openlayers.org\/doc\/quickstart.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">OpenLayers: Quick Start<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/openlayers.org\/en\/latest\/examples\/getfeatureinfo-tile.html?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">OpenLayers: WMS GetFeatureInfo example<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<li>\n<div>\n<div>\n<p><a href=\"https:\/\/docs.geoserver.org\/stable\/en\/user\/production\/container\/?utm_source=chatgpt.com\" target=\"_blank\" rel=\"noopener\">GeoServer: Production and CORS configuration<\/a><\/p>\n<\/div>\n<\/div>\n<\/li>\n<\/ul>\n<\/div>\n","protected":false},"excerpt":{"rendered":"<p>Publishing PostGIS Layers to the Web Using GeoServer and OpenLayers Technical tutorial \u00b7 Web GIS architecture \u00b7 PostgreSQL\/PostGIS, GeoServer, OpenLayers A web map is more than a map displayed in a browser. It is a connected system in which spatial data is stored in a database, exposed through a map server, styled for presentation, and &#8230; <a title=\"Publishing PostGIS Layers to the Web Using GeoServer and OpenLayers\" class=\"read-more\" href=\"https:\/\/helpwithmygisassignment.com\/blog\/publishing-postgis-layers-to-the-web-using-geoserver-and-openlayers\/\" aria-label=\"Read more about Publishing PostGIS Layers to the Web Using GeoServer and OpenLayers\">Read more<\/a><\/p>\n","protected":false},"author":1,"featured_media":1348,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"footnotes":""},"categories":[1,4],"tags":[],"class_list":["post-1203","post","type-post","status-publish","format-standard","has-post-thumbnail","hentry","category-uncategorized","category-gis-articles"],"_links":{"self":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts\/1203","targetHints":{"allow":["GET"]}}],"collection":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/comments?post=1203"}],"version-history":[{"count":2,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts\/1203\/revisions"}],"predecessor-version":[{"id":1349,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/posts\/1203\/revisions\/1349"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/media\/1348"}],"wp:attachment":[{"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/media?parent=1203"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/categories?post=1203"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/helpwithmygisassignment.com\/blog\/wp-json\/wp\/v2\/tags?post=1203"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}