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
This commit is contained in:
Russell Ballestrini 2025-12-15 08:17:01 -05:00
parent 50404f62d9
commit 5329bccdec
4 changed files with 170 additions and 1 deletions

126
app.py
View file

@ -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'<img[^>]+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
)

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.9 KiB

View file

@ -4,6 +4,33 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{% block title %}Chatroom{% endblock %}</title>
<!-- Meta description for SEO -->
<meta name="description" content="{{ og_description|default('OpenCompletion - AI-powered collaborative chat rooms for machine learning') }}">
<!-- Open Graph meta tags for social sharing (Facebook, Discord, LinkedIn, etc.) -->
<meta property="og:type" content="website">
<meta property="og:title" content="{{ og_title|default('OpenCompletion - AI-Powered Chat Rooms') }}">
<meta property="og:description" content="{{ og_description|default('OpenCompletion - AI-powered collaborative chat rooms for machine learning') }}">
<meta property="og:site_name" content="OpenCompletion">
{% if og_image %}
<meta property="og:image" content="{{ og_image }}">
<meta property="og:image:alt" content="Preview image for {{ og_title|default('OpenCompletion') }}">
{% else %}
<meta property="og:image" content="{{ url_for('static', filename='images/og-default.png', _external=True) }}">
<meta property="og:image:alt" content="OpenCompletion logo">
{% endif %}
<!-- Twitter Card meta tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="{{ og_title|default('OpenCompletion - AI-Powered Chat Rooms') }}">
<meta name="twitter:description" content="{{ og_description|default('OpenCompletion - AI-powered collaborative chat rooms for machine learning') }}">
{% if og_image %}
<meta name="twitter:image" content="{{ og_image }}">
{% else %}
<meta name="twitter:image" content="{{ url_for('static', filename='images/og-default.png', _external=True) }}">
{% endif %}
<script>
// Set theme immediately to prevent flash
(function() {

View file

@ -4,6 +4,24 @@
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>OpenCompletion - AI-Powered Chat Rooms</title>
<!-- Meta description for SEO -->
<meta name="description" content="OpenCompletion - Machine learning powered collaboration. Create public or private AI chat rooms, collaborate with AI models, and share conversations.">
<!-- Open Graph meta tags for social sharing (Facebook, Discord, LinkedIn, etc.) -->
<meta property="og:type" content="website">
<meta property="og:title" content="OpenCompletion - AI-Powered Chat Rooms">
<meta property="og:description" content="Machine learning powered collaboration. Create public or private AI chat rooms, collaborate with AI models, and share conversations.">
<meta property="og:site_name" content="OpenCompletion">
<meta property="og:image" content="{{ url_for('static', filename='images/og-default.png', _external=True) }}">
<meta property="og:image:alt" content="OpenCompletion - AI-Powered Chat Rooms">
<!-- Twitter Card meta tags -->
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="OpenCompletion - AI-Powered Chat Rooms">
<meta name="twitter:description" content="Machine learning powered collaboration. Create public or private AI chat rooms, collaborate with AI models, and share conversations.">
<meta name="twitter:image" content="{{ url_for('static', filename='images/og-default.png', _external=True) }}">
<link rel="icon" href="{{ url_for('static', filename='favicon.ico') }}">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<script src="https://cdnjs.cloudflare.com/ajax/libs/socket.io/4.0.1/socket.io.min.js"></script>