Merge pull request 'feat: social image generator with GD text overlay' (#213) from feature/157-social-image-generator into dev
Universal: Auto Version Bump / Version Bump (push) Has been skipped
Universal: Pre-Release / Build Pre-Release (${{ inputs.stability || github.ref_name }}) (push) Successful in 16s

This commit was merged in pull request #213.
This commit is contained in:
2026-06-29 11:34:18 +00:00
5 changed files with 89 additions and 32 deletions
+3 -2
View File
@@ -12,8 +12,9 @@
- **Analytics service filter**: Filter heatmap and stats by service type with configurable date range
- **Analytics service breakdown**: Per-service success rate, failure count, and average posts per day
- **Analytics AJAX endpoint**: JSON heatmap data for dynamic filtering without page reload
- **Social image generator**: Generate Open Graph images with article title overlay using PHP GD library (#157)
- **Social image config**: Background color, text color, overlay style, and site name override in component options (#157)
- **Social image generator**: Generate branded 1200x630 OG images with article title overlay using PHP GD (#157)
- **Social image config**: Background color, text color, font size, and site name branding options (#157)
- **Generate Social Image button**: One-click image generation in the Share Content panel (#157)
- **AI caption generation**: Generate platform-optimized cross-post captions from article content using Claude or OpenAI (#161)
- **AI provider config**: New "AI Caption Generation" fieldset in component options with provider, API key, model, and tone settings
- **AI Generate button**: One-click AI generation button in the Share Content panel that fills all caption fields
@@ -266,7 +266,7 @@
</field>
</fieldset>
<fieldset name="social_image" label="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE">
<fieldset name="social_image" label="COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE">
<field
name="social_image_enabled"
type="radio"
@@ -570,7 +570,24 @@ COM_MOKOSUITECROSS_AI_GENERATE_DESC="Generate platform-optimized captions from t
COM_MOKOSUITECROSS_AI_GENERATING="Generating captions..."
COM_MOKOSUITECROSS_AI_GENERATED="AI captions generated successfully."
COM_MOKOSUITECROSS_AI_ERROR="AI generation failed: %s"
COM_MOKOSUITECROSS_AI_NOT_CONFIGURED="AI is not configured. Go to Options to set up a provider and API key."
; Social Image Generator
COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE="Social Image Generator"
COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_ENABLED="Enable Social Images"
COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_ENABLED_DESC="Generate branded OG images with article title overlay for social sharing."
COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_BG_COLOR="Background Color"
COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_BG_COLOR_DESC="Hex color for the image background (e.g. #1a1a2e)."
COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_TEXT_COLOR="Text Color"
COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_TEXT_COLOR_DESC="Hex color for the title text overlay."
COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_FONT_SIZE="Font Size"
COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_FONT_SIZE_DESC="Font size in pixels for the title text (24-96)."
COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_SHOW_SITE_NAME="Show Site Name"
COM_MOKOSUITECROSS_CONFIG_SOCIAL_IMAGE_SHOW_SITE_NAME_DESC="Display the site name in the bottom-right corner of generated images."
COM_MOKOSUITECROSS_SOCIAL_IMAGE_GENERATE="Generate Social Image"
COM_MOKOSUITECROSS_SOCIAL_IMAGE_GENERATING="Generating image..."
COM_MOKOSUITECROSS_SOCIAL_IMAGE_GENERATED="Social image generated."
COM_MOKOSUITECROSS_SOCIAL_IMAGE_ERROR="Image generation failed: %s"
COM_MOKOSUITECROSS_SOCIAL_IMAGE_NOT_CONFIGURED="Social image generator is not enabled. Go to Options to enable it."
; Analytics
COM_MOKOSUITECROSS_SUBMENU_ANALYTICS="Analytics"
@@ -17,7 +17,6 @@ use Joomla\CMS\Component\ComponentHelper;
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\SocialImageHelper;
class SocialImageController extends BaseController
@@ -33,7 +32,7 @@ class SocialImageController extends BaseController
$user = $this->app->getIdentity();
if (!$user->authorise('core.manage', 'com_mokosuitecross')) {
if (!$user->authorise('core.edit', 'com_mokosuitecross')) {
echo json_encode(['success' => false, 'error' => 'Permission denied']);
$this->app->close();
@@ -49,47 +48,40 @@ class SocialImageController extends BaseController
return;
}
$params = ComponentHelper::getParams('com_mokosuitecross');
if (!(int) $params->get('social_image_enabled', 0)) {
echo json_encode(['success' => false, 'error' => 'Social image generator is not enabled']);
$this->app->close();
return;
}
$db = Factory::getDbo();
$query = $db->getQuery(true)
->select($db->quoteName(['id', 'title', 'images']))
->select($db->quoteName('title'))
->from($db->quoteName('#__content'))
->where($db->quoteName('id') . ' = ' . $articleId);
$db->setQuery($query);
$article = $db->loadObject();
$title = $db->loadResult();
if (!$article) {
if (!$title) {
echo json_encode(['success' => false, 'error' => 'Article not found']);
$this->app->close();
return;
}
$params = ComponentHelper::getParams('com_mokosuitecross');
$siteName = $params->get('social_image_site_name', '') ?: Factory::getApplication()->get('sitename', '');
$siteName = $this->app->get('sitename', '');
$options = [
'bg_color' => $params->get('social_image_bg_color', '#1a1a2e'),
'text_color' => $params->get('social_image_text_color', '#ffffff'),
'overlay' => $params->get('social_image_overlay', 'dark'),
$config = [
'bg_color' => $params->get('social_image_bg_color', '#1a1a2e'),
'text_color' => $params->get('social_image_text_color', '#ffffff'),
'font_size' => $params->get('social_image_font_size', 48),
'show_site_name' => (bool) $params->get('social_image_show_site_name', 1),
];
$backgroundPath = null;
$images = json_decode($article->images ?? '{}', true);
if (!empty($images['image_intro'])) {
$backgroundPath = JPATH_ROOT . '/' . ltrim($images['image_intro'], '/');
} elseif (!empty($images['image_fulltext'])) {
$backgroundPath = JPATH_ROOT . '/' . ltrim($images['image_fulltext'], '/');
}
try {
$imagePath = SocialImageHelper::generate($article->title, $siteName, $backgroundPath, $options);
$imageUrl = str_replace(JPATH_ROOT, Uri::root(true), str_replace('\\', '/', $imagePath));
$result = ['success' => true, 'image_url' => $imageUrl, 'image_path' => $imagePath];
} catch (\Throwable $e) {
$result = ['success' => false, 'error' => $e->getMessage()];
}
$result = SocialImageHelper::generate($title, $siteName, $config);
$this->app->setHeader('Content-Type', 'application/json; charset=utf-8');
echo json_encode($result);
@@ -257,6 +257,53 @@ XML;
$form->load($aiXml);
$form->setFieldAttribute('mokosuitecross_ai_generate', 'description', $aiButtonHtml, 'attribs');
}
// Social Image Generator button (#157)
$siParams = ComponentHelper::getParams('com_mokosuitecross');
$siEnabled = (bool) $siParams->get('social_image_enabled', 0);
if ($siEnabled && $articleId > 0) {
$siToken = Session::getFormToken();
$siUrl = Uri::base() . 'index.php?option=com_mokosuitecross&task=socialimage.generate&format=raw&article_id=' . $articleId . '&' . $siToken . '=1';
$siButtonHtml = '<div class="mb-3">'
. '<button type="button" id="mokosuitecross-si-btn" class="btn btn-sm btn-outline-success" onclick="mokosuitecrossSiGenerate()">'
. '<span class="icon-image" aria-hidden="true"></span> '
. \Joomla\CMS\Language\Text::_('COM_MOKOSUITECROSS_SOCIAL_IMAGE_GENERATE')
. '</button>'
. '<span id="mokosuitecross-si-status" class="ms-2 small"></span>'
. '<div id="mokosuitecross-si-preview" class="mt-2" style="display:none;">'
. '<img id="mokosuitecross-si-thumb" src="" alt="Social image preview" style="max-width:300px;border:1px solid #ccc;border-radius:4px;" />'
. '</div>'
. '</div>'
. '<script>'
. 'function mokosuitecrossSiGenerate(){'
. 'var btn=document.getElementById("mokosuitecross-si-btn");'
. 'var st=document.getElementById("mokosuitecross-si-status");'
. 'var pv=document.getElementById("mokosuitecross-si-preview");'
. 'var img=document.getElementById("mokosuitecross-si-thumb");'
. 'btn.disabled=true;st.textContent="' . \Joomla\CMS\Language\Text::_('COM_MOKOSUITECROSS_SOCIAL_IMAGE_GENERATING', true) . '";'
. 'pv.style.display="none";'
. 'fetch("' . $siUrl . '")'
. '.then(function(r){return r.json();})'
. '.then(function(d){'
. 'btn.disabled=false;'
. 'if(!d.success){st.textContent=d.error||"Error";return;}'
. 'st.textContent="' . \Joomla\CMS\Language\Text::_('COM_MOKOSUITECROSS_SOCIAL_IMAGE_GENERATED', true) . '";'
. 'img.src="' . Uri::root() . '"+d.image_url+"?t="+Date.now();'
. 'pv.style.display="block";'
. '})'
. '.catch(function(){btn.disabled=false;st.textContent="Request failed";});'
. '}'
. '</script>';
$siXml = '<?xml version="1.0"?>
<form><fields name="attribs"><fieldset name="mokosuitecross_share">
<field name="mokosuitecross_si_generate" type="note"
label="" description="" />
</fieldset></fields></form>';
$form->load($siXml);
$form->setFieldAttribute('mokosuitecross_si_generate', 'description', $siButtonHtml, 'attribs');
}
// Cross-post history panel for existing articles