From 7592ede17cd0e3e1fd99b5383fa3b63355e329e4 Mon Sep 17 00:00:00 2001 From: Dominik Picheta Date: Tue, 18 Nov 2025 11:43:31 +0000 Subject: [PATCH] Adds opengraph example --- 11-opengraph/README.md | 68 +++++++++++++++ 11-opengraph/package.json | 13 +++ 11-opengraph/pyproject.toml | 15 ++++ 11-opengraph/src/entry.py | 170 ++++++++++++++++++++++++++++++++++++ 11-opengraph/wrangler.jsonc | 12 +++ 5 files changed, 278 insertions(+) create mode 100644 11-opengraph/README.md create mode 100644 11-opengraph/package.json create mode 100644 11-opengraph/pyproject.toml create mode 100644 11-opengraph/src/entry.py create mode 100644 11-opengraph/wrangler.jsonc diff --git a/11-opengraph/README.md b/11-opengraph/README.md new file mode 100644 index 0000000..4d64bb4 --- /dev/null +++ b/11-opengraph/README.md @@ -0,0 +1,68 @@ +# OpenGraph Meta Tag Injection Example + +This example demonstrates how to build a Python Worker that dynamically injects OpenGraph meta tags into web pages based on the request path. This is perfect for controlling how your content appears when shared on social media platforms like Facebook, Twitter, LinkedIn, and Slack. + +## What It Does + +The Worker: +1. **Receives a request** for a specific URL path (e.g., `/blog/my-article`) +2. **Generates OpenGraph metadata** dynamically based on the path +3. **Fetches the original HTML** from your target website +4. **Uses Cloudflare's HTMLRewriter** to inject OpenGraph meta tags into the HTML `` section +5. **Returns the enhanced HTML** with proper social media preview tags + +This example showcases how to use Cloudflare's powerful HTMLRewriter API from Python Workers via the `js` module interop. + +## How to Run + +First ensure that `uv` is installed: +https://docs.astral.sh/uv/getting-started/installation/#standalone-installer + +Now, if you run `uv run pywrangler dev` within this directory, it should use the config +in `wrangler.jsonc` to run the example. + +```bash +uv run pywrangler dev +``` + +Then visit: +- `http://localhost:8787/` - Home page with default metadata +- `http://localhost:8787/blog/python-workers-intro` - Blog post example +- `http://localhost:8787/products/awesome-widget` - Product page example +- `http://localhost:8787/about` - About page example + +## Deployment + +Deploy to Cloudflare Workers: + +```bash +uv run pywrangler deploy +``` + +## Customization + +To adapt this example for your own website: + +1. **Update the target URL** in `src/entry.py`: + ```python + target_url = f"https://your-website.com{path}" + ``` + +2. **Customize metadata patterns** in the `get_opengraph_data()` method: + ```python + if path.startswith("/your-section/"): + og_data.update({ + "title": "Your Custom Title", + "description": "Your custom description", + "image": "https://your-image-url.com/image.jpg" + }) + ``` + +3. **Add more URL patterns** to match your site structure + +## Testing Your OpenGraph Tags + +Use these tools to validate your OpenGraph tags: +- [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/) +- [X Card Validator](https://cards-dev.x.com/validator) +- [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/) diff --git a/11-opengraph/package.json b/11-opengraph/package.json new file mode 100644 index 0000000..b77afec --- /dev/null +++ b/11-opengraph/package.json @@ -0,0 +1,13 @@ +{ + "name": "python-opengraph", + "version": "0.0.0", + "private": true, + "scripts": { + "deploy": "uv run pywrangler deploy", + "dev": "uv run pywrangler dev", + "start": "uv run pywrangler dev" + }, + "devDependencies": { + "wrangler": "^4.46.0" + } +} diff --git a/11-opengraph/pyproject.toml b/11-opengraph/pyproject.toml new file mode 100644 index 0000000..252b233 --- /dev/null +++ b/11-opengraph/pyproject.toml @@ -0,0 +1,15 @@ +[project] +name = "python-opengraph" +version = "0.1.0" +description = "Python opengraph example" +readme = "README.md" +requires-python = ">=3.12" +dependencies = [ + "webtypy>=0.1.7", +] + +[dependency-groups] +dev = [ + "workers-py", + "workers-runtime-sdk" +] diff --git a/11-opengraph/src/entry.py b/11-opengraph/src/entry.py new file mode 100644 index 0000000..4b7f3a5 --- /dev/null +++ b/11-opengraph/src/entry.py @@ -0,0 +1,170 @@ +from workers import WorkerEntrypoint, Request, fetch +from js import HTMLRewriter +from urllib.parse import urlparse +from html import escape + +from pyodide.ffi import create_proxy + + +class MetaTagInjector: + """ + Element handler for HTMLRewriter that injects OpenGraph meta tags. + Uses Python's html.escape() for proper HTML escaping. + """ + + def __init__(self, og_data: dict): + self.og_data = og_data + self.injected = False + + def element(self, element): + """Called when the element is encountered.""" + if not self.injected: + # Create and inject meta tags + self._inject_meta_tags(element) + self.injected = True + + def _inject_meta_tags(self, head_element): + """Inject OpenGraph and Twitter Card meta tags.""" + # OpenGraph tags + self._create_meta(head_element, "property", "og:title", self.og_data["title"]) + self._create_meta( + head_element, "property", "og:description", self.og_data["description"] + ) + self._create_meta(head_element, "property", "og:image", self.og_data["image"]) + self._create_meta(head_element, "property", "og:url", self.og_data["url"]) + self._create_meta(head_element, "property", "og:type", self.og_data["type"]) + self._create_meta( + head_element, "property", "og:site_name", self.og_data["site_name"] + ) + + # Twitter Card tags + self._create_meta(head_element, "name", "twitter:card", "summary_large_image") + self._create_meta(head_element, "name", "twitter:title", self.og_data["title"]) + self._create_meta( + head_element, "name", "twitter:description", self.og_data["description"] + ) + self._create_meta(head_element, "name", "twitter:image", self.og_data["image"]) + + def _create_meta(self, head_element, attr_name: str, attr_value: str, content: str): + """ + Create a meta tag and prepend it to the head element. + Uses Python's html.escape() for proper attribute escaping. + """ + # Use Python's built-in html.escape() which handles all necessary escaping + escaped_attr_value = escape(attr_value, quote=True) + escaped_content = escape(content, quote=True) + meta_html = ( + f'' + ) + head_element.prepend(meta_html, html=True) + + +class ExistingMetaRemover: + """ + Element handler that removes existing OpenGraph and Twitter meta tags. + """ + + def element(self, element): + """Remove the element by calling remove().""" + element.remove() + + +class Default(WorkerEntrypoint): + """ + OpenGraph Meta Tag Injection Example + + This Worker fetches a web page and injects OpenGraph meta tags + based on the request path using Cloudflare's HTMLRewriter API. + """ + + async def fetch(self, request: Request): + # Parse the request path to determine which page we're serving + url = urlparse(request.url) + path = url.path + + # Define OpenGraph metadata based on the path + og_data = self.get_opengraph_data(path) + + # Fetch the original HTML from a target website + # In this example, we'll use example.com, but you can replace this + # with your actual website URL + # + # Note that this isn't necessary if your worker will also be serving + # content of your website, in that case you should already have the HTML + # you're returning ready to go here. + target_url = f"https://example.com{path}" + + # Fetch the original page + response = await fetch(target_url) + + # Use HTMLRewriter to inject OpenGraph meta tags + rewritten_response = self.inject_opengraph_tags(response, og_data) + + return rewritten_response + + def get_opengraph_data(self, path: str) -> dict: + """ + Generate OpenGraph metadata based on the request path. + Customize this function to match your site's structure. + """ + # Default metadata + og_data = { + "title": "My Awesome Website", + "description": "Welcome to my website built with Python Workers!", + "image": "https://images.unsplash.com/photo-1518770660439-4636190af475", + "url": f"https://yoursite.com{path}", + "type": "website", + "site_name": "Python Workers Demo", + } + + # Customize based on path + if path.startswith("/blog/"): + article_slug = path.replace("/blog/", "").strip("/") + og_data.update( + { + "title": f"Blog Post: {article_slug.replace('-', ' ').title()}", + "description": f"Read our latest article about {article_slug.replace('-', ' ')}", + "image": "https://images.unsplash.com/photo-1499750310107-5fef28a66643", + "type": "article", + } + ) + elif path.startswith("/products/"): + product_slug = path.replace("/products/", "").strip("/") + og_data.update( + { + "title": f"Product: {product_slug.replace('-', ' ').title()}", + "description": f"Check out our amazing {product_slug.replace('-', ' ')} product", + "image": "https://images.unsplash.com/photo-1505740420928-5e560c06d30e", + "type": "product", + } + ) + elif path == "/about": + og_data.update( + { + "title": "About Us - Python Workers", + "description": "Learn more about our team and what we do with Python Workers", + "image": "https://images.unsplash.com/photo-1522071820081-009f0129c71c", + } + ) + + return og_data + + def inject_opengraph_tags(self, response, og_data: dict): + """ + Use HTMLRewriter to inject OpenGraph meta tags into the HTML response. + Removes existing OG tags first to avoid duplicates. + """ + # Create an HTMLRewriter instance + rewriter = HTMLRewriter.new() + + meta_remover = create_proxy(ExistingMetaRemover()) + meta_injector = create_proxy(MetaTagInjector(og_data)) + + rewriter = HTMLRewriter.new() + # Remove existing OpenGraph and Twitter meta tags to avoid duplicates + rewriter.on('meta[property^="og:"]', meta_remover) + rewriter.on('meta[name^="twitter:"]', meta_remover) + # Inject new OpenGraph meta tags into the element + rewriter.on("head", meta_injector) + + return rewriter.transform(response.js_object) diff --git a/11-opengraph/wrangler.jsonc b/11-opengraph/wrangler.jsonc new file mode 100644 index 0000000..42be06b --- /dev/null +++ b/11-opengraph/wrangler.jsonc @@ -0,0 +1,12 @@ +{ + "$schema": "node_modules/wrangler/config-schema.json", + "name": "python-opengraph", + "main": "src/entry.py", + "compatibility_date": "2025-11-02", + "compatibility_flags": [ + "python_workers" + ], + "observability": { + "enabled": true + } +}