Skip to content

Mel000000/weweb-dynamic-metadata

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

62 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

weweb-dynamic-metadata

⭐ Build-time SEO metadata generator for WeWeb static exports

npm version npm downloads License: MIT GitHub stars

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.


Table of Contents


Overview

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.

Repository Structure

  • πŸ“¦ This Package: The npm package that does the metadata generation
  • πŸ“¦ Your WeWeb Project: Where you install and run it

Key challenges addressed:

  • Complete generation in ~1 second for 100 articles
  • Zero runtime overhead - all metadata pre-generated
  • Tiny footprint - only ~500 bytes per article

Why This Package Matters

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

Who This Is For

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

Why This Project Exists

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

Usage Example

  1. Export your WeWeb project (creates dist/ folder)
  2. Create weweb.config.js in your project root
  3. Run npx @mel000000/weweb-dynamic-metadata or npx weweb-metadata
  4. 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

Features

  • πŸš€ 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

Quick Start

# 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 metadata

Getting Started

Prerequisites Checklist

Before 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)

Database Schema Requirements

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

Setup

1. Configure Supabase

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);

2. Get Your Supabase API Key

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.

3. Set Up Environment Variables

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

⚠️ Important: Never commit your .env file to version control. Add it to your .gitignore:

# .gitignore
.env

The package uses dotenv to automatically load these environment variables when you run the generator.

4. Create Config File

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
      }
    }
  ]
};

5. Run the Generator

# One-time generation
npx @mel000000/weweb-dynamic-metadata

How It Works

1. Reads Your Config

The package reads weweb.config.js from your project root to understand your Supabase connection and dynamic routes.

2. Discovers Content IDs

Fetches all IDs from your Supabase table to know which articles need metadata.

3. Fetches Metadata

For each ID, retrieves the metadata fields you specified (title, content, image, etc.).

4. Generates Central Metadata

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
};

5. Injects Script into Template

Adds the metadata injector script to your your-page-name/_param/index.html template.

6. Creates Reference Files

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>

Architecture

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
Loading

Project Folder Transformation

Before: WeWeb Export (No Metadata)

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.


After: Package Runs

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!

Output Summary

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                            β•‘
β•šβ•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•β•

Programmatic Usage

import { processFiles } from '@mel000000/weweb-dynamic-metadata';

const result = await processFiles();
console.log(`Generated ${result.totalMetadataEntries} metadata entries`);
console.log(`Took ${result.duration} seconds`);

Why Not Cloudflare Workers?

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

Troubleshooting

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)

License

This project is licensed under the MIT License. See the LICENSE file for details.

About

Build-time SEO metadata generator for WeWeb static exports. Zero runtime overhead, perfect SEO, completely free.

Topics

Resources

License

Code of conduct

Stars

Watchers

Forks

Packages

 
 
 

Contributors