Search Engine Optimization (SEO) is crucial for modern web applications, ensuring that content is discoverable and indexed by search engines. In this article, I will walk through how I implemented an SEO module for the AWA framework, focusing on sitemap generation—a key feature for SEO.
Implementing a Search Engine Optimization Module for AWA
By Stephane Carrez2026-03-29 07:44:00

Introduction
The AWA framework is a web application framework written in Ada. Recently, AWA was awarded the Ada Crate of the Year 2025. To improve the visibility of AWA-based applications, I developed an SEO module that enables the dynamic generation of sitemaps. Sitemaps are XML files that list URLs for a site, helping search engines crawl and index content more efficiently.
The SEO module is designed to:
- Register sitemap providers for different parts of an application.
- Generate sitemap XML files dynamically.
- Cache sitemap entries to avoid repeated database queries.
- Serve sitemaps via a dedicated servlet.
Sitemap Description
A sitemap entry is described by the Sitemap_Entry type and consists of an absolute URI location, a last modification date, a priority, an optional image URI location, and an optional image title. The Ada containers Vectors generic package is instantiated to define the Sitemap_Entry_Vector type, which represents a list of sitemaps. A module that wants to provide a list of sitemap entries must implement the Sitemap_Provider limited interface and implement the Create_Sitemap procedure. This procedure must populate a Sitemap_Info record which contains the sitemap entries that must be exposed. Below is an extract of the main declarations of these types from the SEO module:
package AWA.SEO is
...
subtype UString is Ada.Strings.Unbounded.Unbounded_String;
-- Describe an entry in the sitmap generation.
type Sitemap_Entry is record
Location : UString;
Date : Ada.Calendar.Time;
Priority : Natural;
Image : UString;
Image_Title : UString;
end record;
package Sitemap_Entry_Vectors is
new Ada.Containers.Vectors (Index_Type => Positive,
Element_Type => Sitemap_Entry);
subtype Sitemap_Entry_Vector is Sitemap_Entry_Vectors.Vector;
type Sitemap_Info is record
Entries : Sitemap_Entry_Vectors.Vector;
end record;
type Sitemap_Provider is limited interface;
type Sitemap_Provider_Access is access all Sitemap_Provider'Class;
procedure Create_Sitemap (Provider : in Sitemap_Provider;
Sitemap : in out Sitemap_Info) is abstract;
...
end AWA.SEO;
Sitemap Registry
Sitemap providers generate information to create an XML sitemap. A web application can register multiple sitemap providers, each associated with a unique name, which is also the XML sitemap name. Therefore, we need a registry where these sitemap providers are registered during application setup. Additionally, we need a cache to avoid asking the sitemap provider to generate the list of entries every time we need to generate a sitemap (since such generation could be expensive).
To define such a registry, I have defined the Sitemap_Record type, which includes the name of the provider (also the name of the XML sitemap), the associated provider, the last sitemap entries that the provider created, and the date when it produced such a list.
type Sitemap_Record is record
Name : UString;
Provider : Sitemap_Provider_Access;
Sitemap : Sitemap_Info;
Date : Ada.Calendar.Time;
Has_Data : Boolean := False;
end record;
package Sitemap_Record_Vectors is
new Ada.Containers.Vectors (Index_Type => Positive,
Element_Type => Sitemap_Record);
Because the server can receive concurrent requests, the registry is declared within an Ada protected type, as shown below. The protected type ensures that only one task at a time can access the information and update it.
protected type Seo_Data is
procedure Register (Name : in String;
Provider : Sitemap_Provider_Access);
procedure Get_Sitemap (Name : in String;
List : in out Sitemap_Entry_Vectors.Vector;
Found : out Boolean);
procedure Get_Sitemap_Index (Index : in out Sitemap_Index_Vector);
procedure Set_Refresh_Delay (New_Delay : in Duration);
private
Refresh_Delay : Duration := 3600.0;
Sitemaps : Sitemap_Record_Vectors.Vector;
end Seo_Data;
Sitemap Servlet
To serve the sitemaps, the SEO module will use an Ada Servlet. It is an adaptation and implementation of the JSR 315 (Java Servlet Specification) for the Ada language. The Ada API is very close to the Java API as it provides the Servlet, Filter, Request, Response and Session types with quite the same methods. It should be quite easy for someone who is familiar with Java servlets to write an Ada servlet.
The Ada Servlet implementation can use either the Ada Web Server as a web server or the Embedded Web Server. Porting to another web server implementation should be relatively easy.
The servlet API is represented by the Servlet tagged type which represents the root of all servlets. A servlet must extend this Servlet tagged type and it can override one of the Do_Get, Do_Post, Do_Head Do_Delete, Do_Put, Do_Options or Do_Trace procedure. Each Do_XXX procedure receives a request object and a response object.
For the SEO module, we only need to implement the Do_Get procedure and we have to generate the XML sitemap, or the XML sitemap index. When the servlet is called and we have an empty path, we have to generate the XML sitemap index which indicates the list of sitemaps that the application provides. In our case, such index is created by looking at the sitemap providers.
The sitemap index or the specific sitemap is retrieved by accessing the SEO module and asking it either the index or the sitemap with the given name. The SEO module looks the information through its protected type hence protecting concurrent accesses and it returns the information we need.
Having the data, the SEO servlet must generate the XML content and for this it gets a stream object dedicated to XML generation. We only have to call either Start_Entity or Write_Entity to build the XML structure we want.
package body AWA.SEO.Servlets is
Log : constant Util.Log.Loggers.Logger
:= Util.Log.Loggers.Create ("AWA.SEO.Servlets");
overriding procedure Do_Get
(Server : in SEO_Servlet;
Request : in out Servlet.Requests.Request'Class;
Response : in out Servlet.Responses.Response'Class) is
URI : constant String := Request.Get_Path_Info;
Module : constant AWA.SEO.Modules.SEO_Module_Access
:= Modules.Get_SEO_Module;
Stream : Servlet.Streams.XML.Print_Stream
:= Response.Get_Output_Stream;
begin
Log.Info ("GET: {0}", URI);
Response.Set_Content_Type (Util.Http.Mimes.Xml);
if URI'Length = 0 or else URI = "/sitemap.xml" then
-- Generate sitemap index
declare
List : Sitemap_Index_Vector;
begin
Module.Get_Sitemap_Index (List);
Stream.Start_Document;
Stream.Start_Entity ("sitemapindex");
Stream.Write_Attribute ("xmlns",
"http://www.sitemaps.org/schemas/sitemap/0.9");
for Index of List loop
Stream.Start_Entity ("sitemap");
Stream.Write_Entity ("loc",
Server.Sitemap_Prefix & Index.Location);
Stream.Write_Entity ("lastmod",
Format_Date (Index.Date));
Stream.End_Entity ("sitemap");
end loop;
Stream.End_Entity ("sitemapindex");
Stream.End_Document;
end;
else
-- Generate specific sitemap
declare
List : Sitemap_Entry_Vectors.Vector;
Found : Boolean;
begin
Module.Get_Sitemap (URI (URI'First + 1 .. URI'Last), List, Found);
if not Found then
Response.Send_Error (Servlet.Responses.SC_NOT_FOUND);
return;
end if;
Stream.Start_Document;
Stream.Start_Entity ("urlset");
Stream.Write_Attribute ("xmlns",
"http://www.sitemaps.org/schemas/sitemap/0.9");
for Item of List loop
Stream.Start_Entity ("url");
Stream.Write_Entity ("loc",
String'(To_String (Item.Location)));
Stream.Write_Entity ("lastmod",
Format_Date (Item.Date));
Stream.End_Entity ("url");
end loop;
Stream.End_Entity ("urlset");
Stream.End_Document;
end;
end if;
exception
when Servlet.Routes.No_Parameter =>
Response.Send_Error (Servlet.Responses.SC_BAD_REQUEST);
end Do_Get;
end AWA.SEO.Servlets;
Module Structure
The SEO module is registered as a plugin in the AWA framework. In the AWA framework, a module is a software component that can be integrated into the web application. It is defined by extending the AWA.Modules.Module tagged type and overrides several important procedures. The module can provide UI components that are used by other modules, or it can define some ready-to-use functionality such as the Blogs module or the Wiki module.
The core of this SEO module is the SEO_Module tagged type, which manages our sitemap providers and servlet registration. We override the Initialize procedure, which is called during application startup to initialize the module, and the Configure procedure, which is called during the application configuration phase.
The SEO module declaration is shown below:
package AWA.SEO.Modules is
type SEO_Module is new AWA.Modules.Module with private;
type SEO_Module_Access is access all SEO_Module'Class;
overriding procedure Initialize
(Plugin : in out SEO_Module;
App : in AWA.Modules.Application_Access;
Props : in ASF.Applications.Config);
overriding
procedure Configure (Plugin : in out SEO_Module;
Props : in ASF.Applications.Config);
private
...
type SEO_Module is new AWA.Modules.Module with record
SEO_Servlet : aliased AWA.SEO.Servlets.SEO_Servlet;
Data : Seo_Data;
end record;
end AWA.SEO.Modules;
And the Initialize procedure implementation is easily implemented by calling the parent Initialize procedure and register our servlet instance by giving it a name. The SEO module, its internal data, the servlet are global to the application and are used by concurrent requests.
package body AWA.SEO.Modules is
Log : constant Util.Log.Loggers.Logger
:= Util.Log.Loggers.Create ("AWA.SEO.Module");
overriding procedure Initialize
(Plugin : in out SEO_Module;
App : in AWA.Modules.Application_Access;
Props : in ASF.Applications.Config) is
begin
Log.Info ("Initializing the SEO module");
AWA.Modules.Module (Plugin).Initialize (App, Props);
App.Add_Servlet ("sitemaps", Plugin.SEO_Servlet'Unchecked_Access);
end Initialize;
...
end AWA.SEO.Modules;
Module Configuration
With AWA framework, a module can define a configuration file in the form of an XML file which is read during application configuration phase. This allows the module to define its configuration in terms of parameters, security configuration, web page navigation and so on.
The SEO module uses the following configuration:
<module version="1.0">
<context-param>
<param-name>seo.sitemap_prefix</param-name>
<param-value>#{app_url_base}/sitemaps/</param-value>
<description>
The URL base prefix for sitemap XML files.
</description>
</context-param>
<context-param>
<param-name>seo.sitemap_refresh</param-name>
<param-value>3600</param-value>
<description>
Cache delay in seconds before refreshing the sitemap.
</description>
</context-param>
<servlet-mapping>
<servlet-name>sitemaps</servlet-name>
<url-pattern>/sitemaps/*</url-pattern>
</servlet-mapping>
<filter-mapping>
<filter-name>service</filter-name>
<url-pattern>/sitemaps/*</url-pattern>
</filter-mapping>
</module>
This defines:
- A first configuration parameter,
seo.sitemap_prefix, which indicates the base URL prefix to be used in the creation of the XML sitemap. The default value should work for most applications but can be changed easily. - A second configuration parameter,
seo.sitemap_refresh, which allows control of the cache delay used to refresh the sitemaps. - Finally, the module configuration also declares the servlet mapping (
servlet-mapping), which indicates how the SEO servlet registered under the namesitemapswill receive the URL requests. With this configuration, the URL represented by/sitemaps/*will all be handled by the SEO servlet. Together with this servlet mapping, a filter mapping (filter-mapping) activates theservicefilter, which is necessary for the support of database access.
Use case with the Blogs Module
The AWA framework provides a Blogs module that allows users to write blog articles using Markdown, other wiki formats or event direct HTML. To demonstrate the SEO module’s flexibility, I integrated it with the Blogs module, enabling blog posts to be included in the sitemap.
To achieve this, the Blog_Module now implements the Sitemap_Provider interface.
package AWA.Blogs.Modules is
type Blog_Module is new AWA.Modules.Module
and AWA.SEO.Sitemap_Provider with private;
...
end AWA.Blogs.Modules;
The implementation is straightforward but requires executing an SQL query. The AWA framework uses the Ada Database Objects library for this purpose. It provides, together with the Dynamo generation tool, a direct mapping of SQL results in Ada records. The SQL query is defined in an XML file that describes both the expected Ada mapping in terms of records and the SQL query itself. In our case, the SQL query includes a join with multiple tables and retrieves 5 columns, which are mapped to a Sitemap_Info record generated in the AWA.Blogs.Models package.
<query-mapping package='AWA.Blogs.Models'>
<class name="AWA.Blogs.Models.Sitemap_Info" bean="yes">
<property type='Identifier' name="id"/>
<property type='Date' name="date"/>
<property type='String' name="uri"/>
<property type='Identifier' name="image_id"/>
<property type='String' name="image_title"/>
</class>
<query name='blog-post-sitemap'>
<sql>
SELECT post.id, post.publish_date, post.uri,
img.id, COALESCE(store.name, '')
FROM awa_post AS post
LEFT JOIN awa_image AS img ON post.image_id = img.id
LEFT JOIN awa_storage AS store ON img.storage_id = store.id
WHERE post.status = 1 ORDER BY post.id DESC;
</sql>
</query>
</query-mapping>
From the above XML description, Dynamo generates the Sitemap_Info type as well as the List procedure, which executes the SQL query and loads the results into a vector of Sitemap_Info records. Having the SQL results directly mapped in an Ada record reduces the errors in accessing the SQL columns and simplifies the implementation. If a column does not have the correct type, a Constraint_Error exception will be raised during execution. Having the SQL query itself externalized in an XML file also helps to fix quickly the SQL query without rebuilding the application.
The implementation of our sitemap provider can now be implemented as follows:
- We prepare the query execution,
- We execute the query to obtain the vector of
Sitemap_Info, - We populate the sitemap provider result with an entry for each item. The most challenging task is creating the absolute URI that will be used for the XML sitemap generation.
package body AWA.Blogs.Modules is
overriding procedure Create_Sitemap
(Provider : in Blog_Module;
Sitemap : in out AWA.SEO.Sitemap_Info) is
DB : ADO.Sessions.Session := Provider.Get_Session;
Query : ADO.Queries.Context;
List : AWA.Blogs.Models.Sitemap_Info_Vector;
Ctx : EL.Contexts.Default.Default_Context;
Variables : aliased EL.Variables.Default.Default_Variable_Mapper;
begin
Ctx.Set_Variable_Mapper (Variables'Unchecked_Access);
Query.Set_Query (AWA.Blogs.Models.Query_Blog_Post_Sitemap);
AWA.Blogs.Models.List (List, DB, Query);
Sitemap.Entries.Clear;
for Info of List loop
Variables.Bind ("uri", UBO.To_Object (Info.URI));
declare
Item : AWA.SEO.Sitemap_Entry;
URI : constant Util.Beans.Objects.Object
:= Provider.Post_URI.Get_Value (Ctx);
begin
Item.Location := UBO.To_Unbounded_String (URI);
Item.Date := Info.Date;
Item.Priority := 0;
Sitemap.Entries.Append (Item);
end;
end loop;
end Create_Sitemap;
end AWA.Blogs.Modules;
The last step for this implementation is to register the sitemap provider in the SEO module. This is done from the Configure procedure in the Blogs module.
overriding
procedure Configure (Plugin : in out Blog_Module;
Props : in ASF.Applications.Config) is
begin
...
AWA.SEO.Register ("blog-sitemap.xml", Plugin'Unchecked_Access);
end Configure;
These changes can be looked at in the following commit 1d56fe9 if you want to analyze more.
With this implementation, the Blogs module exposes the sitemap with the following URL https://blog.vacs.fr/vacs/sitemaps/blog-sitemap.xml and the sitemap index is available from https://blog.vacs.fr/vacs/sitemaps/sitemap.xml
Conclusion
The SEO module for AWA framework provides a flexible and efficient way to generate sitemaps, enhancing the discoverability of web applications. By allowing modules like Blogs to register as sitemap providers, it ensures that all relevant content is indexed by search engines. Ada/SPARK Crate Of The Year 2025 Winners Announced!
I was able to write this SEO module very quickly due to the strong typing and many verifications that the Ada language imposes and that the Ada compiler verifies. The implementation was made by copying here and there some pieces of Ada code from other AWA modules. After fixing the compilation errors, the final implementation was indeed correct. Almost zero debugging was necessary.
For more details, check out the AWA framework on GitHub and to further analyse the changes, you may look at the following commit b77ddc9.
Add a comment
To add a comment, you must be connected. Login