diff --git a/source/packages/com_mokosuitecross/src/Controller/PreviewController.php b/source/packages/com_mokosuitecross/src/Controller/PreviewController.php
new file mode 100644
index 00000000..7dc45502
--- /dev/null
+++ b/source/packages/com_mokosuitecross/src/Controller/PreviewController.php
@@ -0,0 +1,86 @@
+
+ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+namespace Joomla\Component\MokoSuiteCross\Administrator\Controller;
+
+defined('_JEXEC') or die;
+
+use Joomla\CMS\Factory;
+use Joomla\CMS\MVC\Controller\BaseController;
+use Joomla\CMS\Session\Session;
+use Joomla\CMS\Uri\Uri;
+use Joomla\Component\MokoSuiteCross\Administrator\Helper\CrossPostDispatcher;
+use Joomla\Component\MokoSuiteCross\Administrator\Helper\PreviewHelper;
+
+class PreviewController extends BaseController
+{
+ public function render(): void
+ {
+ if (!Session::checkToken('get')) {
+ echo json_encode(['error' => 'Invalid token']);
+ $this->app->close();
+
+ return;
+ }
+
+ $articleId = $this->input->getInt('article_id', 0);
+ $platform = $this->input->getCmd('platform', 'twitter');
+
+ if ($articleId < 1) {
+ echo json_encode(['error' => 'Missing article ID']);
+ $this->app->close();
+
+ return;
+ }
+
+ $db = Factory::getDbo();
+
+ $query = $db->getQuery(true)
+ ->select('*')
+ ->from($db->quoteName('#__content'))
+ ->where($db->quoteName('id') . ' = ' . $articleId);
+ $db->setQuery($query);
+ $article = $db->loadObject();
+
+ if (!$article) {
+ echo json_encode(['error' => 'Article not found']);
+ $this->app->close();
+
+ return;
+ }
+
+ $meta = CrossPostDispatcher::buildArticleMeta($article);
+
+ $title = $meta['{title}'] ?? '';
+ $text = $meta['{introtext}'] ?? '';
+ $url = $meta['{url}'] ?? '';
+ $imageUrl = $meta['{image}'] ?? '';
+ $authorName = $meta['{author}'] ?? '';
+
+ $supportedPlatforms = PreviewHelper::getSupportedPlatforms();
+ $html = '';
+
+ if ($platform === 'all') {
+ foreach ($supportedPlatforms as $p) {
+ $html .= '
'
+ . '
' . htmlspecialchars(ucfirst($p)) . '
'
+ . PreviewHelper::render($p, $title, $text, $url, $imageUrl, $authorName)
+ . '
';
+ }
+ } else {
+ $html = PreviewHelper::render($platform, $title, $text, $url, $imageUrl, $authorName);
+ }
+
+ $this->app->setHeader('Content-Type', 'text/html; charset=utf-8');
+ echo $html;
+ $this->app->close();
+ }
+}
diff --git a/source/packages/com_mokosuitecross/src/Helper/PreviewHelper.php b/source/packages/com_mokosuitecross/src/Helper/PreviewHelper.php
new file mode 100644
index 00000000..fc0fd0ad
--- /dev/null
+++ b/source/packages/com_mokosuitecross/src/Helper/PreviewHelper.php
@@ -0,0 +1,207 @@
+
+ * @copyright Copyright (C) 2026 Moko Consulting. All rights reserved.
+ * @license GNU General Public License version 3 or later; see LICENSE
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+
+namespace Joomla\Component\MokoSuiteCross\Administrator\Helper;
+
+defined('_JEXEC') or die;
+
+class PreviewHelper
+{
+ public static function render(string $platform, string $title, string $text, string $url, string $imageUrl = '', string $authorName = ''): string
+ {
+ $title = htmlspecialchars($title, ENT_QUOTES, 'UTF-8');
+ $text = htmlspecialchars($text, ENT_QUOTES, 'UTF-8');
+ $url = htmlspecialchars($url, ENT_QUOTES, 'UTF-8');
+ $authorName = htmlspecialchars($authorName, ENT_QUOTES, 'UTF-8');
+
+ $imageHtml = '';
+
+ if (!empty($imageUrl)) {
+ $imageUrl = htmlspecialchars($imageUrl, ENT_QUOTES, 'UTF-8');
+ $imageHtml = '
';
+ }
+
+ return match ($platform) {
+ 'twitter' => self::renderTwitter($title, $text, $url, $imageHtml, $authorName),
+ 'facebook' => self::renderFacebook($title, $text, $url, $imageHtml, $authorName),
+ 'mastodon' => self::renderMastodon($title, $text, $url, $imageHtml, $authorName),
+ 'linkedin' => self::renderLinkedIn($title, $text, $url, $imageHtml, $authorName),
+ 'bluesky' => self::renderBluesky($title, $text, $url, $imageHtml, $authorName),
+ 'telegram' => self::renderTelegram($title, $text, $url, $imageHtml),
+ default => self::renderGeneric($platform, $title, $text, $url, $imageHtml),
+ };
+ }
+
+ public static function getSupportedPlatforms(): array
+ {
+ return ['twitter', 'facebook', 'mastodon', 'linkedin', 'bluesky', 'telegram'];
+ }
+
+ private static function renderTwitter(string $title, string $text, string $url, string $imageHtml, string $author): string
+ {
+ $displayText = !empty($text) ? $text : $title;
+ $charCount = mb_strlen(strip_tags($displayText));
+
+ return <<
+
+
X
+
+
{$author}
+
@username
+
+
+ {$displayText}
+ {$imageHtml}
+
+
yoursite.com
+
{$title}
+
+ {$charCount}/280
+
+HTML;
+ }
+
+ private static function renderFacebook(string $title, string $text, string $url, string $imageHtml, string $author): string
+ {
+ $displayText = !empty($text) ? $text : $title;
+
+ return <<
+
+ {$imageHtml}
+
+
yoursite.com
+
{$title}
+
+
+HTML;
+ }
+
+ private static function renderMastodon(string $title, string $text, string $url, string $imageHtml, string $author): string
+ {
+ $displayText = !empty($text) ? $text : $title;
+ $charCount = mb_strlen(strip_tags($displayText));
+
+ return <<
+
+
M
+
+
{$author}
+
@user@mastodon.social
+
+
+ {$displayText}
+ {$imageHtml}
+
+
{$title}
+
yoursite.com
+
+ {$charCount}/500
+
+HTML;
+ }
+
+ private static function renderLinkedIn(string $title, string $text, string $url, string $imageHtml, string $author): string
+ {
+ $displayText = !empty($text) ? $text : $title;
+
+ return <<
+
+ {$imageHtml}
+
+
{$title}
+
yoursite.com
+
+
+HTML;
+ }
+
+ private static function renderBluesky(string $title, string $text, string $url, string $imageHtml, string $author): string
+ {
+ $displayText = !empty($text) ? $text : $title;
+ $charCount = mb_strlen(strip_tags($displayText));
+
+ return <<
+
+
B
+
+
{$author}
+
@user.bsky.social
+
+
+ {$displayText}
+ {$imageHtml}
+
+
{$title}
+
yoursite.com
+
+ {$charCount}/300
+
+HTML;
+ }
+
+ private static function renderTelegram(string $title, string $text, string $url, string $imageHtml): string
+ {
+ $displayText = !empty($text) ? $text : $title;
+
+ return <<
+ {$imageHtml}
+ {$displayText}
+
+
{$title}
+
yoursite.com
+
+ Just now
+
+HTML;
+ }
+
+ private static function renderGeneric(string $platform, string $title, string $text, string $url, string $imageHtml): string
+ {
+ $platformLabel = htmlspecialchars(ucfirst($platform), ENT_QUOTES, 'UTF-8');
+ $displayText = !empty($text) ? $text : $title;
+
+ return <<
+ {$platformLabel}
+ {$displayText}
+ {$imageHtml}
+
+
{$title}
+
yoursite.com
+
+
+HTML;
+ }
+}
diff --git a/source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.ini b/source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.ini
index 50ff4171..990c1de2 100644
--- a/source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.ini
+++ b/source/packages/plg_content_mokosuitecross/language/en-GB/plg_content_mokosuitecross.ini
@@ -31,3 +31,6 @@ PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_CUSTOM="Custom Image"
PLG_CONTENT_MOKOSUITECROSS_SHARE_IMAGE_NONE="No Image"
PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE="Custom Share Image"
PLG_CONTENT_MOKOSUITECROSS_CUSTOM_IMAGE_DESC="Select an image from the media manager to use for cross-posting."
+
+PLG_CONTENT_MOKOSUITECROSS_FIELDSET_PREVIEW="Social Preview"
+PLG_CONTENT_MOKOSUITECROSS_PREVIEW="Preview Post"
diff --git a/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php
index 4f863368..376b4003 100644
--- a/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php
+++ b/source/packages/plg_content_mokosuitecross/src/Extension/MokoSuiteCrossContent.php
@@ -19,6 +19,7 @@ use Joomla\CMS\Factory;
use Joomla\CMS\Form\Form;
use Joomla\CMS\HTML\HTMLHelper;
use Joomla\CMS\Plugin\CMSPlugin;
+use Joomla\CMS\Session\Session;
use Joomla\CMS\Uri\Uri;
use Joomla\Component\MokoSuiteCross\Administrator\Helper\CrossPostDispatcher;
use Joomla\Event\SubscriberInterface;
@@ -265,6 +266,47 @@ XML;
$form->load($historyXml);
$form->setFieldAttribute('mokosuitecross_history', 'description', $historyHtml, 'attribs');
}
+
+ // Social Preview panel (#156)
+ $token = Session::getFormToken();
+ $previewUrl = Uri::base() . 'index.php?option=com_mokosuitecross&task=preview.render&format=raw&article_id=' . $articleId . '&' . $token . '=1';
+
+ $previewButtonHtml = ''
+ . '
'
+ . ' '
+ . ''
+ . '
'
+ . '
'
+ . '
'
+ . '';
+
+ $previewXml = '
+';
+ $form->load($previewXml);
+ $form->setFieldAttribute('mokosuitecross_preview_panel', 'description', $previewButtonHtml, 'attribs');
}
}