una icon indicating copy to clipboard operation
una copied to clipboard

The current Open Graph (`og:`) and Twitter metadata generated by UNA often has formatting issues in the `description` field

Open BrownEbee opened this issue 3 months ago • 2 comments

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

  1. Whitespace normalization

    • Collapses multiple spaces and line breaks into single spaces.
    • Ensures consistent spacing after punctuation (.?!:).
  2. Run-on word correction

    • Adds missing spaces between lowercase+uppercase transitions (e.g., wellBeingwell Being).
  3. Sentence / bullet separation

    • If multiple sentences or items follow without punctuation, they are separated with a middle dot () for clarity.
  4. 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 "...".
  5. Title cleaning

    • Strips tags and normalizes whitespace.
    • Ensures Open Graph and Twitter titles are consistent.
  6. 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 .= ''; `

BrownEbee avatar Sep 30 '25 23:09 BrownEbee

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" />';

`

BrownEbee avatar Oct 01 '25 06:10 BrownEbee

Thank you @BrownEbee Could you please open Pull Request with proposed changes ?

AlexTr avatar Oct 01 '25 10:10 AlexTr