Dynamic Metadata through Cloudflare don't appear

i figured it out, it now works for me.

i’m using the cloudflare reverse proxy. i just followed the guide, even if some cloudflare UI was updated, it stays more or less the same. biggest struggles were:

  • figure out how to make it work with supabase
  • fix the pattern matching in the config to skip css and js files, or page load would break
  • use the cloudflare Routes to avoid touching the DNS

all the meta tags in the head are updated with dynamic content that is in my db, you can check the head source code of eg this page https://www.nomadretreats.co/experience/workation-in-tursi-italy-in-may-by-tursi-digital-nomads/

only one caveat. even if my reverse proxy updates the title tag, weweb dynamically overwrites it after page loads. i’ll try to open a bug or get more info on how to prevent this.

How to get it to work

Replace these lines of the original code

with something like this:

      // Fetch metadata from the API endpoint
      try {
        const metaDataResponse = await fetch(metaDataEndpointWithId, {
          method: 'POST',
          headers: {
            'Authorization': 'Bearer ' + env.SUPABASE_KEY, // Set this in the Cloudflare Worker settings
            'Content-Type': 'application/json'
          },
          body: JSON.stringify({ name: 'Functions' })
        });

        // Check if the response is valid JSON
        if (!metaDataResponse.ok) {
          console.error(`Error fetching metadata: ${metaDataResponse.status} ${metaDataResponse.statusText}`);
          const errorText = await metaDataResponse.text();
          console.error(`Response body: ${errorText}`);
          throw new Error(`Failed to fetch metadata: ${metaDataResponse.status}`);
        }

        return (await metaDataResponse.json());
      } catch (error) {
        console.error('Error in requestMetadata:', error);
        throw error;
      }
    }
    // END of async function requestMetadata(url, metaDataEndpoint)

Supabase Edge function

import "jsr:@supabase/functions-js/edge-runtime.d.ts";
import { createClient } from 'npm:@supabase/supabase-js@2';
Deno.serve(async (req)=>{
  const corsHeaders = {
    'Access-Control-Allow-Origin': '*',
    'Access-Control-Allow-Methods': 'GET, OPTIONS',
    'Access-Control-Allow-Headers': 'Authorization, Content-Type'
  };
  if (req.method === 'OPTIONS') {
    return new Response('ok', {
      headers: corsHeaders
    });
  }
  try {
    const url = new URL(req.url);
    // Remove any trailing slash from the URL
    const trimmedPath = url.pathname.endsWith('/') ? url.pathname.slice(0, -1) : url.pathname;
    // Extract the slug from the URL
    const slug = trimmedPath.split('/').pop();
    const supabase = createClient(Deno.env.get('SUPABASE_URL') ?? '', Deno.env.get('SUPABASE_ANON_KEY') ?? '', {
      global: {
        headers: {
          Authorization: req.headers.get('Authorization')
        }
      }
    });
    const { data, error } = await supabase.from('your-meta-table').select('meta_title, meta_description, media_ids, tags').eq('slug', slug); // Query by slug
    if (error) {
      throw new Error(`Error fetching experience: ${error.message}`);
    }

// I have images in another table, used for the opengraph preview (not working yet)
    // Fetch media URLs based on media_id
    const mediaIds = data.flatMap((item)=>item.media_ids) || [];
    const { data: mediaData, error: mediaError } = await supabase.from('media').select('id, public_url').eq('id', mediaIds[0]);
    if (mediaError) {
      throw new Error(`Error fetching media: ${mediaError.message}`);
    }
    // Create a map of media IDs to URLs for easy lookup
    const mediaMap = Object.fromEntries(mediaData.map((item)=>[
        item.id,
        item.public_url
      ]));
    // Format the response to match the desired JSON structure
    const formattedData = data.map((item)=>({
        title: item.meta_title,
        description: item.meta_description,
        image: mediaMap[item.media_ids[0]],
        keywords: item.tags ? item.tags.join(", ") : ""
      }));
    // It's an array with one element, take first element
    return new Response(JSON.stringify(formattedData[0]), {
      headers: {
        'Content-Type': 'application/json',
        ...corsHeaders
      },
      status: 200
    });
  } catch (err) {
    return new Response(JSON.stringify({
      message: err.message
    }), {
      headers: {
        'Content-Type': 'application/json',
        ...corsHeaders
      },
      status: 500
    });
  }
});

Config file

i had to touch the config file compared to the one in the guide. the pattern with the starting ^ is matching only the right urls. this was critical to make it work for me or the worker would run on css and js files, breaking the whole page loading.

export const config = {
    domainSource: "https://your-preview-weweb-url-123456.weweb-preview.io", // Your WeWeb app preview link
    patterns: [
        {
            // the initial "^" is important to match only the page url, not its styles and scripts that share a similar url
            pattern: "^/provider/[^/]+",
            metaDataEndpoint: "https://your-supabase-url.supabase.co/functions/v1/getMetaProvider/{slug}"
        },
        {
            pattern: "^/experience/[^/]+",
            metaDataEndpoint: "https://your-supabase-url.supabase.co/functions/v1/getMetaExperience/{slug}"
        }
        // Add more patterns and their metadata endpoints as needed
    ]
};

DNS

you don’t need to add a dns like in the guide, a routes option is now available in the workers config. (there’s no reason why one has www and one *, i was just lazy)

my dns for the www (the only one i have) stays the same
image

3 Likes