The current Open Graph (`og:`) and Twitter metadata generated by UNA often has formatting issues in the `description` field
Purpose of Update
The current Open Graph (og:) and Twitter metadata generated by UNA often has formatting issues in the description field:
- Missing spaces between sentences when line breaks separate them.
- Run-on words when lowercase letters are followed by uppercase letters without spacing.
- Descriptions cut off abruptly without clear indication.
This update introduces cleaning and normalization logic for titles and descriptions before they are injected into meta tags.
Key Improvements
-
Whitespace normalization
- Collapses multiple spaces and line breaks into single spaces.
- Ensures consistent spacing after punctuation (
.?!:).
-
Run-on word correction
- Adds missing spaces between lowercase+uppercase transitions (e.g.,
wellBeing→well Being).
- Adds missing spaces between lowercase+uppercase transitions (e.g.,
-
Sentence / bullet separation
- If multiple sentences or items follow without punctuation, they are separated with a middle dot (
•) for clarity.
- If multiple sentences or items follow without punctuation, they are separated with a middle dot (
-
Ellipsis handling
- If the description is truncated or does not end with punctuation,
"..."is appended. - If description length exceeds 200 characters (safe for Facebook/WhatsApp preview), it is cut cleanly and suffixed with
"...".
- If the description is truncated or does not end with punctuation,
-
Title cleaning
- Strips tags and normalizes whitespace.
- Ensures Open Graph and Twitter titles are consistent.
-
Generic and neutral fallback
- If description is truly missing, no hardcoded fallback text is injected.
- This avoids project-specific branding (so UNA instances remain clean and customizable).
Benefits
- Improved sharing previews on Facebook, Twitter (X), WhatsApp, Telegram, and other platforms.
- Consistent formatting across all UNA apps (Spaces, Events, Groups, Photos, etc.).
- SEO friendliness by ensuring titles and descriptions remain readable and properly spaced.
- Future-proof: Adds safeguards without locking UNA into project-specific slogans.
👉 Recommendation: Replace the existing Facebook / Twitter Open Graph Metadata block in BxDolTemplate.php with the provided drop-in replacement to standardize metadata across UNA installs.
`// ========================================== // Facebook / Twitter Open Graph Metadata // ==========================================
// Image $bPageImage = !empty($this->aPage['image']); $sRet .= ''; if ($bPageImage) { $sRet .= ''; }
// ========================================== // Title with improved whitespace // ========================================== $sHeader = ''; if (isset($this->aPage['header'])) { $sRawTitle = strip_tags($this->aPage['header']); $sRawTitle = preg_replace('/\s+/', ' ', $sRawTitle); // collapse spaces/newlines $sRawTitle = preg_replace('/([a-z0-9])([A-Z])/', '$1 $2', $sRawTitle); // fix run-on words $sHeader = trim($sRawTitle); } $sRet .= ''; $sRet .= '';
// ========================================== // Description with punctuation/spacing fixes // ========================================== $sCleanDesc = ''; if ($bDescription) { $sRawDesc = strip_tags($sDescription);
// Normalize whitespace
$sRawDesc = preg_replace('/\s+/', ' ', $sRawDesc);
// Fix missing space after punctuation (.?!:)
$sRawDesc = preg_replace('/([.?!:])(\S)/u', '$1 $2', $sRawDesc);
// Fix run-on lowercase+uppercase
$sRawDesc = preg_replace('/([a-z0-9])([A-Z])/', '$1 $2', $sRawDesc);
// Replace multiple sentences without punctuation → bullet separators
$sRawDesc = preg_replace('/\s+(?=[A-Z])/', ' • ', $sRawDesc);
$sCleanDesc = trim($sRawDesc);
}
// If still empty, do not inject fallback text if ($sCleanDesc !== '') { // Limit length (safe for FB/WA, prevents cut-off mid-word) $maxLen = 200; if (mb_strlen($sCleanDesc) > $maxLen) { $sCleanDesc = mb_substr($sCleanDesc, 0, $maxLen - 3) . '...'; } else { // If description does not end with proper punctuation, add "..." if (!preg_match('/[.!?]$/u', $sCleanDesc)) { $sCleanDesc .= '...'; } }
$sRet .= '<meta property="og:description" content="' . bx_html_attribute($sCleanDesc) . '" />';
$sRet .= '<meta name="twitter:description" content="' . bx_html_attribute($sCleanDesc) . '" />';
}
// ========================================== // Canonical URL // ========================================== if (!empty($this->aPage['url'])) { $sRet .= ''; }
// ========================================== // Dynamic og:type detection // ========================================== $sUri = bx_get('i') ?: (isset($_GET['i']) ? $_GET['i'] : ''); $ogType = 'website'; // default
if (strpos($sUri, 'view-video') !== false) { $ogType = 'video.other'; } elseif (strpos($sUri, 'view-publication') !== false) { $ogType = 'article'; } elseif (strpos($sUri, 'view-event') !== false) { $ogType = 'event'; } elseif (strpos($sUri, 'view-space-profile') !== false) { $ogType = 'profile'; } elseif (strpos($sUri, 'view-organization-profile') !== false) { $ogType = 'profile'; } elseif (strpos($sUri, 'view-group-profile') !== false) { $ogType = 'profile'; } elseif (strpos($sUri, 'view-band-profile') !== false) { $ogType = 'profile'; } elseif (strpos($sUri, 'view-course') !== false) { $ogType = 'article'; } elseif (strpos($sUri, 'view-album') !== false) { $ogType = 'album'; } elseif (strpos($sUri, 'view-channel') !== false) { $ogType = 'video.other'; } elseif (strpos($sUri, 'view-photo') !== false) { $ogType = 'image'; } elseif (strpos($sUri, 'view-discussion') !== false) { $ogType = 'article'; } elseif (strpos($sUri, 'view-faq') !== false) { $ogType = 'article'; } elseif (strpos($sUri, 'view-ad') !== false) { $ogType = 'product'; }
$sRet .= ''; `
Final version....
` // ========================================== // Facebook / Twitter Open Graph Metadata // ==========================================
// Image
$bPageImage = !empty($this->aPage['image']);
$sRet .= '<meta name="twitter:card" content="' . ($bPageImage ? 'summary_large_image' : 'summary') . '" />';
if ($bPageImage) {
$sRet .= '<meta property="og:image" content="' . $this->aPage['image'] . '" />';
}
// ==========================================
// Title with improved whitespace
// ==========================================
$sHeader = '';
if (isset($this->aPage['header'])) {
$sRawTitle = strip_tags($this->aPage['header']);
$sRawTitle = preg_replace('/\s+/', ' ', $sRawTitle); // collapse spaces/newlines
$sRawTitle = preg_replace('/([a-z0-9])([A-Z])/', '$1 $2', $sRawTitle); // fix run-on words
$sHeader = trim($sRawTitle);
}
$sRet .= '<meta property="og:title" content="' . bx_html_attribute($sHeader) . '" />';
$sRet .= '<meta name="twitter:title" content="' . bx_html_attribute($sHeader) . '" />';
// ==========================================
// Description with improved whitespace, smart separators & ellipsis
// ==========================================
$sCleanDesc = '';
if ($bDescription) {
$sRawDesc = $sDescription;
// Convert HTML breaks/lists to spaces before stripping tags
$sRawDesc = preg_replace('#<(?:br\s*/?|/p|/li)>#i', ' ', $sRawDesc);
// Strip tags
$sRawDesc = strip_tags($sRawDesc);
// Normalize whitespace
$sRawDesc = preg_replace('/\s+/u', ' ', $sRawDesc);
// Ensure space after punctuation if missing (".This" → ". This")
$sRawDesc = preg_replace('/([.,;:])([^\s])/u', '$1 $2', $sRawDesc);
// Insert "|" only when no punctuation separator exists
$sRawDesc = preg_replace('/(?<![.,;:])([a-z0-9\)])([A-Z])/u', '$1 | $2', $sRawDesc);
$sRawDesc = preg_replace('/(?<![.,;:])([a-z0-9\)])\s+([A-Z])/u', '$1 | $2', $sRawDesc);
$sCleanDesc = trim($sRawDesc);
}
// Fallback if empty
if ($sCleanDesc === '') {
$sCleanDesc = "Content unavailable";
}
// Prevent mid-word truncation + clean ellipsis
$maxLen = 300; // adjust if needed
if (mb_strlen($sCleanDesc) > $maxLen) {
$sTruncated = mb_substr($sCleanDesc, 0, $maxLen);
$sTruncated = preg_replace('/\s+\S*$/u', '', $sTruncated); // trim partial word
$sTruncated = rtrim($sTruncated, " ,;:-"); // remove dangling punctuation
$sCleanDesc = $sTruncated . '...';
}
$sRet .= '<meta property="og:description" content="' . bx_html_attribute($sCleanDesc) . '" />';
$sRet .= '<meta name="twitter:description" content="' . bx_html_attribute($sCleanDesc) . '" />';
// ==========================================
// Canonical URL
// ==========================================
if (!empty($this->aPage['url'])) {
$sRet .= '<meta property="og:url" content="' . bx_html_attribute($this->aPage['url']) . '" />';
}
// ==========================================
// Dynamic og:type detection
// ==========================================
$sUri = bx_get('i') ?: (isset($_GET['i']) ? $_GET['i'] : '');
$ogType = 'website'; // default
if (strpos($sUri, 'view-video') !== false) {
$ogType = 'video.other';
} elseif (strpos($sUri, 'view-publication') !== false) {
$ogType = 'article';
} elseif (strpos($sUri, 'view-event') !== false) {
$ogType = 'event';
} elseif (strpos($sUri, 'view-space-profile') !== false) {
$ogType = 'profile';
} elseif (strpos($sUri, 'view-organization-profile') !== false) {
$ogType = 'profile';
} elseif (strpos($sUri, 'view-group-profile') !== false) {
$ogType = 'profile';
} elseif (strpos($sUri, 'view-band-profile') !== false) {
$ogType = 'profile';
} elseif (strpos($sUri, 'view-course') !== false) {
$ogType = 'article';
} elseif (strpos($sUri, 'view-album') !== false) {
$ogType = 'album';
} elseif (strpos($sUri, 'view-channel') !== false) {
$ogType = 'video.other';
} elseif (strpos($sUri, 'view-photo') !== false) {
$ogType = 'image';
} elseif (strpos($sUri, 'view-discussion') !== false) {
$ogType = 'article';
} elseif (strpos($sUri, 'view-faq') !== false) {
$ogType = 'article';
} elseif (strpos($sUri, 'view-ad') !== false) {
$ogType = 'product';
}
$sRet .= '<meta property="og:type" content="' . $ogType . '" />';
// ==========================================
// Facebook App ID + Site Constants
// ==========================================
$sRet .= '<meta property="fb:app_id" content="YOUR_FB_APP_ID_HERE" />';
$sRet .= '<meta property="og:site_name" content="' . bx_html_attribute(getParam('site_title')) . '" />';
$sRet .= '<meta property="og:locale" content="en_US" />';
`
Thank you @BrownEbee Could you please open Pull Request with proposed changes ?