Static sites and SEO don't have to be mutually exclusive. In fact, for niche B2B SaaS, a well-structured static site with proper Schema.org markup can outrank dynamic apps that never thought about structured data. Here's exactly how I built a library of 50+ SEO-optimized pages for a financial services SaaS — and had them indexed within 48 hours.
The Problem: Niche B2B Pages Nobody Searches For (Until They Do)
If you're building for a niche audience — say, registered investment advisors who need compliant client letter templates — generic content marketing won't cut it. Your buyers search for specific things: "form CRS delivery letter template", "RIA privacy policy notice template". These are low-volume, high-intent queries.
The play: generate one highly-structured static page per query. No backend, no CMS. Just HTML with the right metadata.
Step 1: The HTML Template Structure
Every page follows the same skeleton. The key is semantic HTML + consistent <head> metadata:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<!-- Primary SEO -->
<title>Form CRS Delivery Letter Template for RIAs | RIALetters</title>
<meta name="description" content="Compliant Form CRS delivery letter template for registered investment advisors.">
<link rel="canonical" href="https://hlteoh37.github.io/ria-letters/form-crs-delivery-letter-ria">
<!-- Open Graph -->
<meta property="og:title" content="Form CRS Delivery Letter Template for RIAs">
<meta property="og:type" content="website">
<meta property="og:url" content="https://hlteoh37.github.io/ria-letters/form-crs-delivery-letter-ria">
<!-- Schema.org JSON-LD -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "SoftwareApplication",
"name": "RIALetters — Form CRS Delivery Letter",
"applicationCategory": "BusinessApplication",
"offers": {
"@type": "Offer",
"price": "49.00",
"priceCurrency": "USD"
},
"url": "https://hlteoh37.github.io/ria-letters/"
}
<\/script>
</head>
<body><!-- content --></body>
</html>
Step 2: Schema.org JSON-LD — What Actually Moves the Needle
Plain HTML tells Google what the page says. JSON-LD tells Google what the page is. For SaaS landing pages, combining SoftwareApplication + FAQPage schemas is the winning pattern:
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "FAQPage",
"mainEntity": [
{
"@type": "Question",
"name": "What is a Form CRS delivery letter?",
"acceptedAnswer": {
"@type": "Answer",
"text": "A Form CRS delivery letter is required by SEC regulations for RIAs to document when they delivered the Client Relationship Summary to a retail investor."
}
},
{
"@type": "Question",
"name": "How do I customize this template?",
"acceptedAnswer": {
"@type": "Answer",
"text": "Fill in your firm name, advisor name, client details, and delivery date. No legal modification needed for standard RIA use cases."
}
}
]
}
<\/script>
The FAQPage schema is the secret weapon — it triggers rich results in Google Search, showing expandable Q&A directly in the SERP. For niche queries, this often doubles click-through rate.
Step 3: Generating Pages at Scale with the Claude API
The template is fixed. The variable parts are: page title, meta description, H1, intro paragraph, and FAQ content. I used the Claude API to generate all variable content from a prompt template:
const Anthropic = require('@anthropic-ai/sdk');
const anthropic = new Anthropic();
const generatePageContent = async (letterType, audience) => {
const response = await anthropic.messages.create({
model: 'claude-opus-4-6',
max_tokens: 1024,
messages: [{
role: 'user',
content: `Generate SEO content for a B2B SaaS page.
Audience: ${audience}
Letter type: ${letterType}
Return JSON with: title (60 chars), metaDescription (155 chars),
h1, intro (2 sentences), faqs (3 x {question, answer})`
}]
});
return JSON.parse(response.content[0].text);
};
Then a build script injects content into the template:
const buildPage = (content, outputPath) => {
const html = PAGE_TEMPLATE
.replace(/{{TITLE}}/g, content.title)
.replace(/{{META_DESC}}/g, content.metaDescription)
.replace(/{{H1}}/g, content.h1)
.replace(/{{INTRO}}/g, content.intro)
.replace(/{{FAQ_JSON_LD}}/g, JSON.stringify(buildFaqSchema(content.faqs), null, 2))
.replace(/{{CANONICAL_URL}}/g, buildCanonicalUrl(outputPath));
fs.writeFileSync(outputPath, html);
};
const letterTypes = [
'Form CRS Delivery Letter',
'Fee Disclosure Letter',
'Privacy Policy Notice',
'IRA Distribution Letter',
'RMD Required Minimum Distribution Letter',
// ... 45 more
];
(async () => {
for (const lt of letterTypes) {
const content = await generatePageContent(lt, 'registered investment advisors');
const slug = lt.toLowerCase().replace(/\s+/g, '-');
buildPage(content, `public/${slug}-ria.html`);
console.log(`Built: ${slug}-ria.html`);
}
})();
Step 4: GitHub Pages Deployment
For static sites at this scale, GitHub Pages is the right call:
- Free hosting, no bandwidth limits on public repos
- HTTPS by default (direct Google ranking signal)
- Custom domain support via CNAME
- Deploys on every
git push— zero CI config needed
Repo structure:
repo/
index.html # Hub page
sitemap.xml # Auto-generated
form-crs-delivery-letter-ria.html
fee-disclosure-letter-ria.html
privacy-policy-letter-ria.html
rmd-required-minimum-distribution-letter-ria.html
# ... 46 more
Submitting to Google's Indexing API
Don't wait for organic crawl. Submit immediately after deploy:
# After verifying your property in Search Console:
for slug in form-crs-delivery-letter fee-disclosure privacy-policy; do
curl -s -X POST \
"https://indexing.googleapis.com/v3/urlNotifications:publish" \
-H "Authorization: Bearer $(gcloud auth print-access-token)" \
-H "Content-Type: application/json" \
-d "{\"url\": \"https://yourdomain.github.io/your-site/${slug}\",
\"type\": \"URL_UPDATED\"}"
done
Free tier allows 200 URL submissions/day — more than enough for 50 pages.
Step 5: The Sitemap Generator
const generateSitemap = (pages, baseUrl) => {
const today = new Date().toISOString().split('T')[0];
const urls = pages.map(p => `
<url>
<loc>${baseUrl}/${p.slug}</loc>
<lastmod>${today}</lastmod>
<changefreq>monthly</changefreq>
<priority>${p.isHub ? '1.0' : '0.8'}</priority>
</url>`).join('');
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls}
</urlset>`;
};
fs.writeFileSync('public/sitemap.xml', generateSitemap(allPages, BASE_URL));
Reference it in every page <head> and submit the sitemap URL in Google Search Console.
Real Results
The live example is RIALetters — a niche SaaS for registered investment advisors who need SEC-compliant letter templates. After deploying with Schema.org markup and submitting the sitemap:
- 48 hours: First pages appeared in Google's index
- 1 week: Rich FAQ results showing in SERPs for letter-type queries
- Zero ad spend: 100% organic traffic
The niche is everything here. Broad terms like "letter templates" would take months. But "form CRS delivery letter RIA" — a query with real purchase intent and near-zero competition — now has a structured-data-rich page answering it precisely.
Takeaways
- JSON-LD is mandatory for B2B SaaS — it communicates page semantics to Google at scale
- FAQPage schema triggers rich results; implement it programmatically for every page
- GitHub Pages + Indexing API gets you indexed in 48 hours, not 2 weeks
- One page per keyword intent beats one page trying to rank for everything
- Claude API + template injection scales 50+ pages in a few hours of work
This approach works best when your product maps to many specific queries — compliance tools, HR templates, legal docs, API documentation, and similar process-heavy B2B products all fit this pattern perfectly.
The full technique generalizes far beyond financial services. Anywhere there are many specific, low-competition, high-intent searches, this pattern will outperform generic content marketing.




