From 5329bccdeca1a4f56d0ba625f263242285a5ea22 Mon Sep 17 00:00:00 2001 From: Russell Ballestrini Date: Mon, 15 Dec 2025 08:17:01 -0500 Subject: [PATCH] Add Open Graph and Twitter Card meta tags for social sharing - Add helper functions to extract first image and generate description from chat messages - Update base.html and index.html with og:image, og:description, twitter:card meta tags - Chat rooms now use first image from messages for unfurling instead of site logo - Description shows first 500 chars of room content for link previews - Add default OG image (black with OC) as fallback when no image found --- app.py | 126 ++++++++++++++++++++++++++++++++++- static/images/og-default.png | Bin 0 -> 10117 bytes templates/base.html | 27 ++++++++ templates/index.html | 18 +++++ 4 files changed, 170 insertions(+), 1 deletion(-) create mode 100644 static/images/og-default.png diff --git a/app.py b/app.py index 9fb3915..f39ab66 100644 --- a/app.py +++ b/app.py @@ -369,6 +369,115 @@ def build_message_content(msg, is_vision: bool, room_id: int = None) -> dict | s return msg.content +def extract_first_image_for_og(room_id: int) -> str | None: + """Extract the first image URL from messages for Open Graph meta tags. + + Checks messages in the room for: + 1. Base64 images (returns as data URL - may be large) + 2. External image URLs in markdown format ![alt](url) + 3. External image URLs in img tags + + Returns image URL/data URL or None if no image found. + """ + import re + + messages = Message.query.filter_by(room_id=room_id).order_by(Message.id.asc()).limit(50).all() + + for msg in messages: + if not msg.content: + continue + + # Check for base64 image + if msg.is_base64_image(): + img_data = extract_base64_from_img_tag(msg.content) + if img_data: + media_type, base64_data = img_data + return f"data:{media_type};base64,{base64_data}" + + # Check for markdown image ![alt](url) + md_img_match = re.search(r'!\[[^\]]*\]\(([^)]+)\)', msg.content) + if md_img_match: + url = md_img_match.group(1) + if url.startswith(('http://', 'https://')): + return url + + # Check for img tag with src + img_src_match = re.search(r']+src=["\']([^"\']+)["\']', msg.content) + if img_src_match: + url = img_src_match.group(1) + if url.startswith(('http://', 'https://')): + return url + + return None + + +def generate_og_description(room, max_chars: int = 500) -> str: + """Generate a description for Open Graph meta tags. + + Uses room title if available, then extracts text from first few messages. + Strips markdown/code/images and limits to max_chars. + """ + import re + + parts = [] + + # Start with room title if available + if room and room.title: + parts.append(room.title) + + # Get first few messages for description + if room: + messages = Message.query.filter_by(room_id=room.id).order_by(Message.id.asc()).limit(10).all() + + for msg in messages: + if not msg.content: + continue + + # Skip base64 images + if msg.is_base64_image(): + continue + + text = msg.content + + # Remove code blocks + text = re.sub(r'```[\s\S]*?```', '', text) + text = re.sub(r'`[^`]+`', '', text) + + # Remove markdown images + text = re.sub(r'!\[[^\]]*\]\([^)]+\)', '', text) + + # Remove HTML tags + text = re.sub(r'<[^>]+>', '', text) + + # Remove markdown links but keep text + text = re.sub(r'\[([^\]]+)\]\([^)]+\)', r'\1', text) + + # Remove markdown headers + text = re.sub(r'^#{1,6}\s+', '', text, flags=re.MULTILINE) + + # Remove bold/italic markers + text = re.sub(r'\*{1,2}([^*]+)\*{1,2}', r'\1', text) + text = re.sub(r'_{1,2}([^_]+)_{1,2}', r'\1', text) + + # Clean up whitespace + text = re.sub(r'\s+', ' ', text).strip() + + if text: + parts.append(text) + + # Stop if we have enough content + if len(' '.join(parts)) > max_chars: + break + + description = ' '.join(parts) + + # Truncate to max_chars + if len(description) > max_chars: + description = description[:max_chars - 3].rsplit(' ', 1)[0] + '...' + + return description if description else "AI-powered chat room on OpenCompletion" + + def get_openai_client_and_model( model_name="adamo1139/Hermes-3-Llama-3.1-8B-FP8-Dynamic", ): @@ -1345,6 +1454,18 @@ def chat(room_name): # Use authenticated user's display name, or None (will prompt on client side) username = user.display_name if user else None + # Generate Open Graph metadata for social sharing + og_image = None + og_description = "AI-powered chat room on OpenCompletion" + og_title = f"{room_name} - OpenCompletion" + + if room: + # Try to get first image from messages + og_image = extract_first_image_for_og(room.id) + og_description = generate_og_description(room) + if room.title: + og_title = f"{room.title} - OpenCompletion" + # Pass username, rooms, room (current room), and user into the template return render_template( "chat.html", @@ -1353,7 +1474,10 @@ def chat(room_name): public_rooms=public_rooms, private_rooms=private_rooms, username=username, - user=user + user=user, + og_title=og_title, + og_description=og_description, + og_image=og_image ) diff --git a/static/images/og-default.png b/static/images/og-default.png new file mode 100644 index 0000000000000000000000000000000000000000..eeb5d07484a09304464d80d7d87b30c1106ff8b9 GIT binary patch literal 10117 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 Ti>+hm>hyKaoyyR {% block title %}Chatroom{% endblock %} + + + + + + + + + + {% if og_image %} + + + {% else %} + + + {% endif %} + + + + + + {% if og_image %} + + {% else %} + + {% endif %} +