Most Wix SEO work happens in the frontend: setting meta tags, generating schema, optimizing content. But some of the most powerful SEO techniques require server-side logic that runs before the page is ever delivered to the browser or the bot. Velo HTTP functions let you create custom API endpoints on your Wix site, and Velo routers let you intercept URL requests with server-side logic. Together, these tools enable custom sitemaps, programmatic redirect systems, SEO A/B testing, and intelligent 404 handling that would otherwise require a standalone server.

Understanding Velo HTTP Functions for SEO
Velo HTTP functions are server-side endpoints you create in the backend/http-functions.js file. Each function corresponds to a URL pattern on your site: a function named get_customSitemap becomes accessible at yoursite.com/_functions/customSitemap via GET request. These functions execute on Wix servers, have access to your CMS data, and can return any HTTP response including XML, JSON, or plain text.
For SEO, the key advantage is that HTTP functions run entirely server-side with no client JavaScript rendering required. Search engine bots receive the response directly, exactly as your function constructs it. This makes HTTP functions ideal for generating XML sitemaps, serving robots.txt modifications, creating JSON API endpoints for external SEO tools, and building webhook receivers for content syndication platforms.
import { ok, notFound, serverError } from 'wix-http-functions';
import wixData from 'wix-data';
export async function get_customSitemap(request) {
try {
const result = await wixData.query('Products')
.eq('isPublished', true)
.ne('noIndex', true)
.limit(1000)
.find();
const baseUrl = 'https://www.yourstore.com';
let xml = '<?xml version="1.0" encoding="UTF-8"?>';
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
result.items.forEach(item => {
xml += '<url>';
xml += '<loc>' + baseUrl + '/products/' + item.slug + '</loc>';
xml += '<lastmod>' + new Date(item._updatedDate).toISOString().split('T')[0] + '</lastmod>';
xml += '<changefreq>weekly</changefreq>';
xml += '<priority>0.8</priority>';
xml += '</url>';
});
xml += '</urlset>';
return ok({
headers: { 'Content-Type': 'application/xml' },
body: xml
});
} catch (error) {
return serverError({ body: 'Sitemap generation failed' });
}
}Building a Custom Sitemap System
The default Wix sitemap works well for most sites, but it has limitations. It includes all published pages regardless of their SEO value, does not support custom priority or changefreq values per URL, and cannot include URLs from external systems or custom routing logic. A custom sitemap built with HTTP functions gives you complete control over which URLs are included, their metadata, and how the sitemap is structured.
For large sites with more than 50,000 URLs, you need a sitemap index that references multiple individual sitemaps. The sitemap protocol limits each sitemap file to 50,000 URLs and 50MB uncompressed. Your HTTP function can generate a sitemap index that points to paginated sitemap files, each of which is also served by an HTTP function with offset and limit parameters.
export async function get_sitemapIndex(request) {
const collections = [
{ name: 'Products', count: 5000 },
{ name: 'BlogPosts', count: 800 },
{ name: 'Locations', count: 200 }
];
const baseUrl = 'https://www.yourstore.com';
let xml = '<?xml version="1.0" encoding="UTF-8"?>';
xml += '<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
collections.forEach(col => {
const pages = Math.ceil(col.count / 1000);
for (let i = 0; i < pages; i++) {
xml += '<sitemap>';
xml += '<loc>' + baseUrl + '/_functions/sitemap_' + col.name.toLowerCase() + '?page=' + i + '</loc>';
xml += '<lastmod>' + new Date().toISOString().split('T')[0] + '</lastmod>';
xml += '</sitemap>';
}
});
xml += '</sitemapindex>';
return ok({
headers: { 'Content-Type': 'application/xml' },
body: xml
});
}Dynamic Redirect Systems with HTTP Functions
URL redirects are essential during site migrations, product discontinuations, and URL restructuring. While Wix offers a built-in redirect manager, it has a limit on the number of redirects and does not support pattern-based or conditional redirects. A Velo-powered redirect system stores redirect rules in a CMS collection and evaluates them server-side, supporting thousands of redirects with pattern matching and conditional logic.
Store your redirect rules in a collection with fields for source URL pattern, destination URL, redirect type (301 permanent or 302 temporary), and an active/inactive flag. Your HTTP function or router queries this collection on each request and returns the appropriate redirect response. This approach scales to tens of thousands of redirects and can be updated by non-technical team members through the CMS interface.
import { ok, redirect, notFound } from 'wix-router';
import wixData from 'wix-data';
export async function products_Router(request) {
const slug = request.path[0];
const redirectResult = await wixData.query('Redirects')
.eq('sourceSlug', slug)
.eq('isActive', true)
.find();
if (redirectResult.items.length > 0) {
const rule = redirectResult.items[0];
return redirect(rule.destinationUrl, rule.redirectType === '301' ? '301' : '302');
}
const productResult = await wixData.query('Products')
.eq('slug', slug)
.eq('isPublished', true)
.find();
if (productResult.items.length === 0) {
return notFound();
}
return ok('product-page', productResult.items[0]);
}Server-Side Logic for SEO A/B Testing
SEO A/B testing allows you to test different title tags, meta descriptions, or page content variations to determine which version produces better rankings or click-through rates. Unlike traditional A/B testing where you split traffic between two page versions, SEO A/B testing assigns a variation to a page permanently (for the duration of the test) so that Googlebot consistently sees the same version and can evaluate it fairly.
Implement this by creating a CMS collection that maps page slugs to test variations. The router or page-level Velo code queries this collection and applies the assigned variation. Each variation includes its own title, meta description, and any other elements being tested. After sufficient time (typically 4-8 weeks), compare rankings and CTR data in Google Search Console to determine the winner.
import wixData from 'wix-data';
import wixSeo from 'wix-seo';
$w.onReady(async function () {
const routerData = $w('#dynamicDataset').getCurrentItem();
const slug = routerData.slug;
const testResult = await wixData.query('SeoTests')
.eq('pageSlug', slug)
.eq('isActive', true)
.find();
if (testResult.items.length > 0) {
const test = testResult.items[0];
wixSeo.title = test.testTitle;
wixSeo.metaTags = [
{ name: 'description', content: test.testDescription }
];
} else {
wixSeo.title = routerData.seoTitle || routerData.name + ' | YourBrand';
wixSeo.metaTags = [
{ name: 'description', content: routerData.metaDescription }
];
}
});A/B Testing Tip
Only test one SEO element at a time to isolate its impact. If you change both the title and meta description simultaneously, you will not know which change drove the result. Start with title tag testing because it has the most direct impact on both rankings and CTR, then test meta descriptions in a separate experiment.
Custom 404 Handling with Velo Routers
A well-designed 404 system does more than show an error page. It logs missing URLs to help you identify broken links and redirect opportunities, suggests relevant content to keep users on your site, and returns the correct HTTP status code so search engines know the page is genuinely gone rather than temporarily unavailable. Velo routers give you full control over this behavior.
When a router cannot find a matching item for the requested slug, instead of returning a generic 404, query your CMS for similar items and pass them to a custom 404 page template. Log the requested URL to a "BrokenUrls" collection for later review. If the URL matches a known pattern from a previous site structure, automatically redirect to the new equivalent.
import { ok, notFound, redirect } from 'wix-router';
import wixData from 'wix-data';
export async function blog_Router(request) {
const slug = request.path[0];
const postResult = await wixData.query('BlogPosts')
.eq('slug', slug)
.eq('isPublished', true)
.find();
if (postResult.items.length > 0) {
return ok('blog-post-page', postResult.items[0]);
}
const redirectResult = await wixData.query('Redirects')
.eq('sourceSlug', slug)
.find();
if (redirectResult.items.length > 0) {
return redirect(redirectResult.items[0].destinationUrl, '301');
}
await wixData.insert('BrokenUrls', {
requestedUrl: '/blog/' + slug,
referrer: request.referrer || 'direct',
timestamp: new Date(),
resolved: false
});
const relatedPosts = await wixData.query('BlogPosts')
.eq('isPublished', true)
.descending('publishDate')
.limit(5)
.find();
return notFound('custom-404-page', {
requestedSlug: slug,
suggestedPosts: relatedPosts.items
});
}Rate Limiting and Caching for Server-Side SEO Functions
HTTP functions that generate sitemaps or handle redirects can receive significant traffic, both from search engine bots and from malicious scrapers. Without caching, every request triggers a fresh database query, which can exhaust your Wix data quotas and slow down response times. Implement simple in-memory caching by storing the generated response in a module-level variable with a timestamp, and serving the cached version until it expires.
For sitemaps specifically, a cache duration of 1-6 hours is appropriate. Your product catalog does not change every minute, and search engines typically re-fetch sitemaps on a daily or weekly basis. For redirect lookups, consider loading all active redirects into memory on the first request and refreshing the cache every 30 minutes, rather than querying the database on every single page request.
import { ok } from 'wix-http-functions';
import wixData from 'wix-data';
let cachedSitemap = null;
let cacheTimestamp = 0;
const CACHE_DURATION = 3600000;
export async function get_productSitemap(request) {
const now = Date.now();
if (cachedSitemap && (now - cacheTimestamp) < CACHE_DURATION) {
return ok({
headers: { 'Content-Type': 'application/xml' },
body: cachedSitemap
});
}
const result = await wixData.query('Products')
.eq('isPublished', true)
.limit(1000)
.find();
const baseUrl = 'https://www.yourstore.com';
let xml = '<?xml version="1.0" encoding="UTF-8"?>';
xml += '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
result.items.forEach(item => {
xml += '<url>';
xml += '<loc>' + baseUrl + '/products/' + item.slug + '</loc>';
xml += '<lastmod>' + new Date(item._updatedDate).toISOString().split('T')[0] + '</lastmod>';
xml += '</url>';
});
xml += '</urlset>';
cachedSitemap = xml;
cacheTimestamp = now;
return ok({
headers: { 'Content-Type': 'application/xml' },
body: xml
});
}Server-Side SEO Power
HTTP functions and routers are the most underused SEO tools in the Velo ecosystem. Most Wix developers never touch them, which means most Wix sites miss out on custom sitemaps, intelligent redirects, and server-side logic that can give you a significant competitive edge. If you are comfortable with JavaScript, these backend tools transform Wix from a simple website builder into a fully programmable SEO platform.
Complete How-To Guide: Building Custom HTTP Functions and Routers for SEO
This guide walks you through creating custom sitemaps, programmatic redirect systems, and intelligent 404 handling using Velo HTTP functions and routers on your Wix site.
How to implement server-side SEO tools with Velo
- 1Step 1: Enable Velo on your Wix site. In the code editor, navigate to the backend folder. Create a new file called http-functions.js if it does not already exist. This is where all your HTTP function endpoints will live.
- 2Step 2: Build your first custom sitemap function. Import ok, notFound, and serverError from wix-http-functions and wixData from wix-data. Create an exported async function named get_customSitemap that queries your Products collection filtered by isPublished equals true.
- 3Step 3: In the sitemap function, build an XML string following the sitemaps.org protocol. Start with the XML declaration and urlset element. Loop through query results, adding a url element for each item with loc, lastmod (from the item _updatedDate), changefreq, and priority values.
- 4Step 4: Return the XML using ok() with Content-Type set to application/xml. Wrap the entire function in a try-catch block returning serverError on failure. Test the endpoint by visiting yoursite.com/_functions/customSitemap.
- 5Step 5: For sites with more than 1000 URLs, build a sitemap index function. Create get_sitemapIndex that lists individual sitemap files for each collection. Each individual sitemap function accepts a page query parameter for pagination with 1000 items per page.
- 6Step 6: Add caching to your sitemap function. Declare a module-level variable for the cached XML and a timestamp. On each request, check if the cache is still valid (less than 1 hour old). Serve the cached version if valid, otherwise regenerate and update the cache.
- 7Step 7: Build a CMS-driven redirect system. Create a Redirects collection in your CMS with fields for sourceSlug, destinationUrl, redirectType (301 or 302), and isActive boolean. Create a router file (routers.js) that queries this collection on each request.
- 8Step 8: In your router function, first check the Redirects collection for a matching source slug. If found, return redirect() with the destination URL and the specified redirect type. If not found, proceed to query the main content collection for the requested item.
- 9Step 9: Implement intelligent 404 handling. When no matching item is found and no redirect exists, log the broken URL to a BrokenUrls collection with the requested path, referrer, timestamp, and a resolved boolean set to false.
- 10Step 10: Before returning the 404 response, query your main collection for the 5 most recent published items. Pass these as suggested content to a custom 404 page template using notFound('custom-404-page', { suggestedPosts: relatedItems }).
- 11Step 11: Submit your custom sitemap URL to Google Search Console. Navigate to Sitemaps in GSC and add yoursite.com/_functions/customSitemap. Monitor the submission status and fix any reported errors.
- 12Step 12: Set up a monthly routine to review the BrokenUrls collection. Sort by frequency of hits and create redirects for the most-requested broken URLs. Check if any represent content gaps that you should fill by creating new pages at those URLs.
HTTP Function Testing
Test HTTP functions by visiting the endpoint URL directly in your browser. For XML responses like sitemaps, use a browser extension or copy the URL into an XML validator to verify the output is well-formed. For router functions, test with both valid and invalid slugs to verify the redirect, content serving, and 404 handling logic all work correctly.
