Skip to content

Commit eb78296

Browse files
committed
Adds opengraph example
1 parent 606f91f commit eb78296

File tree

6 files changed

+450
-0
lines changed

6 files changed

+450
-0
lines changed

11-opengraph/README.md

Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
# OpenGraph Meta Tag Injection Example
2+
3+
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.
4+
5+
## What It Does
6+
7+
The Worker:
8+
1. **Receives a request** for a specific URL path (e.g., `/blog/my-article`)
9+
2. **Generates OpenGraph metadata** dynamically based on the path
10+
3. **Fetches the original HTML** from your target website
11+
4. **Uses Cloudflare's HTMLRewriter** to inject OpenGraph meta tags into the HTML `<head>` section
12+
5. **Returns the enhanced HTML** with proper social media preview tags
13+
14+
This example showcases how to use Cloudflare's powerful HTMLRewriter API from Python Workers via the `js` module interop.
15+
16+
## How to Run
17+
18+
First ensure that `uv` is installed:
19+
https://docs.astral.sh/uv/getting-started/installation/#standalone-installer
20+
21+
Now, if you run `uv run pywrangler dev` within this directory, it should use the config
22+
in `wrangler.jsonc` to run the example.
23+
24+
```bash
25+
uv run pywrangler dev
26+
```
27+
28+
Then visit:
29+
- `http://localhost:8787/` - Home page with default metadata
30+
- `http://localhost:8787/blog/python-workers-intro` - Blog post example
31+
- `http://localhost:8787/products/awesome-widget` - Product page example
32+
- `http://localhost:8787/about` - About page example
33+
34+
## Deployment
35+
36+
Deploy to Cloudflare Workers:
37+
38+
```bash
39+
uv run pywrangler deploy
40+
```
41+
42+
## Customization
43+
44+
To adapt this example for your own website:
45+
46+
1. **Update the target URL** in `src/entry.py`:
47+
```python
48+
target_url = f"https://your-website.com{path}"
49+
```
50+
51+
2. **Customize metadata patterns** in the `get_opengraph_data()` method:
52+
```python
53+
if path.startswith("/your-section/"):
54+
og_data.update({
55+
"title": "Your Custom Title",
56+
"description": "Your custom description",
57+
"image": "https://your-image-url.com/image.jpg"
58+
})
59+
```
60+
61+
3. **Add more URL patterns** to match your site structure
62+
63+
## Testing Your OpenGraph Tags
64+
65+
Use these tools to validate your OpenGraph tags:
66+
- [Facebook Sharing Debugger](https://developers.facebook.com/tools/debug/)
67+
- [X Card Validator](https://cards-dev.x.com/validator)
68+
- [LinkedIn Post Inspector](https://www.linkedin.com/post-inspector/)

11-opengraph/package.json

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"name": "python-opengraph",
3+
"version": "0.0.0",
4+
"private": true,
5+
"scripts": {
6+
"deploy": "uv run pywrangler deploy",
7+
"dev": "uv run pywrangler dev",
8+
"start": "uv run pywrangler dev"
9+
},
10+
"devDependencies": {
11+
"wrangler": "^4.46.0"
12+
}
13+
}

11-opengraph/pyproject.toml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
[project]
2+
name = "python-opengraph"
3+
version = "0.1.0"
4+
description = "Python opengraph example"
5+
readme = "README.md"
6+
requires-python = ">=3.12"
7+
dependencies = [
8+
"webtypy>=0.1.7",
9+
]
10+
11+
[dependency-groups]
12+
dev = [
13+
"workers-py",
14+
"workers-runtime-sdk"
15+
]

11-opengraph/src/entry.py

Lines changed: 174 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,174 @@
1+
from workers import WorkerEntrypoint, Request, fetch
2+
from js import HTMLRewriter
3+
from urllib.parse import urlparse
4+
from html import escape
5+
6+
from pyodide.ffi import create_proxy
7+
8+
9+
class MetaTagInjector:
10+
"""
11+
Element handler for HTMLRewriter that injects OpenGraph meta tags.
12+
Uses Python's html.escape() for proper HTML escaping.
13+
"""
14+
15+
def __init__(self, og_data: dict):
16+
self.og_data = og_data
17+
self.injected = False
18+
19+
def element(self, element):
20+
"""Called when the <head> element is encountered."""
21+
if not self.injected:
22+
# Create and inject meta tags
23+
self._inject_meta_tags(element)
24+
self.injected = True
25+
26+
def _inject_meta_tags(self, head_element):
27+
"""Inject OpenGraph and Twitter Card meta tags."""
28+
# OpenGraph tags
29+
self._create_meta(head_element, "property", "og:title", self.og_data["title"])
30+
self._create_meta(
31+
head_element, "property", "og:description", self.og_data["description"]
32+
)
33+
self._create_meta(head_element, "property", "og:image", self.og_data["image"])
34+
self._create_meta(head_element, "property", "og:url", self.og_data["url"])
35+
self._create_meta(head_element, "property", "og:type", self.og_data["type"])
36+
self._create_meta(
37+
head_element, "property", "og:site_name", self.og_data["site_name"]
38+
)
39+
40+
# Twitter Card tags
41+
self._create_meta(head_element, "name", "twitter:card", "summary_large_image")
42+
self._create_meta(head_element, "name", "twitter:title", self.og_data["title"])
43+
self._create_meta(
44+
head_element, "name", "twitter:description", self.og_data["description"]
45+
)
46+
self._create_meta(head_element, "name", "twitter:image", self.og_data["image"])
47+
48+
def _create_meta(self, head_element, attr_name: str, attr_value: str, content: str):
49+
"""
50+
Create a meta tag and prepend it to the head element.
51+
Uses Python's html.escape() for proper attribute escaping.
52+
"""
53+
# Use Python's built-in html.escape() which handles all necessary escaping
54+
escaped_attr_value = escape(attr_value, quote=True)
55+
escaped_content = escape(content, quote=True)
56+
meta_html = (
57+
f'<meta {attr_name}="{escaped_attr_value}" content="{escaped_content}" />'
58+
)
59+
head_element.prepend(meta_html, html=True)
60+
61+
62+
class ExistingMetaRemover:
63+
"""
64+
Element handler that removes existing OpenGraph and Twitter meta tags.
65+
"""
66+
67+
def element(self, element):
68+
"""Remove the element by calling remove()."""
69+
element.remove()
70+
71+
72+
class Default(WorkerEntrypoint):
73+
"""
74+
OpenGraph Meta Tag Injection Example
75+
76+
This Worker fetches a web page and injects OpenGraph meta tags
77+
based on the request path using Cloudflare's HTMLRewriter API.
78+
"""
79+
80+
async def fetch(self, request: Request):
81+
# Parse the request path to determine which page we're serving
82+
url = urlparse(request.url)
83+
path = url.path
84+
85+
# Define OpenGraph metadata based on the path
86+
og_data = self.get_opengraph_data(path)
87+
88+
# Fetch the original HTML from a target website
89+
# In this example, we'll use example.com, but you can replace this
90+
# with your actual website URL
91+
#
92+
# Note that this isn't necessary if your worker will also be serving
93+
# content of your website, in that case you should already have the HTML
94+
# you're returning ready to go here.
95+
target_url = f"https://example.com{path}"
96+
97+
# Fetch the original page
98+
response = await fetch(target_url)
99+
100+
# Use HTMLRewriter to inject OpenGraph meta tags
101+
rewritten_response = self.inject_opengraph_tags(response, og_data)
102+
103+
return rewritten_response
104+
105+
def get_opengraph_data(self, path: str) -> dict:
106+
"""
107+
Generate OpenGraph metadata based on the request path.
108+
Customize this function to match your site's structure.
109+
"""
110+
# Default metadata
111+
og_data = {
112+
"title": "My Awesome Website",
113+
"description": "Welcome to my website built with Python Workers!",
114+
"image": "https://images.unsplash.com/photo-1518770660439-4636190af475",
115+
"url": f"https://yoursite.com{path}",
116+
"type": "website",
117+
"site_name": "Python Workers Demo",
118+
}
119+
120+
# Customize based on path
121+
if path.startswith("/blog/"):
122+
article_slug = path.replace("/blog/", "").strip("/")
123+
og_data.update(
124+
{
125+
"title": f"Blog Post: {article_slug.replace('-', ' ').title()}",
126+
"description": f"Read our latest article about {article_slug.replace('-', ' ')}",
127+
"image": "https://images.unsplash.com/photo-1499750310107-5fef28a66643",
128+
"type": "article",
129+
}
130+
)
131+
elif path.startswith("/products/"):
132+
product_slug = path.replace("/products/", "").strip("/")
133+
og_data.update(
134+
{
135+
"title": f"Product: {product_slug.replace('-', ' ').title()}",
136+
"description": f"Check out our amazing {product_slug.replace('-', ' ')} product",
137+
"image": "https://images.unsplash.com/photo-1505740420928-5e560c06d30e",
138+
"type": "product",
139+
}
140+
)
141+
elif path == "/about":
142+
og_data.update(
143+
{
144+
"title": "About Us - Python Workers",
145+
"description": "Learn more about our team and what we do with Python Workers",
146+
"image": "https://images.unsplash.com/photo-1522071820081-009f0129c71c",
147+
}
148+
)
149+
150+
return og_data
151+
152+
def inject_opengraph_tags(self, response, og_data: dict):
153+
"""
154+
Use HTMLRewriter to inject OpenGraph meta tags into the HTML response.
155+
Removes existing OG tags first to avoid duplicates.
156+
"""
157+
# Create an HTMLRewriter instance
158+
rewriter = HTMLRewriter.new()
159+
160+
meta_remover = create_proxy(ExistingMetaRemover())
161+
meta_injector = create_proxy(MetaTagInjector(og_data))
162+
163+
try:
164+
rewriter = HTMLRewriter.new()
165+
# Remove existing OpenGraph and Twitter meta tags to avoid duplicates
166+
rewriter.on('meta[property^="og:"]', meta_remover.copy())
167+
rewriter.on('meta[name^="twitter:"]', meta_remover.copy())
168+
# Inject new OpenGraph meta tags into the <head> element
169+
rewriter.on("head", meta_injector.copy())
170+
171+
return rewriter.transform(response.js_object)
172+
finally:
173+
meta_remover.destroy()
174+
meta_injector.destroy()

0 commit comments

Comments
 (0)