Status: IMPLEMENTED. The sitemap servlet shipped —
SitemapServletatcom.wikantik.ui.SitemapServlet(source:wikantik-main/src/main/java/com/wikantik/ui/SitemapServlet.java), serving/sitemap.xml. The Atom feed counterpart isAtomFeedServletatcom.wikantik.content.AtomFeedServlet. This document is preserved as the implementation specification it was written against; treat it as design-of-record rather than a to-do list. As shipped, the servlet emits only<loc>and<lastmod>plus Google image and news entries; thechangefreqandprioritycode below was dropped (see Sitemap Optimization).Note on configuration: The
jspwiki.sitemap.*properties described in Phase 2 below (enabled, includeAttachments, cacheTimeout, etc.) were never implemented. The only real sitemap property iswikantik.sitemap.baseURL— set it when the wiki runs behind a reverse proxy or load balancer that would otherwise cause the servlet to derive an incorrecthttp://base URL. Example:wikantik.sitemap.baseURL = https://wiki.wikantik.com
A sitemap.xml tells search engines about the pages on your wiki, helping them crawl more efficiently. This document provides a detailed breakdown of what needs to be implemented for Google Search Console compliance.
Google Search Console expects sitemaps to follow the Sitemap Protocol
:
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<url>
<loc>https://wiki.example.com/wiki/Main</loc>
<lastmod>2025-11-22</lastmod>
<changefreq>weekly</changefreq>
<priority>1.0</priority>
</url>
<!-- more URLs -->
</urlset>
Key requirements:
Location: wikantik-main/src/main/java/org/apache/wiki/ui/SitemapServlet.java
package com.wikantik.ui;
import java.io.IOException;
import java.io.PrintWriter;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.text.SimpleDateFormat;
import java.util.Collection;
import java.util.List;
import java.util.stream.Collectors;
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.apache.logging.log4j.LogManager;
import org.apache.logging.log4j.Logger;
import com.wikantik.api.core.Context;
import com.wikantik.api.core.ContextEnum;
import com.wikantik.api.core.Engine;
import com.wikantik.api.core.Page;
import com.wikantik.api.core.Session;
import com.wikantik.api.spi.Wiki;
import com.wikantik.auth.AuthorizationManager;
import com.wikantik.auth.permissions.PagePermission;
import com.wikantik.pages.PageManager;
import com.wikantik.references.ReferenceManager;
import com.wikantik.url.URLConstructor;
public class SitemapServlet extends HttpServlet {
private static final Logger LOG = LogManager.getLogger( SitemapServlet.class );
private Engine m_engine;
@Override
public void init( final ServletConfig config ) throws ServletException {
super.init( config );
m_engine = Wiki.engine().find( config );
LOG.info( "SitemapServlet initialized." );
}
@Override
protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
throws ServletException, IOException {
// Generate sitemap XML
}
}
1. 2.1 Get All Pages
PageManager pageManager = m_engine.getManager( PageManager.class );
Collection<Page> allPages = pageManager.getAllPages();
1. 2.2 Filter Pages Based on Permissions
Only include pages that are publicly accessible (anonymous users can view):
AuthorizationManager authManager = m_engine.getManager( AuthorizationManager.class );
Session session = Wiki.session().find( m_engine, req );
List<Page> publicPages = allPages.stream()
.filter( page -> {
try {
PagePermission permission = new PagePermission( page, PagePermission.VIEW_ACTION );
// For sitemap, we want pages viewable by anonymous users
// Create a context to check permissions
Context context = Wiki.context().create( m_engine, req, ContextEnum.PAGE_VIEW.getRequestContext() );
context.setPage( page );
return authManager.checkPermission( context.getWikiSession(), permission );
} catch ( Exception e ) {
return false;
}
} )
.collect( Collectors.toList() );
1. 2.3 Generate URLs for Each Page
Use the URLConstructor to generate proper URLs based on the wiki's configuration:
URLConstructor urlConstructor = m_engine.getManager( URLConstructor.class );
String baseUrl = m_engine.getBaseURL();
for ( Page page : publicPages ) {
// Use URLConstructor to generate URLs in the configured format
String pageUrl = baseUrl + urlConstructor.makeURL(
ContextEnum.PAGE_VIEW.getRequestContext(),
page.getName(),
null
);
// Remove any leading slash duplication
pageUrl = pageUrl.replace( baseUrl + "/" + baseUrl, baseUrl );
Date lastModified = page.getLastModified();
// Write to XML
}
1. 2.4 Write XML Output
resp.setContentType( "application/xml" );
resp.setCharacterEncoding( "UTF-8" );
PrintWriter out = resp.getWriter();
out.println( "<?xml version=\"1.0\" encoding=\"UTF-8\"?>" );
out.println( "<urlset xmlns=\"http://www.sitemaps.org/schemas/sitemap/0.9\">" );
SimpleDateFormat dateFormat = new SimpleDateFormat( "yyyy-MM-dd" );
for ( Page page : publicPages ) {
String pageUrl = m_engine.getManager( URLConstructor.class )
.makeURL( ContextEnum.PAGE_VIEW.getRequestContext(), page.getName(), null );
// Make URL absolute
if ( !pageUrl.startsWith( "http" ) ) {
pageUrl = m_engine.getBaseURL() + pageUrl;
}
out.println( " <url>" );
out.println( " <loc>" + escapeXml( pageUrl ) + "</loc>" );
if ( page.getLastModified() != null ) {
out.println( " <lastmod>" + dateFormat.format( page.getLastModified() ) + "</lastmod>" );
}
out.println( " <changefreq>" + determineChangeFreq( page ) + "</changefreq>" );
out.println( " <priority>" + determinePriority( page ) + "</priority>" );
out.println( " </url>" );
}
out.println( "</urlset>" );
1. 3.1 Determine Change Frequency
Based on page edit history:
private String determineChangeFreq( final Page page ) {
PageManager pm = m_engine.getManager( PageManager.class );
List<Page> history = pm.getVersionHistory( page.getName() );
if ( history == null || history.size() <= 1 ) {
return "monthly";
}
// Calculate days since last edit
long daysSinceLastEdit = java.time.temporal.ChronoUnit.DAYS.between(
page.getLastModified().toInstant(), java.time.Instant.now() );
if ( daysSinceLastEdit < 1 ) return "daily";
if ( daysSinceLastEdit < 7 ) return "weekly";
if ( daysSinceLastEdit < 30 ) return "monthly";
return "yearly";
}
1. 3.2 Determine Priority
Based on page importance:
private String determinePriority( final Page page ) {
String pageName = page.getName();
// Main page gets highest priority
if ( pageName.equals( m_engine.getFrontPage() ) ) {
return "1.0";
}
// Consider number of incoming links
ReferenceManager refManager = m_engine.getManager( ReferenceManager.class );
Collection<String> referrers = refManager.findReferrers( pageName );
if ( referrers != null && referrers.size() > 10 ) {
return "0.8";
} else if ( referrers != null && referrers.size() > 5 ) {
return "0.6";
}
return "0.5"; // Default priority
}
1. 3.3 XML Escaping
private String escapeXml( final String input ) {
return input
.replace( "&", "&" )
.replace( "<", "<" )
.replace( ">", ">" )
.replace( "\"", """ )
.replace( "'", "'" );
}
Location: wikantik-war/src/main/webapp/WEB-INF/web.xml
<servlet>
<servlet-name>SitemapServlet</servlet-name>
<servlet-class>com.wikantik.ui.SitemapServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>SitemapServlet</servlet-name>
<url-pattern>/sitemap.xml</url-pattern>
</servlet-mapping>
Location: wikantik-main/src/test/java/org/apache/wiki/ui/SitemapServletTest.java
Create tests for:
For performance, cache the generated sitemap using Wikantik's CachingManager:
private static final String CACHE_SITEMAP = "wikantik.sitemapCache";
private static final String CACHE_KEY = "sitemap";
@Override
protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
throws ServletException, IOException {
CachingManager cachingManager = m_engine.getManager( CachingManager.class );
String sitemap = cachingManager.get( CACHE_SITEMAP, CACHE_KEY, () -> generateSitemapContent( req ) );
resp.setContentType( "application/xml" );
resp.setCharacterEncoding( "UTF-8" );
resp.getWriter().write( sitemap );
}
private String generateSitemapContent( final HttpServletRequest req ) {
// Generate sitemap XML as a String
// ...
}
Note: You may need to register the cache name in the caching configuration.
The shipped SitemapServlet has no enable/disable flag — it is registered
unconditionally at /sitemap.xml in web.xml and always generates the
sitemap. There is also no includeAttachments, cacheTimeout,
excludePatterns, defaultChangeFreq, or defaultPriority property; those
are not read anywhere in the code.
The one real property is an optional base-URL override, for deployments
behind a reverse proxy that terminates SSL (the incoming-request-derived
base URL would otherwise resolve to http:// instead of https://):
# wikantik-custom.properties
wikantik.sitemap.baseURL = https://wiki.example.com
If unset, SitemapServlet derives the base URL from the incoming request,
which is correct whenever Wikantik (or a proxy that forwards X-Forwarded-*
correctly) terminates SSL itself.
For wikis with more than 50,000 pages, implement a sitemap index:
1. 8.1 Sitemap Index Format
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap>
<loc>https://wiki.example.com/sitemap/1</loc>
<lastmod>2025-11-22</lastmod>
</sitemap>
<sitemap>
<loc>https://wiki.example.com/sitemap/2</loc>
<lastmod>2025-11-22</lastmod>
</sitemap>
</sitemapindex>
1. 8.2 URL Routing
Add additional servlet mapping in web.xml:
<servlet-mapping>
<servlet-name>SitemapServlet</servlet-name>
<url-pattern>/sitemap/*</url-pattern>
</servlet-mapping>
Implementation:
private static final int MAX_URLS_PER_SITEMAP = 50000;
@Override
protected void doGet( final HttpServletRequest req, final HttpServletResponse resp )
throws ServletException, IOException {
String pathInfo = req.getPathInfo(); // e.g., "/1" for sitemap chunk 1
PageManager pageManager = m_engine.getManager( PageManager.class );
int totalPages = pageManager.getTotalPageCount();
if ( pathInfo == null || pathInfo.equals( "/" ) ) {
// Check if we need sitemap index
if ( totalPages > MAX_URLS_PER_SITEMAP ) {
generateSitemapIndex( resp, totalPages );
} else {
generateSitemap( resp, req, 0, totalPages );
}
} else {
// Generate specific sitemap chunk
int chunk = Integer.parseInt( pathInfo.substring( 1 ) );
generateSitemap( resp, req, chunk * MAX_URLS_PER_SITEMAP, MAX_URLS_PER_SITEMAP );
}
}
To index attachments:
AttachmentManager attachmentManager = m_engine.getManager( AttachmentManager.class );
for ( Page page : publicPages ) {
// Add page URL...
// Add attachment URLs
if ( includeAttachments ) {
Collection<Attachment> attachments = attachmentManager.listAttachments( page );
for ( Attachment att : attachments ) {
String attUrl = m_engine.getManager( URLConstructor.class )
.makeURL( ContextEnum.PAGE_ATTACH.getRequestContext(), att.getName(), null );
// Write attachment URL to sitemap
}
}
}
Location: wikantik-war/src/main/webapp/robots.txt
User-agent: *
Allow: /
Sitemap: https://wiki.example.com/sitemap.xml
Note: The sitemap URL should be configurable or dynamically generated based on engine.getBaseURL().
| File | Action | Phase | Description |
|---|---|---|---|
wikantik-main/.../ui/SitemapServlet.java | Create | 1 | Main servlet implementation |
wikantik-war/.../WEB-INF/web.xml | Modify | 1 | Register servlet mapping |
wikantik-main/.../ui/SitemapServletTest.java | Create | 1 | Unit tests |
wikantik-main/.../ini/wikantik.properties | Modify | 2 | Add configuration properties |
wikantik-war/.../robots.txt | Create/Modify | 4 | Add sitemap reference |
URL Format: The implementation uses URLConstructor to generate URLs in the configured format (e.g., /wiki/PageName for ShortViewURLConstructor)
HTTP Headers: Add Last-Modified and ETag headers for efficient caching by search engines
Compression: Support gzip compression for large sitemaps (sitemap.xml.gz)
Internationalization: If your wiki has multiple language versions, consider using hreflang annotations
News Sitemaps: If your wiki has time-sensitive content, consider implementing Google News sitemap extensions
Wiki.engine().find( config )Wiki.session().find( engine, request )Wiki.context().create( engine, request, contextEnum )engine.getManager( URLConstructor.class ).makeURL( context, name, params )engine.getManager( AuthorizationManager.class ).checkPermission( session, permission )engine.getManager( CachingManager.class ).get( cacheName, key, supplier )engine.getManager( PageManager.class ).getAllPages()engine.getManager( ReferenceManager.class ).findReferrers( pageName )Use Jakarta EE (not javax):
import jakarta.servlet.ServletConfig;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServlet;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
Cluster Hub: Wikantik Platform Development Hub: Architecture, MCP Protocols, and Agent Briefings (wikantik-development)