β Build-time SEO metadata generator for WeWeb static exports
A build-time tool that generates unique SEO metadata for each dynamic page in your WeWeb project. Zero runtime overhead, perfect SEO, and completely free.
- Overview
- Why This Package Matters
- Who This Is For
- Why This Project Exists
- Usage Example
- Architecture
- Features
- Quick Start
- Prerequisites Checklist
- Database Schema Requirements
- Setup
- How It Works
- Project Folder Transformation
- Output Summary
- Programmatic Usage
- Why Not Cloudflare Workers?
- Troubleshooting
- License
WeWeb exports static HTML files where all dynamic routes (like /article/1, /article/2) share the exact same HTML template with identical metadata. This is terrible for SEO - every article page looks identical to search engines.
This package solves that by generating a central metadata file and tiny reference HTML files that point to your main template. The result? Each article page gets its own unique metadata while maintaining a single source of truth.
- π¦ This Package: The npm package that does the metadata generation
- π¦ Your WeWeb Project: Where you install and run it
- Complete generation in ~1 second for 100 articles
- Zero runtime overhead - all metadata pre-generated
- Tiny footprint - only ~500 bytes per article
WeWeb currently has no built-in solution for dynamic SEO metadata. Every dynamic page shares the same HTML template, making SEO optimization impossible.
This package provides a production-tested solution that:
- Gives each page unique metadata
- Requires no serverless functions
- Works with any hosting platform
- Preserves all WeWeb dynamic functionality
This package is ideal for:
- WeWeb users needing SEO for dynamic pages
- Developers deploying WeWeb to any static host
- Projects requiring unique metadata per page
- Teams wanting zero-latency SEO solution
This package may NOT be ideal if:
- Your content changes every minute (use server-side instead)
- You can't run build scripts in your deployment
- You need real-time metadata updates
WeWeb's dynamic pages share HTML templates, making unique metadata impossible. Official solutions require Cloudflare Workers (cost, complexity, latency).
This project provides a simpler, cheaper, faster alternative:
- Zero runtime costs - Everything runs at build time
- Perfect SEO - Instant HTML for crawlers
- Dead simple - Just a config file and one command
- Works everywhere - Any static hosting works
- Export your WeWeb project (creates
dist/folder) - Create
weweb.config.jsin your project root - Run
npx @mel000000/weweb-dynamic-metadataornpx weweb-metadata - Deploy anywhere - each article now has unique metadata!
# One-time setup
npm install --save-dev @mel000000/weweb-dynamic-metadata
# Generate metadata (run after each WeWeb export)
npx @mel000000/weweb-dynamic-metadata
# That's it! Your pages now have unique SEO metadata- π Zero Runtime Overhead: All metadata pre-generated at build time
- π¦ Tiny Footprint: Only ~500 bytes per article (reference files)
- π― Perfect SEO: Each page gets unique titles, descriptions, and Open Graph tags
- π§ Simple Setup: Just add a config file and run
- πΈ Completely Free: No Cloudflare Workers, no serverless costs
- π Works Everywhere: Deploy to any static hosting (Netlify, Vercel, GitHub Pages, S3, etc.)
- β‘ Fast: Generates 1000 articles in ~3 seconds
- π Commit Traceability: Optional git-info.js injection for deploy visibility
# 1. Install the package
npm install --save-dev @mel000000/weweb-dynamic-metadata
# 2. Create weweb.config.js and env-File in your project root
# (see Setup section below)
# 3. Run it!
npx @mel000000/weweb-dynamic-metadata
# Done! Your pages now have unique metadataBefore using this package, ensure you have:
- A WeWeb project exported to static files (has
your-page-name/_param/index.html) - Node.js 18 or higher installed
- A Supabase project with your content
- Your Supabase URL and anon key ready
- Your WeWeb build folder (usually
dist/or project root)
| Field | Purpose | Required | Notes |
|---|---|---|---|
title |
Page title for SEO and social sharing | β Yes | Used for <title>, og:title, twitter:title |
description |
Page description for search results and social sharing | β Yes | Used for meta description, og:description, twitter:description |
content |
Alternative to description |
β Optional | If description is not set, falls back to content |
image |
Featured image for social sharing | β Optional | Used for og:image, twitter:image |
image_url |
Alternative to image |
β Optional | If image is not set, falls back to image_url |
Create a view in your Supabase database for optimal performance:
-- Create a view for article metadata
CREATE VIEW article_metadata AS
SELECT
id,
title,
LEFT(content, 160) AS excerpt, -- First 160 chars for descriptions
image_url
FROM articles;
-- Enable public read access
ALTER VIEW article_metadata ENABLE ROW LEVEL SECURITY;
CREATE POLICY "Allow public read access"
ON article_metadata
FOR SELECT
TO anon
USING (true);In your Supabase dashboard, go to Project Settings > API Keys. You'll see two types of keys:
| Key Type | Format | When to Use |
|---|---|---|
| Project URL | https://your-project.supabase.co |
Always needed |
| anon / publishable key | sb_publishable_... or JWT |
Legacy option (being phased out) |
| secret key (recommended) | sb_secret_... |
β Recommended for new projects |
Note: Supabase is transitioning away from the legacy anon key. For new projects, use the secret key (starts with
sb_secret_...). If you're using an older project, the anon key will continue working for now, but consider migrating to the new key format.
Create a .env file in your project root to store your Supabase credentials securely:
# .env file
SUPABASE_URL=https://your-project.supabase.co
SUPABASE_KEY=your-secret-or-anon-key-here# .gitignore
.env
The package uses dotenv to automatically load these environment variables when you run the generator.
Create weweb.config.js in your project root:
export default {
supabase: {
url: process.env.SUPABASE_URL,
anonKey: process.env.SUPABASE_KEY // Works with both anon and secret keys
},
outputDir: "./",
pages: [
{
route: "/article/:id",
table: "article_metadata", // Your Supabase table name
metadata: {
title: "title", // Required: maps to window.METADATA[id].title
description: "excerpt", // Required: maps to window.METADATA[id].description
image: "featured_image" // Optional: maps to window.METADATA[id].image
}
},
{
route: "/product/:id",
table: "products_meta",
metadata: {
title: "title",
description: "excerpt",
author: "brand" // Ooptional: maps to window.METADATA[id].author
}
}
]
};# One-time generation
npx @mel000000/weweb-dynamic-metadata
The package reads weweb.config.js from your project root to understand your Supabase connection and dynamic routes.
Fetches all IDs from your Supabase table to know which articles need metadata.
For each ID, retrieves the metadata fields you specified (title, content, image, etc.).
Creates a central JavaScript file with all metadata:
window.METADATA = {
"1": { title: "Article 1", content: "...", image: "..." },
"2": { title: "Article 2", content: "...", image: "..." },
// ... one entry per article
};Adds the metadata injector script to your your-page-name/_param/index.html template.
Generates tiny HTML files for each article that load the template and pass the article ID:
<!-- article/1/index.html - only ~500 bytes! -->
<script>
window.__REFERENCE_CONTENT_ID = "1";
fetch('../_param/index.html')
.then(response => response.text())
.then(html => {
document.open();
document.write(html);
document.close();
});
</script>flowchart TD
%% Styles
classDef setup fill:#2da44e,stroke:#1a7f37,color:#ffffff
classDef core fill:#0969da,stroke:#0550ae,color:#ffffff
classDef output fill:#8250df,stroke:#6639ba,color:#ffffff
classDef runtime fill:#f66a0a,stroke:#bf4e00,color:#ffffff
classDef note fill:#fff,stroke:#6e7781,color:#24292f,stroke-width:1px,stroke-dasharray:3 3
subgraph Setup ["π’ User Setup (One Time)"]
A["User exports WeWeb project<br/>creates static files"] --> B
B["User creates weweb.config.js<br/>in project root"] --> C
C["Configure:<br/>β’ Supabase URL & anonKey<br/>β’ Dynamic routes (/article/:id)<br/>β’ Table & metadata fields<br/>β’ Optional: outputDir"] --> D
D["Save config file"]
end
class A,B,C,D setup
subgraph Build ["π΅ Build Time - Metadata Generation"]
direction TB
E["Run: npx @mel000000/weweb-dynamic-metadata"] --> F
F["Read weweb.config.js"] --> G
G["Validate configuration"] --> I["For each dynamic route:<br/>e.g., /article/:id"]
I --> J["Discover content IDs<br/>GET /rest/v1/table?select=id"]
J --> K["IDs: [1, 2, 3, ...]"]
K --> L["Fetch metadata for each ID<br/>GET /rest/v1/table?id=eq.{id}"]
L --> M["Build central metadata object<br/>{1: {...}, 2: {...}, ...}"]
M --> N["Generate article/metadata.js<br/>window.METADATA = {...}"]
N --> O["Locate WeWeb template<br/>article/_param/index.html"]
O --> P["Inject metadata script into template"]
P --> Q["For each ID, create reference file:<br/>article/1/index.html<br/>article/2/index.html<br/>..."]
Q --> R["Copy metadata.js to _param/<br/>for backward compatibility"]
end
class E,F,G,I,J,K,L,M,N,O,P,Q,R core
subgraph Output ["π£ Generated Output"]
S["π article/<br/>βββ metadata.js<br/>βββ _param/<br/>β βββ index.html (modified)<br/>β βββ metadata.js<br/>βββ 1/<br/>β βββ index.html (reference)<br/>β βββ metadata.js<br/>βββ 2/<br/>β βββ index.html (reference)<br/>β βββ metadata.js<br/>βββ ..."]
end
class S output
subgraph Runtime ["π Runtime - Browser"]
T["User visits /article/2"] --> U
U["Browser loads article/2/index.html<br/>(tiny reference file)"] --> V
V["JavaScript sets window.CURRENT_ARTICLE_ID=2<br/>and loads _param/index.html"] --> W
W["Main template loads with ID=2"] --> X
X["Metadata injector reads window.METADATA[2]<br/>and updates page metadata"] --> Y
Y["Page displays with correct title,<br/>description, Open Graph tags"]
end
class T,U,V,W,X,Y runtime
%% Connections
D ==> E
R ==> S
S ==> T
%% Legend
L1["π’ User Setup: One-time configuration"]:::note
L2["π΅ Build Time: Generates metadata + reference files"]:::note
L3["π£ Output: Generated files ready for deployment"]:::note
L4["π Runtime: Browser loads and applies metadata"]:::note
L5["βοΈ weweb.config.js: Central configuration file"]:::note
L1 ~~~ L2 ~~~ L3 ~~~ L4 ~~~ L5
dist/ (or your build folder)
βββ your-page-name/
β βββ _param/
β βββ index.html # Same for ALL articles!
βββ assets/
βββ index.html
βββ ...
The Problem: Every article at /your-page-name/1, /your-page-name/2, etc. serves the EXACT same HTML file with identical metadata.
dist/
βββ your-page-name/
β βββ metadata.js # Central metadata for all articles
β βββ _param/
β β βββ index.html # Original template (script injected)
β βββ 1/
β β βββ index.html # Tiny reference file (~500 bytes)
β βββ 2/
β β βββ index.html # Tiny reference file
β βββ ...
βββ assets/
βββ index.html
The Solution: Each article now has its own unique metadata while sharing the same template!
After running, you'll get a JSON summary:
{
"timestamp": "2024-03-14T20:49:28.825Z",
"pages": [
{
"route": "/article/:id",
"total": 150,
"succeeded": 150,
"failed": 0,
"metadataEntries": 150,
"referencesCreated": 150
}
],
"totalMetadataEntries": 150,
"outputDirectories": ["dist/article"],
"duration": "2.34"
}and an overview in the console:
[dotenv@17.3.1] injecting env (2) from .env -- tip: π‘οΈ auth for agents: https://vestauth.com
π WeWeb Dynamic Metadata Generator
βοΈ Metadata injector already present in: article\_param\index.html
π§Ή Found 2 duplicate injectors, cleaning up...
ββββββββββββββββββββββββββββββββββββββββββββββββββ
βπ GENERATION COMPLETE β
ββββββββββββββββββββββββββββββββββββββββββββββββββ’
β β±οΈ Duration: 1.31s β
β π Total entries: 9 β
β π Output: article β
ββββββββββββββββββββββββββββββββββββββββββββββββββ
import { processFiles } from '@mel000000/weweb-dynamic-metadata';
const result = await processFiles();
console.log(`Generated ${result.totalMetadataEntries} metadata entries`);
console.log(`Took ${result.duration} seconds`);| Approach | This Package | Cloudflare Worker |
|---|---|---|
| Runtime | None (pre-generated) | Each request |
| Latency | 0ms | +100-300ms |
| Cost | Free | Pay per request |
| SEO | Perfect - instant HTML | Crawlers might timeout |
| Complexity | Simple config | Worker deployment |
| Scaling | Infinite (static files) | Worker limits |
| Cold starts | None | Possible |
| Issue | Likely Cause | Solution |
|---|---|---|
getOutputDir: Could not find build folder |
No WeWeb export found | Run weweb export first or set outputDir in config |
Config error: Invalid or unexpected token |
BOM characters in config | Recreate file without BOM (use VSCode "Save with Encoding β UTF-8") |
| No metadata generated | Supabase connection issue | Check your Supabase URL and anon/secret key |
Failed to fetch ID |
Table or field names wrong | Verify table and field names in config |
| Reference files not created | Permission issues | Check write permissions in build folder |
| Metadata appears (og:url, canonical) but title/description missing | Database field names don't match what the injector expects | Ensure your config maps database fields to title and description (not name or excerpt) |
This project is licensed under the MIT License. See the LICENSE file for details.