Image genartiona dn temaplte design redesign
This commit is contained in:
@@ -816,6 +816,27 @@ class AIModelConfig(models.Model):
|
|||||||
help_text="basic / quality / premium - for image models"
|
help_text="basic / quality / premium - for image models"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# Image Size Configuration (for image models)
|
||||||
|
landscape_size = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
null=True,
|
||||||
|
blank=True,
|
||||||
|
help_text="Landscape image size for this model (e.g., '1792x1024', '1280x768')"
|
||||||
|
)
|
||||||
|
|
||||||
|
square_size = models.CharField(
|
||||||
|
max_length=20,
|
||||||
|
default='1024x1024',
|
||||||
|
blank=True,
|
||||||
|
help_text="Square image size for this model (e.g., '1024x1024')"
|
||||||
|
)
|
||||||
|
|
||||||
|
valid_sizes = models.JSONField(
|
||||||
|
default=list,
|
||||||
|
blank=True,
|
||||||
|
help_text="List of valid sizes for this model (e.g., ['1024x1024', '1792x1024'])"
|
||||||
|
)
|
||||||
|
|
||||||
# Model Limits
|
# Model Limits
|
||||||
max_tokens = models.IntegerField(
|
max_tokens = models.IntegerField(
|
||||||
null=True,
|
null=True,
|
||||||
@@ -884,6 +905,21 @@ class AIModelConfig(models.Model):
|
|||||||
model_type='image',
|
model_type='image',
|
||||||
is_active=True
|
is_active=True
|
||||||
).order_by('quality_tier', 'model_name')
|
).order_by('quality_tier', 'model_name')
|
||||||
|
|
||||||
|
def validate_size(self, size: str) -> bool:
|
||||||
|
"""Validate that the given size is valid for this image model"""
|
||||||
|
if not self.valid_sizes:
|
||||||
|
# If no valid_sizes defined, accept common sizes
|
||||||
|
return True
|
||||||
|
return size in self.valid_sizes
|
||||||
|
|
||||||
|
def get_landscape_size(self) -> str:
|
||||||
|
"""Get the landscape size for this model"""
|
||||||
|
return self.landscape_size or '1792x1024'
|
||||||
|
|
||||||
|
def get_square_size(self) -> str:
|
||||||
|
"""Get the square size for this model"""
|
||||||
|
return self.square_size or '1024x1024'
|
||||||
|
|
||||||
|
|
||||||
class WebhookEvent(models.Model):
|
class WebhookEvent(models.Model):
|
||||||
|
|||||||
@@ -800,6 +800,11 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
|||||||
'description': 'For IMAGE models only',
|
'description': 'For IMAGE models only',
|
||||||
'classes': ('collapse',)
|
'classes': ('collapse',)
|
||||||
}),
|
}),
|
||||||
|
('Image Model Sizes', {
|
||||||
|
'fields': ('landscape_size', 'square_size', 'valid_sizes'),
|
||||||
|
'description': 'For IMAGE models: specify supported image dimensions',
|
||||||
|
'classes': ('collapse',)
|
||||||
|
}),
|
||||||
('Capabilities', {
|
('Capabilities', {
|
||||||
'fields': ('capabilities',),
|
'fields': ('capabilities',),
|
||||||
'description': 'JSON: vision, function_calling, json_mode, etc.',
|
'description': 'JSON: vision, function_calling, json_mode, etc.',
|
||||||
|
|||||||
@@ -0,0 +1,78 @@
|
|||||||
|
# Generated migration for adding image size fields to AIModelConfig
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
def populate_image_sizes(apps, schema_editor):
|
||||||
|
"""Populate image sizes based on model name"""
|
||||||
|
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
|
||||||
|
|
||||||
|
# Model-specific sizes
|
||||||
|
model_sizes = {
|
||||||
|
'runware:97@1': {
|
||||||
|
'landscape_size': '1280x768',
|
||||||
|
'square_size': '1024x1024',
|
||||||
|
'valid_sizes': ['1024x1024', '1280x768', '768x1280'],
|
||||||
|
},
|
||||||
|
'bria:10@1': {
|
||||||
|
'landscape_size': '1344x768',
|
||||||
|
'square_size': '1024x1024',
|
||||||
|
'valid_sizes': ['1024x1024', '1344x768', '768x1344'],
|
||||||
|
},
|
||||||
|
'google:4@2': {
|
||||||
|
'landscape_size': '1376x768',
|
||||||
|
'square_size': '1024x1024',
|
||||||
|
'valid_sizes': ['1024x1024', '1376x768', '768x1376'],
|
||||||
|
},
|
||||||
|
'dall-e-3': {
|
||||||
|
'landscape_size': '1792x1024',
|
||||||
|
'square_size': '1024x1024',
|
||||||
|
'valid_sizes': ['1024x1024', '1792x1024', '1024x1792'],
|
||||||
|
},
|
||||||
|
'dall-e-2': {
|
||||||
|
'landscape_size': '1024x1024',
|
||||||
|
'square_size': '1024x1024',
|
||||||
|
'valid_sizes': ['256x256', '512x512', '1024x1024'],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for model_name, sizes in model_sizes.items():
|
||||||
|
AIModelConfig.objects.filter(
|
||||||
|
model_name=model_name,
|
||||||
|
model_type='image'
|
||||||
|
).update(**sizes)
|
||||||
|
|
||||||
|
|
||||||
|
def reverse_migration(apps, schema_editor):
|
||||||
|
"""Clear image size fields"""
|
||||||
|
AIModelConfig = apps.get_model('billing', 'AIModelConfig')
|
||||||
|
AIModelConfig.objects.filter(model_type='image').update(
|
||||||
|
landscape_size=None,
|
||||||
|
square_size='1024x1024',
|
||||||
|
valid_sizes=[],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
|
||||||
|
dependencies = [
|
||||||
|
('billing', '0026_populate_aimodel_credits'),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='aimodelconfig',
|
||||||
|
name='landscape_size',
|
||||||
|
field=models.CharField(blank=True, help_text="Landscape image size for this model (e.g., '1792x1024', '1280x768')", max_length=20, null=True),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='aimodelconfig',
|
||||||
|
name='square_size',
|
||||||
|
field=models.CharField(blank=True, default='1024x1024', help_text="Square image size for this model (e.g., '1024x1024')", max_length=20),
|
||||||
|
),
|
||||||
|
migrations.AddField(
|
||||||
|
model_name='aimodelconfig',
|
||||||
|
name='valid_sizes',
|
||||||
|
field=models.JSONField(blank=True, default=list, help_text="List of valid sizes for this model (e.g., ['1024x1024', '1792x1024'])"),
|
||||||
|
),
|
||||||
|
migrations.RunPython(populate_image_sizes, reverse_migration),
|
||||||
|
]
|
||||||
@@ -183,8 +183,8 @@ class ContentSettingsViewSet(viewsets.ViewSet):
|
|||||||
setting = AccountSettings.objects.get(account=account, key=pk)
|
setting = AccountSettings.objects.get(account=account, key=pk)
|
||||||
return success_response(data={
|
return success_response(data={
|
||||||
'key': setting.key,
|
'key': setting.key,
|
||||||
'config': setting.config,
|
'config': setting.value, # Model uses 'value', frontend expects 'config'
|
||||||
'is_active': setting.is_active,
|
'is_active': getattr(setting, 'is_active', True),
|
||||||
}, request=request)
|
}, request=request)
|
||||||
except AccountSettings.DoesNotExist:
|
except AccountSettings.DoesNotExist:
|
||||||
# Return default settings if not yet saved
|
# Return default settings if not yet saved
|
||||||
@@ -234,17 +234,17 @@ class ContentSettingsViewSet(viewsets.ViewSet):
|
|||||||
# Filter to only valid fields
|
# Filter to only valid fields
|
||||||
filtered_config = {k: v for k, v in config.items() if k in valid_fields}
|
filtered_config = {k: v for k, v in config.items() if k in valid_fields}
|
||||||
|
|
||||||
# Get or create setting
|
# Get or create setting - model uses 'value' field
|
||||||
setting, created = AccountSettings.objects.update_or_create(
|
setting, created = AccountSettings.objects.update_or_create(
|
||||||
account=account,
|
account=account,
|
||||||
key=pk,
|
key=pk,
|
||||||
defaults={'config': filtered_config, 'is_active': True}
|
defaults={'value': filtered_config}
|
||||||
)
|
)
|
||||||
|
|
||||||
return success_response(data={
|
return success_response(data={
|
||||||
'key': setting.key,
|
'key': setting.key,
|
||||||
'config': setting.config,
|
'config': setting.value, # Model uses 'value', frontend expects 'config'
|
||||||
'is_active': setting.is_active,
|
'is_active': getattr(setting, 'is_active', True),
|
||||||
'message': 'Settings saved successfully',
|
'message': 'Settings saved successfully',
|
||||||
}, request=request)
|
}, request=request)
|
||||||
|
|
||||||
@@ -607,8 +607,28 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
|
|||||||
account=account,
|
account=account,
|
||||||
key='ai.image_quality_tier'
|
key='ai.image_quality_tier'
|
||||||
).first()
|
).first()
|
||||||
if tier_setting and tier_setting.config:
|
if tier_setting and tier_setting.value: # Model uses 'value' field
|
||||||
selected_tier = tier_setting.config.get('value', 'quality')
|
selected_tier = tier_setting.value.get('value', 'quality')
|
||||||
|
|
||||||
|
# Get default image model (or model for selected tier)
|
||||||
|
default_image_model = AIModelConfig.get_default_image_model()
|
||||||
|
|
||||||
|
# Try to find model matching selected tier
|
||||||
|
selected_model = AIModelConfig.objects.filter(
|
||||||
|
model_type='image',
|
||||||
|
quality_tier=selected_tier,
|
||||||
|
is_active=True
|
||||||
|
).first() or default_image_model
|
||||||
|
|
||||||
|
# Get image sizes from the selected model
|
||||||
|
featured_image_size = '1792x1024' # Default
|
||||||
|
landscape_image_size = '1792x1024' # Default
|
||||||
|
square_image_size = '1024x1024' # Default
|
||||||
|
|
||||||
|
if selected_model:
|
||||||
|
landscape_image_size = selected_model.landscape_size or '1792x1024'
|
||||||
|
square_image_size = selected_model.square_size or '1024x1024'
|
||||||
|
featured_image_size = landscape_image_size # Featured uses landscape
|
||||||
|
|
||||||
response_data = {
|
response_data = {
|
||||||
'content_generation': {
|
'content_generation': {
|
||||||
@@ -622,6 +642,12 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
|
|||||||
'selected_style': image_style,
|
'selected_style': image_style,
|
||||||
'max_images': max_images,
|
'max_images': max_images,
|
||||||
'max_allowed': 8,
|
'max_allowed': 8,
|
||||||
|
# Image sizes based on selected model
|
||||||
|
'featured_image_size': featured_image_size,
|
||||||
|
'landscape_image_size': landscape_image_size,
|
||||||
|
'square_image_size': square_image_size,
|
||||||
|
'model_name': selected_model.model_name if selected_model else None,
|
||||||
|
'model_display_name': selected_model.display_name if selected_model else None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -640,7 +666,12 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
|
|||||||
PUT/POST /api/v1/accounts/settings/ai/
|
PUT/POST /api/v1/accounts/settings/ai/
|
||||||
|
|
||||||
Save account-specific overrides to AccountSettings.
|
Save account-specific overrides to AccountSettings.
|
||||||
Request body per the plan:
|
Accepts nested structure:
|
||||||
|
{
|
||||||
|
"content_generation": { "temperature": 0.8, "max_tokens": 4096 },
|
||||||
|
"image_generation": { "quality_tier": "premium", "image_style": "illustration", "max_images_per_article": 6 }
|
||||||
|
}
|
||||||
|
Or flat structure:
|
||||||
{
|
{
|
||||||
"temperature": 0.8,
|
"temperature": 0.8,
|
||||||
"max_tokens": 4096,
|
"max_tokens": 4096,
|
||||||
@@ -662,6 +693,19 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
|
|||||||
data = request.data
|
data = request.data
|
||||||
saved_keys = []
|
saved_keys = []
|
||||||
|
|
||||||
|
# Handle nested structure from frontend
|
||||||
|
content_gen = data.get('content_generation', {})
|
||||||
|
image_gen = data.get('image_generation', {})
|
||||||
|
|
||||||
|
# Flatten nested structure or use flat keys
|
||||||
|
flat_data = {
|
||||||
|
'temperature': content_gen.get('temperature') if content_gen else data.get('temperature'),
|
||||||
|
'max_tokens': content_gen.get('max_tokens') if content_gen else data.get('max_tokens'),
|
||||||
|
'image_quality_tier': image_gen.get('quality_tier') if image_gen else data.get('image_quality_tier'),
|
||||||
|
'image_style': image_gen.get('image_style') if image_gen else data.get('image_style'),
|
||||||
|
'max_images': image_gen.get('max_images_per_article') if image_gen else data.get('max_images'),
|
||||||
|
}
|
||||||
|
|
||||||
# Map request fields to AccountSettings keys
|
# Map request fields to AccountSettings keys
|
||||||
key_mappings = {
|
key_mappings = {
|
||||||
'temperature': 'ai.temperature',
|
'temperature': 'ai.temperature',
|
||||||
@@ -672,11 +716,12 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
|
|||||||
}
|
}
|
||||||
|
|
||||||
for field, account_key in key_mappings.items():
|
for field, account_key in key_mappings.items():
|
||||||
if field in data:
|
value = flat_data.get(field)
|
||||||
|
if value is not None:
|
||||||
AccountSettings.objects.update_or_create(
|
AccountSettings.objects.update_or_create(
|
||||||
account=account,
|
account=account,
|
||||||
key=account_key,
|
key=account_key,
|
||||||
defaults={'config': {'value': data[field]}}
|
defaults={'value': {'value': value}} # Model uses 'value' field
|
||||||
)
|
)
|
||||||
saved_keys.append(account_key)
|
saved_keys.append(account_key)
|
||||||
|
|
||||||
|
|||||||
815
docs/plans/phase3-content-template-redesign.md
Normal file
815
docs/plans/phase3-content-template-redesign.md
Normal file
@@ -0,0 +1,815 @@
|
|||||||
|
---
|
||||||
|
|
||||||
|
## 🎨 DESIGN ANALYSIS & PLAN
|
||||||
|
|
||||||
|
### Current State Analysis
|
||||||
|
|
||||||
|
**WordPress Single Post Template:**
|
||||||
|
- Uses emojis (📅, 📝, ✍️, 📁, 🏷️) in header metadata
|
||||||
|
- Shows all metadata including internal/debug data (Content ID, Task ID, Sector ID, Cluster ID, etc.)
|
||||||
|
- SEO section shows Meta Title & Meta Description in header (should be hidden from users)
|
||||||
|
- "Section Spotlight" label is hardcoded
|
||||||
|
- Images not optimally distributed - one per section sequentially
|
||||||
|
- Container max-width: 1200px
|
||||||
|
|
||||||
|
**App ContentViewTemplate:**
|
||||||
|
- Uses icons properly via component imports
|
||||||
|
- Similar "Section Spotlight" label issue
|
||||||
|
- Better image handling with aspect ratio detection
|
||||||
|
- Shows extensive metadata in header (Meta Title, Meta Description, Primary/Secondary Keywords)
|
||||||
|
- Container max-width: 1440px
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 📐 DESIGN PLAN
|
||||||
|
|
||||||
|
### 1. CSS Container Width Update
|
||||||
|
|
||||||
|
```
|
||||||
|
.igny8-content-container {
|
||||||
|
max-width: 1280px; /* Default for screens <= 1600px */
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1600px) {
|
||||||
|
.igny8-content-container {
|
||||||
|
max-width: 1530px; /* For screens > 1600px */
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 2. WordPress Header Redesign
|
||||||
|
|
||||||
|
**USER-FACING FIELDS (Keep in Header):**
|
||||||
|
| Field | Display | Icon | Notes |
|
||||||
|
|-------|---------|------|-------|
|
||||||
|
| Title | H1 | - | Post title |
|
||||||
|
| Status Badge | Published/Draft/etc | - | Post status |
|
||||||
|
| Posted Date | Formatted date | Calendar SVG | Publication date |
|
||||||
|
| Word Count | Formatted number | Document SVG | Content word count |
|
||||||
|
| Author | Author name | User SVG | Post author |
|
||||||
|
| Topic | Cluster name (clickable)| Compass SVG | Display cluster_name as "Topic" |
|
||||||
|
| Categories | Badge list (Parent > Child clicakble) | Folder SVG | WP Categories |
|
||||||
|
| Tags | Badge list (clickable)| Tag SVG | WP Tags |
|
||||||
|
|
||||||
|
**NON-USER FIELDS (Move to Metadata Section - Editor+ only):**
|
||||||
|
- Content ID, Task ID
|
||||||
|
- Content Type, Structure
|
||||||
|
- Cluster ID (keep cluster_name as Topic in header)
|
||||||
|
- Sector ID, Sector Name
|
||||||
|
- Primary Keyword, Secondary Keywords
|
||||||
|
- Meta Title, Meta Description
|
||||||
|
- Source, Last Synced
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 3. Section Label Redesign
|
||||||
|
|
||||||
|
**Current:** "Section Spotlight" (generic text)
|
||||||
|
|
||||||
|
**New Approach - Keyword/Tag Matching Algorithm:**
|
||||||
|
|
||||||
|
1. **Source Data:**
|
||||||
|
- Get all WordPress tags assigned to the post
|
||||||
|
- Get all WordPress categories assigned to the post
|
||||||
|
- Get primary keyword from post meta
|
||||||
|
- Get secondary keywords from post meta (if available)
|
||||||
|
|
||||||
|
2. **Matching Logic:**
|
||||||
|
- For each section heading (H2), perform case-insensitive partial matching
|
||||||
|
- Check if any tag name appears in the heading text
|
||||||
|
- Check if any category name appears in the heading text
|
||||||
|
- Check if primary/secondary keywords appear in the heading text
|
||||||
|
- Prioritize: Primary Keyword > Tags > Categories > Secondary Keywords
|
||||||
|
|
||||||
|
3. **Display Rules:**
|
||||||
|
- If matches found: Display up to 2 matched keywords/tags as badges
|
||||||
|
- If no matches: Display topic (cluster_name) or leave section without label badges
|
||||||
|
- Never display generic "Section Spotlight" text
|
||||||
|
|
||||||
|
4. **Badge Styling:**
|
||||||
|
```
|
||||||
|
[Primary Match] [Secondary Match] ← styled badges replacing "Section Spotlight"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Colors:**
|
||||||
|
- Primary badge: `theme-color @ 15% opacity` background, `theme-color` text
|
||||||
|
- Secondary badge: `theme-color @ 8% opacity` background, `theme-color @ 80%` text
|
||||||
|
|
||||||
|
**Implementation Function (Pseudo-code):**
|
||||||
|
```php
|
||||||
|
function igny8_get_section_badges($heading, $post_id) {
|
||||||
|
$badges = [];
|
||||||
|
$heading_lower = strtolower($heading);
|
||||||
|
|
||||||
|
// Get post taxonomies and keywords
|
||||||
|
$tags = get_the_tags($post_id);
|
||||||
|
$categories = get_the_category($post_id);
|
||||||
|
$primary_kw = get_post_meta($post_id, '_igny8_primary_keyword', true);
|
||||||
|
$secondary_kws = get_post_meta($post_id, '_igny8_secondary_keywords', true);
|
||||||
|
|
||||||
|
// Priority 1: Primary keyword
|
||||||
|
if ($primary_kw && stripos($heading_lower, strtolower($primary_kw)) !== false) {
|
||||||
|
$badges[] = ['text' => $primary_kw, 'type' => 'primary'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Tags
|
||||||
|
if ($tags && count($badges) < 2) {
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
if (stripos($heading_lower, strtolower($tag->name)) !== false) {
|
||||||
|
$badges[] = ['text' => $tag->name, 'type' => 'tag'];
|
||||||
|
if (count($badges) >= 2) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Categories
|
||||||
|
if ($categories && count($badges) < 2) {
|
||||||
|
foreach ($categories as $cat) {
|
||||||
|
if (stripos($heading_lower, strtolower($cat->name)) !== false) {
|
||||||
|
$badges[] = ['text' => $cat->name, 'type' => 'category'];
|
||||||
|
if (count($badges) >= 2) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 4: Secondary keywords
|
||||||
|
if ($secondary_kws && count($badges) < 2) {
|
||||||
|
$kw_array = is_array($secondary_kws) ? $secondary_kws : explode(',', $secondary_kws);
|
||||||
|
foreach ($kw_array as $kw) {
|
||||||
|
$kw = trim($kw);
|
||||||
|
if (stripos($heading_lower, strtolower($kw)) !== false) {
|
||||||
|
$badges[] = ['text' => $kw, 'type' => 'keyword'];
|
||||||
|
if (count($badges) >= 2) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $badges;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 4. Image Distribution Strategy
|
||||||
|
|
||||||
|
**Available Images (4 total):**
|
||||||
|
- Position 0: Square (1024×1024)
|
||||||
|
- Position 1: Landscape (1536×1024 or 1920×1080)
|
||||||
|
- Position 2: Square (1024×1024)
|
||||||
|
- Position 3: Landscape (1536×1024 or 1920×1080)
|
||||||
|
|
||||||
|
**Distribution Plan - First 4 Sections (with descriptions):**
|
||||||
|
|
||||||
|
| Section | Image Position | Type | Width | Alignment | Description |
|
||||||
|
|---------|---------------|------|-------|-----------|-------------|
|
||||||
|
| **Featured** | Position 1 | Landscape | 100% max 1024px | Center | Show prompt on first use |
|
||||||
|
| **Section 1** | Position 0 | Square | 50% | Right | With description + widget placeholder below |
|
||||||
|
| **Section 2** | Position 3 | Landscape | 100% max 1024px | Full width | With description |
|
||||||
|
| **Section 3** | Position 2 | Square | 50% | Left | With description + widget placeholder below |
|
||||||
|
| **Section 4** | Position 1 (reuse) | Landscape | 100% max 1024px | Full width | With description |
|
||||||
|
|
||||||
|
**Distribution Plan - Sections 5-7+ (reuse without descriptions):**
|
||||||
|
|
||||||
|
| Section | Reuse Image | Type | Width | Alignment | Description |
|
||||||
|
|---------|-------------|------|-------|-----------|-------------|
|
||||||
|
| **Section 5** | Featured (pos 1) | Landscape | 100% max 1024px | Full width | NO description |
|
||||||
|
| **Section 6** | Position 0 | Square | 50% | Right | NO description + widget placeholder |
|
||||||
|
| **Section 7** | Position 3 | Landscape | 100% max 1024px | Full width | NO description |
|
||||||
|
| **Section 8+** | Cycle through all 4 | Based on type | Based on type | Based on type | NO description |
|
||||||
|
|
||||||
|
**Special Case - Tables:**
|
||||||
|
- When section contains `<table>` element, always place full-width landscape image BEFORE table
|
||||||
|
- Use next available landscape image (Position 1 or 3)
|
||||||
|
- Max width: 1024px, centered
|
||||||
|
- Spacing: `margin-bottom: 2rem` before table
|
||||||
|
- Override normal section pattern when table detected
|
||||||
|
|
||||||
|
**Image Reuse Rules:**
|
||||||
|
- Images 1-4 used in first 4 sections WITH descriptions/prompts
|
||||||
|
- Sections 5+ reuse same images WITHOUT descriptions/prompts
|
||||||
|
- Use CSS classes: `.igny8-image-first-use` vs `.igny8-image-reuse`
|
||||||
|
- Maintain same layout pattern (square = 50%, landscape = 100%)
|
||||||
|
|
||||||
|
**Widget Placeholders:**
|
||||||
|
- Show only below square images (left/right aligned)
|
||||||
|
- Empty div with class `.igny8-widget-placeholder`
|
||||||
|
- Space reserved for future widget insertion
|
||||||
|
- Controlled via plugin settings (future implementation)
|
||||||
|
|
||||||
|
**Implementation Notes:**
|
||||||
|
```php
|
||||||
|
// Check for table in section content
|
||||||
|
function igny8_section_has_table($section_html) {
|
||||||
|
return (stripos($section_html, '<table') !== false);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get image aspect ratio from position
|
||||||
|
function igny8_get_image_aspect($position) {
|
||||||
|
// Even positions (0, 2) = square
|
||||||
|
// Odd positions (1, 3) = landscape
|
||||||
|
return ($position % 2 === 0) ? 'square' : 'landscape';
|
||||||
|
}
|
||||||
|
|
||||||
|
// Determine if image should show description
|
||||||
|
function igny8_show_image_description($section_index) {
|
||||||
|
// First 4 sections (0-3) show descriptions
|
||||||
|
return ($section_index < 4);
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 5. Image Alignment with Tables
|
||||||
|
|
||||||
|
When section contains a `<table>`:
|
||||||
|
- Place landscape image ABOVE the table
|
||||||
|
- Full width (max 800px)
|
||||||
|
- Proper spacing: `margin-bottom: 2rem`
|
||||||
|
- Table should not wrap around image
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 6. Responsive Image Width Rules
|
||||||
|
|
||||||
|
```css
|
||||||
|
/* Landscape images */
|
||||||
|
.igny8-image-landscape {
|
||||||
|
max-width: 1024px; /* Updated from 800px */
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-image-landscape.igny8-image-reuse {
|
||||||
|
/* No description shown on reuse */
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Single square image - Right aligned */
|
||||||
|
.igny8-image-square-right {
|
||||||
|
max-width: 50%;
|
||||||
|
margin-left: auto;
|
||||||
|
float: right;
|
||||||
|
margin-left: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Single square image - Left aligned */
|
||||||
|
.igny8-image-square-left {
|
||||||
|
max-width: 50%;
|
||||||
|
margin-right: auto;
|
||||||
|
float: left;
|
||||||
|
margin-right: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Widget placeholder below square images */
|
||||||
|
.igny8-widget-placeholder {
|
||||||
|
clear: both;
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
border: 1px dashed rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: 12px;
|
||||||
|
display: none; /* Hidden by default, shown when widgets enabled */
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-widget-placeholder.igny8-widgets-enabled {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table-specific image positioning */
|
||||||
|
.igny8-image-before-table {
|
||||||
|
max-width: 1024px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 7. Role-Based Visibility
|
||||||
|
|
||||||
|
**Metadata Section (Bottom):**
|
||||||
|
```php
|
||||||
|
<?php if (current_user_can('edit_posts')): ?>
|
||||||
|
<div class="igny8-metadata-footer igny8-editor-only">
|
||||||
|
<!-- All internal metadata here -->
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
```
|
||||||
|
|
||||||
|
**Visible only to:**
|
||||||
|
- Editor
|
||||||
|
- Administrator
|
||||||
|
- Author (for their own posts)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 8. Header Icon Set (Replace Emojis)
|
||||||
|
|
||||||
|
Create inline SVG icons matching theme color:
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Calendar Icon
|
||||||
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
// Document Icon (Word Count)
|
||||||
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
// User Icon (Author)
|
||||||
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
// Compass Icon (Topic/Cluster)
|
||||||
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
// Folder Icon (Categories)
|
||||||
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
// Tag Icon (Tags)
|
||||||
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z"/>
|
||||||
|
</svg>
|
||||||
|
```
|
||||||
|
|
||||||
|
Icon styling:
|
||||||
|
```css
|
||||||
|
.igny8-icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
color: var(--igny8-theme-color, currentColor);
|
||||||
|
opacity: 0.8;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 9. Table of Contents
|
||||||
|
|
||||||
|
**Position:** Below featured image, before intro section
|
||||||
|
|
||||||
|
**Content:** List all H2 headings from content
|
||||||
|
|
||||||
|
**Features:**
|
||||||
|
- Clickable links with smooth scroll to sections
|
||||||
|
- Collapsible/expandable (optional)
|
||||||
|
- Numbered list matching section numbers
|
||||||
|
- Sticky positioning option (future setting)
|
||||||
|
|
||||||
|
**Implementation:**
|
||||||
|
```php
|
||||||
|
function igny8_generate_table_of_contents($content) {
|
||||||
|
$toc_items = [];
|
||||||
|
|
||||||
|
// Parse content for H2 headings
|
||||||
|
preg_match_all('/<h2[^>]*>(.*?)<\/h2>/i', $content, $matches);
|
||||||
|
|
||||||
|
if (!empty($matches[1])) {
|
||||||
|
foreach ($matches[1] as $index => $heading) {
|
||||||
|
$heading_text = strip_tags($heading);
|
||||||
|
$slug = sanitize_title($heading_text);
|
||||||
|
$toc_items[] = [
|
||||||
|
'number' => $index + 1,
|
||||||
|
'text' => $heading_text,
|
||||||
|
'id' => $slug
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $toc_items;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**HTML Structure:**
|
||||||
|
```html
|
||||||
|
<nav class="igny8-table-of-contents">
|
||||||
|
<div class="igny8-toc-header">
|
||||||
|
<span class="igny8-toc-icon">📑</span>
|
||||||
|
<h3>Table of Contents</h3>
|
||||||
|
</div>
|
||||||
|
<ol class="igny8-toc-list">
|
||||||
|
<?php foreach ($toc_items as $item): ?>
|
||||||
|
<li class="igny8-toc-item">
|
||||||
|
<a href="#<?php echo esc_attr($item['id']); ?>" class="igny8-toc-link">
|
||||||
|
<span class="igny8-toc-number"><?php echo $item['number']; ?>.</span>
|
||||||
|
<span class="igny8-toc-text"><?php echo esc_html($item['text']); ?></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
```
|
||||||
|
|
||||||
|
**CSS:**
|
||||||
|
```css
|
||||||
|
.igny8-table-of-contents {
|
||||||
|
background: var(--wp--preset--color--base, #ffffff);
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: 16px;
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
border-radius: 8px;
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-link:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-number {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
color: rgba(59, 130, 246, 1);
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Settings (Future Implementation):**
|
||||||
|
```php
|
||||||
|
// Plugin settings for TOC
|
||||||
|
$igny8_toc_settings = [
|
||||||
|
'enabled' => true,
|
||||||
|
'show_numbers' => true,
|
||||||
|
'collapsible' => false,
|
||||||
|
'sticky' => false,
|
||||||
|
'min_headings' => 3, // Only show if 3+ H2 headings
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 10. Widget System
|
||||||
|
|
||||||
|
**Widget Placeholders:**
|
||||||
|
|
||||||
|
Widgets appear below square images (left/right aligned) where there's natural space.
|
||||||
|
|
||||||
|
**Placeholder Function:**
|
||||||
|
```php
|
||||||
|
function igny8_render_widget_placeholder($position, $section_index) {
|
||||||
|
// Check if widgets are enabled in settings
|
||||||
|
$widgets_enabled = get_option('igny8_widgets_enabled', false);
|
||||||
|
|
||||||
|
if (!$widgets_enabled) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
$placeholder_class = 'igny8-widget-placeholder igny8-widgets-enabled';
|
||||||
|
$placeholder_class .= ' igny8-widget-' . $position; // left or right
|
||||||
|
$placeholder_class .= ' igny8-widget-section-' . $section_index;
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr($placeholder_class); ?>"
|
||||||
|
data-widget-position="<?php echo esc_attr($position); ?>"
|
||||||
|
data-section-index="<?php echo esc_attr($section_index); ?>">
|
||||||
|
<!-- Widget content will be inserted here via settings -->
|
||||||
|
<?php do_action('igny8_widget_placeholder', $position, $section_index); ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
**Widget Settings (Future Implementation):**
|
||||||
|
```php
|
||||||
|
// Plugin settings for widgets
|
||||||
|
$igny8_widget_settings = [
|
||||||
|
'enabled' => false,
|
||||||
|
'sections' => [
|
||||||
|
'section_1' => [
|
||||||
|
'position' => 'right',
|
||||||
|
'widget_type' => 'related_posts', // or 'custom_html', 'ad_unit', etc.
|
||||||
|
'content' => '',
|
||||||
|
],
|
||||||
|
'section_3' => [
|
||||||
|
'position' => 'left',
|
||||||
|
'widget_type' => 'newsletter_signup',
|
||||||
|
'content' => '',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
**Widget Types (Future):**
|
||||||
|
- Related Posts
|
||||||
|
- Newsletter Signup
|
||||||
|
- Ad Units
|
||||||
|
- Custom HTML
|
||||||
|
- Social Share Buttons
|
||||||
|
- Author Bio
|
||||||
|
- Call-to-Action Boxes
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 11. Updated Structure Overview
|
||||||
|
|
||||||
|
**WordPress Single Post:**
|
||||||
|
```
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ HEADER │
|
||||||
|
│ ← Back to Posts │
|
||||||
|
│ │
|
||||||
|
│ [H1 Title] [Status Badge] │
|
||||||
|
│ │
|
||||||
|
│ 📅 Posted: Date 📄 Words 👤 Author │
|
||||||
|
│ 🧭 Topic: Cluster Name │
|
||||||
|
│ 📁 [Category Badges] 🏷️ [Tag Badges] │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ FEATURED IMAGE (Landscape, max 1024px) │
|
||||||
|
│ │
|
||||||
|
│ [Image Prompt - first use only] │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ TABLE OF CONTENTS │
|
||||||
|
│ 📑 Table of Contents │
|
||||||
|
│ 1. Section Heading One │
|
||||||
|
│ 2. Section Heading Two │
|
||||||
|
│ 3. Section Heading Three │
|
||||||
|
│ ... (clickable, smooth scroll) │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ INTRO SECTION │
|
||||||
|
│ Opening Narrative │
|
||||||
|
│ [Content...] │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ SECTION 1 │
|
||||||
|
│ [Keyword Badge] [Tag Badge] │
|
||||||
|
│ 1 [H2 Heading] │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────────────┐ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ Content │ │ Square Image (50%) │ │
|
||||||
|
│ │ │ │ Right Aligned │ │
|
||||||
|
│ │ │ │ [Image Description] │ │
|
||||||
|
│ └──────────────┘ └──────────────────────┘ │
|
||||||
|
│ [Widget Placeholder] │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ SECTION 2 │
|
||||||
|
│ [Keyword Badge] │
|
||||||
|
│ 2 [H2 Heading] │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────┐ │
|
||||||
|
│ │ Landscape Image (100% max 1024px) │ │
|
||||||
|
│ │ [Image Description] │ │
|
||||||
|
│ └───────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Content...] │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ SECTION 3 │
|
||||||
|
│ [Keyword Badge] [Tag Badge] │
|
||||||
|
│ 3 [H2 Heading] │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────────────┐ ┌──────────────┐ │
|
||||||
|
│ │ │ │ │ │
|
||||||
|
│ │ Square Image (50%) │ │ Content │ │
|
||||||
|
│ │ Left Aligned │ │ │ │
|
||||||
|
│ │ [Image Description] │ │ │ │
|
||||||
|
│ └──────────────────────┘ └──────────────┘ │
|
||||||
|
│ [Widget Placeholder] │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ SECTION 4 (with table example) │
|
||||||
|
│ [Keyword Badge] │
|
||||||
|
│ 4 [H2 Heading] │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────┐ │
|
||||||
|
│ │ Landscape Image (100% max 1024px) │ │
|
||||||
|
│ │ [Image Description] │ │
|
||||||
|
│ └───────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Content before table...] │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────┐ │
|
||||||
|
│ │ TABLE │ │
|
||||||
|
│ │ [Data rows and columns] │ │
|
||||||
|
│ └───────────────────────────────────────┘ │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ SECTION 5 (reuse - no description) │
|
||||||
|
│ [Keyword Badge] │
|
||||||
|
│ 5 [H2 Heading] │
|
||||||
|
│ │
|
||||||
|
│ ┌───────────────────────────────────────┐ │
|
||||||
|
│ │ Featured Image REUSED (no caption) │ │
|
||||||
|
│ └───────────────────────────────────────┘ │
|
||||||
|
│ │
|
||||||
|
│ [Content...] │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ SECTION 6 (reuse - no description) │
|
||||||
|
│ [Tag Badge] │
|
||||||
|
│ 6 [H2 Heading] │
|
||||||
|
│ │
|
||||||
|
│ ┌──────────────┐ ┌──────────────────────┐ │
|
||||||
|
│ │ Content │ │ Square Image REUSED │ │
|
||||||
|
│ │ │ │ (no caption) │ │
|
||||||
|
│ └──────────────┘ └──────────────────────┘ │
|
||||||
|
│ [Widget Placeholder] │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
|
||||||
|
┌─────────────────────────────────────────────┐
|
||||||
|
│ METADATA FOOTER (Editor+ only) │
|
||||||
|
│ ▸ View IGNY8 Metadata │
|
||||||
|
│ - Content ID: 123 │
|
||||||
|
│ - Task ID: 456 │
|
||||||
|
│ - Meta Title: ... │
|
||||||
|
│ - Meta Description: ... │
|
||||||
|
│ - Primary Keyword: ... │
|
||||||
|
│ - Secondary Keywords: [list] │
|
||||||
|
│ - Cluster ID: 789 │
|
||||||
|
│ - Sector: Industry Name │
|
||||||
|
│ - Source: AI Generated │
|
||||||
|
│ - Last Synced: Date/Time │
|
||||||
|
└─────────────────────────────────────────────┘
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### 12. App ContentViewTemplate Updates
|
||||||
|
|
||||||
|
**Changes to body section only (not header):**
|
||||||
|
|
||||||
|
1. **Remove "Section Spotlight" label** - Replace with keyword badge matching system
|
||||||
|
2. **Add Table of Contents** below featured image (matching WordPress implementation)
|
||||||
|
3. **Match image layout rules** from WordPress template:
|
||||||
|
- Section 1: Square right-aligned 50% (with description)
|
||||||
|
- Section 2: Landscape full width max 1024px (with description)
|
||||||
|
- Section 3: Square left-aligned 50% (with description)
|
||||||
|
- Section 4: Landscape full width max 1024px (with description)
|
||||||
|
- Sections 5+: Reuse images without descriptions
|
||||||
|
4. **Featured image** max 1024px centered
|
||||||
|
5. **Widget placeholders** below square images (empty for now)
|
||||||
|
6. **Table detection** - full-width image before tables
|
||||||
|
|
||||||
|
**Implementation Priority:**
|
||||||
|
- Phase 1: Update image sizing (1024px max)
|
||||||
|
- Phase 2: Implement keyword badge matching
|
||||||
|
- Phase 3: Add table of contents component
|
||||||
|
- Phase 4: Add widget placeholder divs
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Summary of Files to Update
|
||||||
|
|
||||||
|
| File | Changes | Priority |
|
||||||
|
|------|---------|----------|
|
||||||
|
| `igny8-content-template.css` | Container width breakpoints, image sizing classes, TOC styles, widget placeholder styles | 🔴 High |
|
||||||
|
| `igny8-header.php` | Remove emojis, add SVG icons, add Topic field, remove SEO/internal metadata | 🔴 High |
|
||||||
|
| `igny8-metadata.php` | Add role check (`current_user_can('edit_posts')`), include all moved metadata fields | 🔴 High |
|
||||||
|
| `igny8-content-sections.php` | Keyword badge matching logic, smart image distribution (Section 1-4 pattern), widget placeholders | 🔴 High |
|
||||||
|
| `igny8-featured-image.php` | Max 1024px, landscape priority | 🟡 Medium |
|
||||||
|
| `includes/template-functions.php` | Add helper functions: `igny8_get_section_badges()`, `igny8_section_has_table()`, `igny8_show_image_description()`, `igny8_generate_table_of_contents()` | 🔴 High |
|
||||||
|
| `ContentViewTemplate.tsx` | Match section labels, image layouts, add TOC component, widget placeholders | 🟡 Medium |
|
||||||
|
| **New File**: `parts/igny8-table-of-contents.php` | Table of contents component | 🟡 Medium |
|
||||||
|
| **New File**: `admin/settings-page.php` | Widget settings, TOC settings (future) | 🟢 Low |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Configuration Settings (Future Implementation)
|
||||||
|
|
||||||
|
```php
|
||||||
|
// Plugin settings structure
|
||||||
|
$igny8_plugin_settings = [
|
||||||
|
'table_of_contents' => [
|
||||||
|
'enabled' => true,
|
||||||
|
'show_numbers' => true,
|
||||||
|
'collapsible' => false,
|
||||||
|
'sticky' => false,
|
||||||
|
'min_headings' => 3,
|
||||||
|
'position' => 'after_featured_image', // or 'before_content', 'floating'
|
||||||
|
],
|
||||||
|
'widgets' => [
|
||||||
|
'enabled' => false,
|
||||||
|
'sections' => [
|
||||||
|
'section_1' => [
|
||||||
|
'position' => 'right',
|
||||||
|
'widget_type' => 'none', // 'related_posts', 'custom_html', 'ad_unit', etc.
|
||||||
|
'content' => '',
|
||||||
|
],
|
||||||
|
'section_3' => [
|
||||||
|
'position' => 'left',
|
||||||
|
'widget_type' => 'none',
|
||||||
|
'content' => '',
|
||||||
|
],
|
||||||
|
],
|
||||||
|
],
|
||||||
|
'images' => [
|
||||||
|
'featured_max_width' => 1024,
|
||||||
|
'landscape_max_width' => 1024,
|
||||||
|
'square_width_percentage' => 50,
|
||||||
|
'show_descriptions_sections' => 4, // Show descriptions in first N sections
|
||||||
|
],
|
||||||
|
'badges' => [
|
||||||
|
'show_section_badges' => true,
|
||||||
|
'max_badges_per_section' => 2,
|
||||||
|
'badge_sources' => ['primary_keyword', 'tags', 'categories', 'secondary_keywords'], // Priority order
|
||||||
|
],
|
||||||
|
];
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Implementation Phases
|
||||||
|
|
||||||
|
### Phase 1: Core Template Updates (Week 1)
|
||||||
|
- ✅ Update CSS container widths and image sizing
|
||||||
|
- ✅ Replace emojis with SVG icons in header
|
||||||
|
- ✅ Add Topic field to header
|
||||||
|
- ✅ Move metadata to bottom with role check
|
||||||
|
- ✅ Implement keyword badge matching logic
|
||||||
|
|
||||||
|
### Phase 2: Advanced Features (Week 2)
|
||||||
|
- ⏳ Table of contents component
|
||||||
|
- ⏳ Widget placeholder system
|
||||||
|
- ⏳ Table detection and image positioning
|
||||||
|
- ⏳ Image reuse logic (sections 5+)
|
||||||
|
|
||||||
|
### Phase 3: App Sync (Week 3)
|
||||||
|
- ⏳ Update ContentViewTemplate.tsx to match WordPress
|
||||||
|
- ⏳ Add TOC component to React app
|
||||||
|
- ⏳ Sync image layouts and sizing
|
||||||
|
|
||||||
|
### Phase 4: Settings & Configuration (Week 4)
|
||||||
|
- ⏳ Plugin settings page
|
||||||
|
- ⏳ TOC configuration options
|
||||||
|
- ⏳ Widget management interface
|
||||||
|
- ⏳ Badge display preferences
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Last Updated:** January 10, 2026
|
||||||
|
**Document Version:** 2.0
|
||||||
|
**Status:** Design Complete - Ready for Implementation
|
||||||
@@ -111,14 +111,17 @@ export const createImagesPageConfig = (
|
|||||||
];
|
];
|
||||||
|
|
||||||
// Add in-article image columns dynamically
|
// Add in-article image columns dynamically
|
||||||
for (let i = 1; i <= maxImages; i++) {
|
// Backend uses 0-indexed positions (0, 1, 2, 3)
|
||||||
|
// Display uses 1-indexed labels (In-Article 1, 2, 3, 4)
|
||||||
|
for (let i = 0; i < maxImages; i++) {
|
||||||
|
const displayIndex = i + 1; // 1-indexed for display
|
||||||
columns.push({
|
columns.push({
|
||||||
key: `in_article_${i}`,
|
key: `in_article_${displayIndex}`,
|
||||||
label: `In-Article ${i}`,
|
label: `In-Article ${displayIndex}`,
|
||||||
sortable: false,
|
sortable: false,
|
||||||
width: '150px',
|
width: '150px',
|
||||||
render: (_value: any, row: ContentImagesGroup) => {
|
render: (_value: any, row: ContentImagesGroup) => {
|
||||||
const image = row.in_article_images.find(img => img.position === i);
|
const image = row.in_article_images.find(img => img.position === i); // 0-indexed position
|
||||||
return (
|
return (
|
||||||
<ContentImageCell
|
<ContentImageCell
|
||||||
image={image || null}
|
image={image || null}
|
||||||
|
|||||||
@@ -94,6 +94,11 @@ export default function SiteSettings() {
|
|||||||
const [aiSettingsLoading, setAiSettingsLoading] = useState(false);
|
const [aiSettingsLoading, setAiSettingsLoading] = useState(false);
|
||||||
const [aiSettingsSaving, setAiSettingsSaving] = useState(false);
|
const [aiSettingsSaving, setAiSettingsSaving] = useState(false);
|
||||||
|
|
||||||
|
// Image size information (from model config)
|
||||||
|
const [featuredImageSize, setFeaturedImageSize] = useState('1792x1024');
|
||||||
|
const [landscapeImageSize, setLandscapeImageSize] = useState('1792x1024');
|
||||||
|
const [squareImageSize, setSquareImageSize] = useState('1024x1024');
|
||||||
|
|
||||||
// Sectors selection state
|
// Sectors selection state
|
||||||
const [industries, setIndustries] = useState<Industry[]>([]);
|
const [industries, setIndustries] = useState<Industry[]>([]);
|
||||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||||
@@ -364,6 +369,11 @@ export default function SiteSettings() {
|
|||||||
setSelectedStyle(response.image_generation.selected_style || 'photorealistic');
|
setSelectedStyle(response.image_generation.selected_style || 'photorealistic');
|
||||||
setMaxImages(response.image_generation.max_images ?? 4);
|
setMaxImages(response.image_generation.max_images ?? 4);
|
||||||
setMaxAllowed(response.image_generation.max_allowed ?? 4);
|
setMaxAllowed(response.image_generation.max_allowed ?? 4);
|
||||||
|
|
||||||
|
// Set image sizes from model config
|
||||||
|
setFeaturedImageSize(response.image_generation.featured_image_size || '1792x1024');
|
||||||
|
setLandscapeImageSize(response.image_generation.landscape_image_size || '1792x1024');
|
||||||
|
setSquareImageSize(response.image_generation.square_image_size || '1024x1024');
|
||||||
}
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('Error loading AI settings:', error);
|
console.error('Error loading AI settings:', error);
|
||||||
@@ -986,6 +996,22 @@ export default function SiteSettings() {
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Image Sizes Display */}
|
||||||
|
<div className="grid grid-cols-3 gap-3 pt-2 border-t border-gray-200 dark:border-gray-700">
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Featured Image</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">{featuredImageSize}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Landscape</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">{landscapeImageSize}</p>
|
||||||
|
</div>
|
||||||
|
<div className="text-center">
|
||||||
|
<p className="text-xs text-gray-500 dark:text-gray-400 mb-1">Square</p>
|
||||||
|
<p className="text-sm font-medium text-gray-900 dark:text-white">{squareImageSize}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@@ -455,7 +455,8 @@ export default function Images() {
|
|||||||
|
|
||||||
if (imageType === 'featured' && contentGroup.featured_image) {
|
if (imageType === 'featured' && contentGroup.featured_image) {
|
||||||
image = contentGroup.featured_image;
|
image = contentGroup.featured_image;
|
||||||
} else if (imageType === 'in_article' && position) {
|
} else if (imageType === 'in_article' && position !== undefined) {
|
||||||
|
// Position is 0-indexed, so check for undefined instead of falsy
|
||||||
image = contentGroup.in_article_images.find(img => img.position === position) || null;
|
image = contentGroup.in_article_images.find(img => img.position === position) || null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -295,10 +295,12 @@ const SectionImageBlock = ({
|
|||||||
image,
|
image,
|
||||||
loading,
|
loading,
|
||||||
heading,
|
heading,
|
||||||
|
showPrompt = true,
|
||||||
}: {
|
}: {
|
||||||
image: ImageRecord | null;
|
image: ImageRecord | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
heading: string;
|
heading: string;
|
||||||
|
showPrompt?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
if (!image && !loading) return null;
|
if (!image && !loading) return null;
|
||||||
|
|
||||||
@@ -323,7 +325,7 @@ const SectionImageBlock = ({
|
|||||||
<ImageStatusPill status={image?.status} />
|
<ImageStatusPill status={image?.status} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{image?.caption && (
|
{showPrompt && image?.caption && (
|
||||||
<figcaption className="space-y-3 px-6 py-5 text-sm leading-relaxed text-gray-600 dark:text-gray-300">
|
<figcaption className="space-y-3 px-6 py-5 text-sm leading-relaxed text-gray-600 dark:text-gray-300">
|
||||||
<p className="font-semibold uppercase tracking-[0.25em] text-gray-400 dark:text-gray-500">
|
<p className="font-semibold uppercase tracking-[0.25em] text-gray-400 dark:text-gray-500">
|
||||||
Image Caption
|
Image Caption
|
||||||
@@ -391,12 +393,14 @@ const splitAtFirstH3 = (html: string): { beforeH3: string; h3AndAfter: string }
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ContentSectionBlock - Renders a content section with image layout based on aspect ratio
|
* ContentSectionBlock - Renders a content section with image layout based on distribution pattern
|
||||||
*
|
*
|
||||||
* Layout rules:
|
* Layout rules (first 4 sections):
|
||||||
* - Single landscape image: 100% width (full width)
|
* - Section 1: Square image right-aligned (50%) with description
|
||||||
* - Single square image: 50% width (centered)
|
* - Section 2: Landscape image full-width (1024px) with description
|
||||||
* - Two square images (paired): Side by side (50% each)
|
* - Section 3: Square image left-aligned (50%) with description
|
||||||
|
* - Section 4: Landscape image full-width (1024px) with description
|
||||||
|
* - Sections 5+: Reuse images without descriptions
|
||||||
*/
|
*/
|
||||||
const ContentSectionBlock = ({
|
const ContentSectionBlock = ({
|
||||||
section,
|
section,
|
||||||
@@ -404,32 +408,33 @@ const ContentSectionBlock = ({
|
|||||||
loading,
|
loading,
|
||||||
index,
|
index,
|
||||||
aspectRatio = 'square',
|
aspectRatio = 'square',
|
||||||
pairedSquareImage = null,
|
imageAlign = 'full',
|
||||||
|
showDescription = true,
|
||||||
}: {
|
}: {
|
||||||
section: ArticleSection;
|
section: ArticleSection;
|
||||||
image: ImageRecord | null;
|
image: ImageRecord | null;
|
||||||
loading: boolean;
|
loading: boolean;
|
||||||
index: number;
|
index: number;
|
||||||
aspectRatio?: 'square' | 'landscape';
|
aspectRatio?: 'square' | 'landscape';
|
||||||
pairedSquareImage?: ImageRecord | null;
|
imageAlign?: 'left' | 'right' | 'full';
|
||||||
|
showDescription?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
const hasImage = Boolean(image);
|
const hasImage = Boolean(image);
|
||||||
const hasPairedImage = Boolean(pairedSquareImage);
|
|
||||||
const headingLabel = section.heading || `Section ${index + 1}`;
|
const headingLabel = section.heading || `Section ${index + 1}`;
|
||||||
const { beforeH3, h3AndAfter } = splitAtFirstH3(section.bodyHtml);
|
const { beforeH3, h3AndAfter } = splitAtFirstH3(section.bodyHtml);
|
||||||
|
|
||||||
// Determine image container width class based on aspect ratio and pairing
|
// Determine image container width class based on aspect ratio and alignment
|
||||||
const getImageContainerClass = () => {
|
const getImageContainerClass = () => {
|
||||||
if (hasPairedImage) {
|
|
||||||
// Two squares side by side
|
|
||||||
return 'w-full';
|
|
||||||
}
|
|
||||||
if (aspectRatio === 'landscape') {
|
if (aspectRatio === 'landscape') {
|
||||||
// Landscape: 100% width
|
return 'w-full max-w-[1024px] mx-auto';
|
||||||
return 'w-full';
|
|
||||||
}
|
}
|
||||||
// Single square: 50% width centered
|
if (imageAlign === 'left') {
|
||||||
return 'w-full max-w-[50%]';
|
return 'w-full max-w-[50%] mr-auto';
|
||||||
|
}
|
||||||
|
if (imageAlign === 'right') {
|
||||||
|
return 'w-full max-w-[50%] ml-auto';
|
||||||
|
}
|
||||||
|
return 'w-full max-w-[50%] mx-auto';
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -460,25 +465,12 @@ const ContentSectionBlock = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* Image section - layout depends on aspect ratio */}
|
{/* Image section - layout depends on aspect ratio and alignment */}
|
||||||
{hasImage && (
|
{hasImage && (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
{hasPairedImage ? (
|
<div className={getImageContainerClass()}>
|
||||||
// Two squares side by side (50% each)
|
<SectionImageBlock image={image} loading={loading} heading={headingLabel} showPrompt={showDescription} />
|
||||||
<div className="grid w-full grid-cols-2 gap-6">
|
</div>
|
||||||
<div className="w-full">
|
|
||||||
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
|
|
||||||
</div>
|
|
||||||
<div className="w-full">
|
|
||||||
<SectionImageBlock image={pairedSquareImage} loading={loading} heading={`${headingLabel} (2)`} />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
// Single image with width based on aspect ratio
|
|
||||||
<div className={getImageContainerClass()}>
|
|
||||||
<SectionImageBlock image={image} loading={loading} heading={headingLabel} />
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -513,21 +505,22 @@ interface ArticleBodyProps {
|
|||||||
const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtml }: ArticleBodyProps) => {
|
const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtml }: ArticleBodyProps) => {
|
||||||
const hasStructuredSections = sections.length > 0;
|
const hasStructuredSections = sections.length > 0;
|
||||||
|
|
||||||
// Determine image aspect ratio from record or fallback to position-based calculation
|
// Image distribution mapping for first 4 sections (matches WordPress template)
|
||||||
// Position 0, 2 = square (1024x1024), Position 1, 3 = landscape (model-specific)
|
const imageDistribution = [
|
||||||
const getImageAspectRatio = (image: ImageRecord | null, index: number): 'square' | 'landscape' => {
|
{ position: 0, type: 'square' as const, align: 'right' as const }, // Section 1
|
||||||
if (image?.aspect_ratio) return image.aspect_ratio;
|
{ position: 3, type: 'landscape' as const, align: 'full' as const }, // Section 2
|
||||||
// Fallback: even positions (0, 2) are square, odd positions (1, 3) are landscape
|
{ position: 2, type: 'square' as const, align: 'left' as const }, // Section 3
|
||||||
return index % 2 === 0 ? 'square' : 'landscape';
|
{ position: 1, type: 'landscape' as const, align: 'full' as const }, // Section 4
|
||||||
};
|
];
|
||||||
|
|
||||||
// Check if two consecutive images are both squares (for side-by-side layout)
|
// Reuse pattern for sections 5+ (without descriptions)
|
||||||
const getNextSquareImage = (currentIndex: number): ImageRecord | null => {
|
const reusePattern = [1, 0, 3, 2];
|
||||||
const nextImage = sectionImages[currentIndex + 1];
|
|
||||||
if (nextImage && getImageAspectRatio(nextImage, currentIndex + 1) === 'square') {
|
// Get image aspect ratio from record or fallback to position-based calculation
|
||||||
return nextImage;
|
const getImageAspectRatio = (image: ImageRecord | null, position: number): 'square' | 'landscape' => {
|
||||||
}
|
if (image?.aspect_ratio) return image.aspect_ratio;
|
||||||
return null;
|
// Fallback: positions 0, 2 are square, positions 1, 3 are landscape
|
||||||
|
return position === 0 || position === 2 ? 'square' : 'landscape';
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!hasStructuredSections && !introHtml && rawHtml) {
|
if (!hasStructuredSections && !introHtml && rawHtml) {
|
||||||
@@ -540,42 +533,46 @@ const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtm
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the first in-article image (position 0)
|
|
||||||
const firstImage = sectionImages.length > 0 ? sectionImages[0] : null;
|
|
||||||
|
|
||||||
// Track which images have been rendered as pairs (to skip the second in the pair)
|
|
||||||
const renderedPairIndices = new Set<number>();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-12">
|
<div className="space-y-12">
|
||||||
{introHtml && <IntroBlock html={introHtml} />}
|
{introHtml && <IntroBlock html={introHtml} />}
|
||||||
{sections.map((section, index) => {
|
{sections.map((section, sectionIndex) => {
|
||||||
// Skip if this image was already rendered as part of a pair
|
let image: ImageRecord | null = null;
|
||||||
if (renderedPairIndices.has(index)) {
|
let aspectRatio: 'square' | 'landscape' = 'landscape';
|
||||||
return null;
|
let imageAlign: 'left' | 'right' | 'full' = 'full';
|
||||||
}
|
let showDescription = true;
|
||||||
|
|
||||||
const currentImage = sectionImages[index] ?? null;
|
// First 4 sections: use distribution pattern
|
||||||
const currentAspectRatio = getImageAspectRatio(currentImage, index);
|
if (sectionIndex < 4) {
|
||||||
|
const dist = imageDistribution[sectionIndex];
|
||||||
// Check if current is square and next is also square for side-by-side layout
|
const imgPosition = dist.position;
|
||||||
let pairedSquareImage: ImageRecord | null = null;
|
image = sectionImages[imgPosition] ?? null;
|
||||||
if (currentAspectRatio === 'square') {
|
aspectRatio = dist.type;
|
||||||
pairedSquareImage = getNextSquareImage(index);
|
imageAlign = dist.align;
|
||||||
if (pairedSquareImage) {
|
showDescription = true;
|
||||||
renderedPairIndices.add(index + 1); // Mark next as rendered
|
}
|
||||||
|
// Sections 5+: reuse images without descriptions
|
||||||
|
else {
|
||||||
|
const reuseIndex = (sectionIndex - 4) % reusePattern.length;
|
||||||
|
const imgPosition = reusePattern[reuseIndex];
|
||||||
|
image = sectionImages[imgPosition] ?? null;
|
||||||
|
if (image) {
|
||||||
|
aspectRatio = getImageAspectRatio(image, imgPosition);
|
||||||
|
imageAlign = (aspectRatio === 'square') ? (reuseIndex % 2 === 0 ? 'right' : 'left') : 'full';
|
||||||
}
|
}
|
||||||
|
showDescription = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ContentSectionBlock
|
<ContentSectionBlock
|
||||||
key={section.id || `section-${index}`}
|
key={section.id || `section-${sectionIndex}`}
|
||||||
section={section}
|
section={section}
|
||||||
image={currentImage}
|
image={image}
|
||||||
loading={imagesLoading}
|
loading={imagesLoading}
|
||||||
index={index}
|
index={sectionIndex}
|
||||||
aspectRatio={currentAspectRatio}
|
aspectRatio={aspectRatio}
|
||||||
pairedSquareImage={pairedSquareImage}
|
imageAlign={imageAlign}
|
||||||
|
showDescription={showDescription}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
@@ -749,7 +746,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|||||||
if (loading) {
|
if (loading) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||||
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-[1280px] 2xl:max-w-[1530px] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="animate-pulse">
|
<div className="animate-pulse">
|
||||||
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-6"></div>
|
<div className="h-8 bg-gray-200 dark:bg-gray-700 rounded w-1/4 mb-6"></div>
|
||||||
<div className="h-12 bg-gray-200 dark:bg-gray-700 rounded w-3/4 mb-4"></div>
|
<div className="h-12 bg-gray-200 dark:bg-gray-700 rounded w-3/4 mb-4"></div>
|
||||||
@@ -766,7 +763,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|||||||
if (!content) {
|
if (!content) {
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||||
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-[1280px] 2xl:max-w-[1530px] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 p-8 text-center">
|
<div className="bg-white dark:bg-gray-800 rounded-2xl shadow-sm border border-gray-200 dark:border-gray-700 p-8 text-center">
|
||||||
<XCircleIcon className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
<XCircleIcon className="w-16 h-16 text-gray-400 mx-auto mb-4" />
|
||||||
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-2">Content Not Found</h2>
|
<h2 className="text-2xl font-semibold text-gray-900 dark:text-white mb-2">Content Not Found</h2>
|
||||||
@@ -834,7 +831,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
<div className="min-h-screen bg-gray-50 dark:bg-gray-900 py-8">
|
||||||
<div className="max-w-[1440px] mx-auto px-4 sm:px-6 lg:px-8">
|
<div className="max-w-[1280px] 2xl:max-w-[1530px] mx-auto px-4 sm:px-6 lg:px-8">
|
||||||
{/* Back Button */}
|
{/* Back Button */}
|
||||||
{onBack && (
|
{onBack && (
|
||||||
<Button
|
<Button
|
||||||
@@ -1103,7 +1100,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
|||||||
|
|
||||||
{/* Featured Image */}
|
{/* Featured Image */}
|
||||||
{shouldShowFeaturedBlock && (
|
{shouldShowFeaturedBlock && (
|
||||||
<div className="mb-12 max-w-[800px] mx-auto">
|
<div className="mb-12 max-w-[1024px] mx-auto">
|
||||||
<FeaturedImageBlock image={resolvedFeaturedImage} loading={imagesLoading} />
|
<FeaturedImageBlock image={resolvedFeaturedImage} loading={imagesLoading} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Plugin Name: IGNY8 WordPress Bridge
|
* Plugin Name: IGNY8 WordPress Bridge
|
||||||
* Plugin URI: https://igny8.com/igny8-wp-bridge
|
* Plugin URI: https://igny8.com/igny8-wp-bridge
|
||||||
* Description: Lightweight bridge plugin that connects WordPress to IGNY8 API for one-way content publishing.
|
* Description: Lightweight bridge plugin that connects WordPress to IGNY8 API for one-way content publishing.
|
||||||
* Version: 1.2.5
|
* Version: 1.2.6
|
||||||
* Author: IGNY8
|
* Author: IGNY8
|
||||||
* Author URI: https://igny8.com/
|
* Author URI: https://igny8.com/
|
||||||
* License: GPL v2 or later
|
* License: GPL v2 or later
|
||||||
@@ -22,7 +22,7 @@ if (!defined('ABSPATH')) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Define plugin constants
|
// Define plugin constants
|
||||||
define('IGNY8_BRIDGE_VERSION', '1.2.5');
|
define('IGNY8_BRIDGE_VERSION', '1.2.6');
|
||||||
define('IGNY8_BRIDGE_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
define('IGNY8_BRIDGE_PLUGIN_DIR', plugin_dir_path(__FILE__));
|
||||||
define('IGNY8_BRIDGE_PLUGIN_URL', plugin_dir_url(__FILE__));
|
define('IGNY8_BRIDGE_PLUGIN_URL', plugin_dir_url(__FILE__));
|
||||||
define('IGNY8_BRIDGE_PLUGIN_FILE', __FILE__);
|
define('IGNY8_BRIDGE_PLUGIN_FILE', __FILE__);
|
||||||
|
|||||||
@@ -199,3 +199,156 @@ function igny8_parse_keywords($keywords) {
|
|||||||
// Remove empty values
|
// Remove empty values
|
||||||
return array_filter($keywords_array);
|
return array_filter($keywords_array);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get section badges based on keyword/tag matching
|
||||||
|
*
|
||||||
|
* @param string $heading Section heading text
|
||||||
|
* @param int $post_id Post ID
|
||||||
|
* @return array Array of badges with 'text' and 'type' keys
|
||||||
|
*/
|
||||||
|
function igny8_get_section_badges($heading, $post_id) {
|
||||||
|
$badges = [];
|
||||||
|
$heading_lower = strtolower($heading);
|
||||||
|
|
||||||
|
// Get post taxonomies and keywords
|
||||||
|
$tags = get_the_tags($post_id);
|
||||||
|
$categories = get_the_category($post_id);
|
||||||
|
$primary_kw = get_post_meta($post_id, '_igny8_primary_keyword', true);
|
||||||
|
$secondary_kws = get_post_meta($post_id, '_igny8_secondary_keywords', true);
|
||||||
|
|
||||||
|
// Priority 1: Primary keyword
|
||||||
|
if ($primary_kw && stripos($heading_lower, strtolower($primary_kw)) !== false) {
|
||||||
|
$badges[] = ['text' => $primary_kw, 'type' => 'primary'];
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 2: Tags
|
||||||
|
if ($tags && !is_wp_error($tags) && count($badges) < 2) {
|
||||||
|
foreach ($tags as $tag) {
|
||||||
|
if (stripos($heading_lower, strtolower($tag->name)) !== false) {
|
||||||
|
$badges[] = ['text' => $tag->name, 'type' => 'tag'];
|
||||||
|
if (count($badges) >= 2) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 3: Categories
|
||||||
|
if ($categories && !is_wp_error($categories) && count($badges) < 2) {
|
||||||
|
foreach ($categories as $cat) {
|
||||||
|
if (stripos($heading_lower, strtolower($cat->name)) !== false) {
|
||||||
|
$badges[] = ['text' => $cat->name, 'type' => 'category'];
|
||||||
|
if (count($badges) >= 2) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Priority 4: Secondary keywords
|
||||||
|
if ($secondary_kws && count($badges) < 2) {
|
||||||
|
$kw_array = is_array($secondary_kws) ? $secondary_kws : explode(',', $secondary_kws);
|
||||||
|
foreach ($kw_array as $kw) {
|
||||||
|
$kw = trim($kw);
|
||||||
|
if (!empty($kw) && stripos($heading_lower, strtolower($kw)) !== false) {
|
||||||
|
$badges[] = ['text' => $kw, 'type' => 'keyword'];
|
||||||
|
if (count($badges) >= 2) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $badges;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if section content contains a table
|
||||||
|
*
|
||||||
|
* @param string $section_html Section HTML content
|
||||||
|
* @return bool True if contains table
|
||||||
|
*/
|
||||||
|
function igny8_section_has_table($section_html) {
|
||||||
|
return (stripos($section_html, '<table') !== false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get image aspect ratio from position
|
||||||
|
*
|
||||||
|
* @param int $position Image position (0-3)
|
||||||
|
* @return string 'square' or 'landscape'
|
||||||
|
*/
|
||||||
|
function igny8_get_image_aspect($position) {
|
||||||
|
// Even positions (0, 2) = square
|
||||||
|
// Odd positions (1, 3) = landscape
|
||||||
|
return ($position % 2 === 0) ? 'square' : 'landscape';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determine if image description should be shown
|
||||||
|
*
|
||||||
|
* @param int $section_index Section index (0-based)
|
||||||
|
* @return bool True if should show description
|
||||||
|
*/
|
||||||
|
function igny8_show_image_description($section_index) {
|
||||||
|
// First 4 sections (0-3) show descriptions
|
||||||
|
return ($section_index < 4);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generate table of contents from content
|
||||||
|
*
|
||||||
|
* @param string $content HTML content
|
||||||
|
* @return array Array of TOC items with 'number', 'text', and 'id' keys
|
||||||
|
*/
|
||||||
|
function igny8_generate_table_of_contents($content) {
|
||||||
|
$toc_items = [];
|
||||||
|
|
||||||
|
// Parse content for H2 headings
|
||||||
|
preg_match_all('/<h2[^>]*>(.*?)<\/h2>/i', $content, $matches);
|
||||||
|
|
||||||
|
if (!empty($matches[1])) {
|
||||||
|
foreach ($matches[1] as $index => $heading) {
|
||||||
|
$heading_text = strip_tags($heading);
|
||||||
|
$slug = sanitize_title($heading_text);
|
||||||
|
$toc_items[] = [
|
||||||
|
'number' => $index + 1,
|
||||||
|
'text' => $heading_text,
|
||||||
|
'id' => $slug
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $toc_items;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Render widget placeholder
|
||||||
|
*
|
||||||
|
* @param string $position 'left' or 'right'
|
||||||
|
* @param int $section_index Section index
|
||||||
|
* @return void
|
||||||
|
*/
|
||||||
|
function igny8_render_widget_placeholder($position, $section_index) {
|
||||||
|
// Check if widgets are enabled in settings
|
||||||
|
$widgets_enabled = get_option('igny8_widgets_enabled', false);
|
||||||
|
|
||||||
|
if (!$widgets_enabled) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$placeholder_class = 'igny8-widget-placeholder igny8-widgets-enabled';
|
||||||
|
$placeholder_class .= ' igny8-widget-' . $position;
|
||||||
|
$placeholder_class .= ' igny8-widget-section-' . $section_index;
|
||||||
|
|
||||||
|
?>
|
||||||
|
<div class="<?php echo esc_attr($placeholder_class); ?>"
|
||||||
|
data-widget-position="<?php echo esc_attr($position); ?>"
|
||||||
|
data-section-index="<?php echo esc_attr($section_index); ?>">
|
||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* Action hook for widget placeholder content
|
||||||
|
*
|
||||||
|
* @param string $position Widget position (left/right)
|
||||||
|
* @param int $section_index Section index
|
||||||
|
*/
|
||||||
|
do_action('igny8_widget_placeholder', $position, $section_index);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
|||||||
@@ -11,12 +11,19 @@
|
|||||||
|
|
||||||
/* === CSS Variables === */
|
/* === CSS Variables === */
|
||||||
:root {
|
:root {
|
||||||
--igny8-max-width: 1200px;
|
--igny8-max-width: 1280px;
|
||||||
--igny8-spacing: 2rem;
|
--igny8-spacing: 2rem;
|
||||||
--igny8-border-radius: 24px;
|
--igny8-border-radius: 24px;
|
||||||
--igny8-border-radius-md: 16px;
|
--igny8-border-radius-md: 16px;
|
||||||
--igny8-border-radius-sm: 12px;
|
--igny8-border-radius-sm: 12px;
|
||||||
--igny8-border-radius-xs: 8px;
|
--igny8-border-radius-xs: 8px;
|
||||||
|
--igny8-theme-color: rgba(59, 130, 246, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1600px) {
|
||||||
|
:root {
|
||||||
|
--igny8-max-width: 1530px;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* === Main Wrapper === */
|
/* === Main Wrapper === */
|
||||||
@@ -255,8 +262,10 @@
|
|||||||
|
|
||||||
.igny8-featured-image {
|
.igny8-featured-image {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
|
max-width: 1024px;
|
||||||
height: auto;
|
height: auto;
|
||||||
display: block;
|
display: block;
|
||||||
|
margin: 0 auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
.igny8-image-prompt {
|
.igny8-image-prompt {
|
||||||
@@ -498,6 +507,59 @@
|
|||||||
display: block;
|
display: block;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Landscape images */
|
||||||
|
.igny8-image-landscape {
|
||||||
|
max-width: 1024px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-image-landscape.igny8-image-reuse .igny8-image-caption {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Square image - Right aligned */
|
||||||
|
.igny8-image-square-right {
|
||||||
|
max-width: 50%;
|
||||||
|
float: right;
|
||||||
|
margin-left: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Square image - Left aligned */
|
||||||
|
.igny8-image-square-left {
|
||||||
|
max-width: 50%;
|
||||||
|
float: left;
|
||||||
|
margin-right: 2rem;
|
||||||
|
margin-bottom: 2rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Table-specific image positioning */
|
||||||
|
.igny8-image-before-table {
|
||||||
|
max-width: 1024px;
|
||||||
|
width: 100%;
|
||||||
|
margin: 0 auto 2rem;
|
||||||
|
display: block;
|
||||||
|
clear: both;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Widget placeholder */
|
||||||
|
.igny8-widget-placeholder {
|
||||||
|
clear: both;
|
||||||
|
min-height: 200px;
|
||||||
|
padding: 1.5rem;
|
||||||
|
margin-top: 1rem;
|
||||||
|
background: rgba(0, 0, 0, 0.02);
|
||||||
|
border: 1px dashed rgba(0, 0, 0, 0.1);
|
||||||
|
border-radius: var(--igny8-border-radius-sm);
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-widget-placeholder.igny8-widgets-enabled {
|
||||||
|
display: block;
|
||||||
|
}
|
||||||
|
|
||||||
.igny8-image-caption {
|
.igny8-image-caption {
|
||||||
padding: 1.25rem;
|
padding: 1.25rem;
|
||||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
@@ -604,6 +666,120 @@
|
|||||||
line-height: 1.4;
|
line-height: 1.4;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* === Table of Contents === */
|
||||||
|
.igny8-table-of-contents {
|
||||||
|
background: var(--wp--preset--color--base, #ffffff);
|
||||||
|
border: 2px solid rgba(0, 0, 0, 0.12);
|
||||||
|
border-radius: var(--igny8-border-radius-md);
|
||||||
|
padding: 1.5rem 2rem;
|
||||||
|
margin-bottom: var(--igny8-spacing);
|
||||||
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
margin-bottom: 1rem;
|
||||||
|
padding-bottom: 1rem;
|
||||||
|
border-bottom: 1px solid rgba(0, 0, 0, 0.08);
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-icon {
|
||||||
|
width: 1.25rem;
|
||||||
|
height: 1.25rem;
|
||||||
|
color: var(--igny8-theme-color);
|
||||||
|
opacity: 0.8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-header h3 {
|
||||||
|
margin: 0;
|
||||||
|
font-size: 1.125rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-list {
|
||||||
|
list-style: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-link {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.75rem;
|
||||||
|
padding: 0.5rem 0.75rem;
|
||||||
|
text-decoration: none;
|
||||||
|
color: inherit;
|
||||||
|
border-radius: var(--igny8-border-radius-xs);
|
||||||
|
transition: background-color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-link:hover {
|
||||||
|
background: rgba(0, 0, 0, 0.04);
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-number {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
background: rgba(59, 130, 246, 0.1);
|
||||||
|
color: rgba(59, 130, 246, 1);
|
||||||
|
border-radius: 50%;
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-toc-text {
|
||||||
|
flex: 1;
|
||||||
|
font-size: 0.9375rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === Section Badges === */
|
||||||
|
.igny8-section-badges {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.75rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-section-badge {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0.375rem 0.875rem;
|
||||||
|
border-radius: var(--igny8-border-radius-xs);
|
||||||
|
font-size: 0.75rem;
|
||||||
|
font-weight: 600;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.05em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-section-badge-primary {
|
||||||
|
background: rgba(59, 130, 246, 0.15);
|
||||||
|
color: rgba(59, 130, 246, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.igny8-section-badge-secondary {
|
||||||
|
background: rgba(59, 130, 246, 0.08);
|
||||||
|
color: rgba(59, 130, 246, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* === SVG Icon Styles === */
|
||||||
|
.igny8-icon {
|
||||||
|
width: 1rem;
|
||||||
|
height: 1rem;
|
||||||
|
color: var(--igny8-theme-color, currentColor);
|
||||||
|
opacity: 0.8;
|
||||||
|
display: inline-block;
|
||||||
|
vertical-align: middle;
|
||||||
|
}
|
||||||
|
|
||||||
/* === Responsive Styles === */
|
/* === Responsive Styles === */
|
||||||
@media (max-width: 768px) {
|
@media (max-width: 768px) {
|
||||||
:root {
|
:root {
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
<?php
|
<?php
|
||||||
/**
|
/**
|
||||||
* IGNY8 Content Sections
|
* IGNY8 Content Sections
|
||||||
* Parses content HTML and displays sections with in-article images
|
* Parses content HTML and displays sections with smart image distribution
|
||||||
|
*
|
||||||
|
* Pattern for first 4 sections:
|
||||||
|
* - Section 1: Square image right-aligned (50%) WITH description
|
||||||
|
* - Section 2: Landscape image full-width (1024px) WITH description
|
||||||
|
* - Section 3: Square image left-aligned (50%) WITH description
|
||||||
|
* - Section 4: Landscape image full-width (1024px) WITH description
|
||||||
|
* - Sections 5+: Reuse images WITHOUT descriptions
|
||||||
*
|
*
|
||||||
* @package Igny8Bridge
|
* @package Igny8Bridge
|
||||||
*/
|
*/
|
||||||
@@ -10,6 +17,17 @@
|
|||||||
if (!defined('ABSPATH')) {
|
if (!defined('ABSPATH')) {
|
||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Image distribution mapping
|
||||||
|
$image_distribution = [
|
||||||
|
0 => ['position' => 0, 'type' => 'square', 'align' => 'right'], // Section 1
|
||||||
|
1 => ['position' => 3, 'type' => 'landscape', 'align' => 'full'], // Section 2
|
||||||
|
2 => ['position' => 2, 'type' => 'square', 'align' => 'left'], // Section 3
|
||||||
|
3 => ['position' => 1, 'type' => 'landscape', 'align' => 'full'], // Section 4
|
||||||
|
];
|
||||||
|
|
||||||
|
// Reuse pattern for sections 5+
|
||||||
|
$reuse_pattern = [1, 0, 3, 2]; // Featured, Square1, Landscape2, Square2
|
||||||
?>
|
?>
|
||||||
|
|
||||||
<div class="igny8-content-body">
|
<div class="igny8-content-body">
|
||||||
@@ -33,72 +51,168 @@ if (!defined('ABSPATH')) {
|
|||||||
<div class="igny8-section-header">
|
<div class="igny8-section-header">
|
||||||
<span class="igny8-section-number"><?php echo $index + 1; ?></span>
|
<span class="igny8-section-number"><?php echo $index + 1; ?></span>
|
||||||
<div class="igny8-section-heading-wrapper">
|
<div class="igny8-section-heading-wrapper">
|
||||||
<span class="igny8-section-label">Section Spotlight</span>
|
<?php
|
||||||
|
// Get section badges based on keyword/tag matching
|
||||||
|
$badges = igny8_get_section_badges($section['heading'], get_the_ID());
|
||||||
|
if (!empty($badges)): ?>
|
||||||
|
<div class="igny8-section-badges">
|
||||||
|
<?php foreach ($badges as $badge_index => $badge): ?>
|
||||||
|
<span class="igny8-section-badge <?php echo $badge_index === 0 ? 'igny8-section-badge-primary' : 'igny8-section-badge-secondary'; ?>">
|
||||||
|
<?php echo esc_html($badge['text']); ?>
|
||||||
|
</span>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
<h2 class="igny8-section-heading"><?php echo esc_html($section['heading']); ?></h2>
|
<h2 class="igny8-section-heading"><?php echo esc_html($section['heading']); ?></h2>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<?php
|
<?php
|
||||||
// Get image for this section (position = section index + 1)
|
// Determine which image to use
|
||||||
$section_position = $index + 1;
|
|
||||||
|
|
||||||
// Try multiple sources for in-article images
|
|
||||||
$img_data = null;
|
$img_data = null;
|
||||||
$img_url = null;
|
$img_url = null;
|
||||||
$img_prompt = '';
|
$img_prompt = '';
|
||||||
|
$img_align = 'full';
|
||||||
|
$img_type = 'landscape';
|
||||||
|
$show_description = igny8_show_image_description($index);
|
||||||
|
|
||||||
// Source 1: From $in_article_images array
|
// First 4 sections: use distribution pattern
|
||||||
if (isset($in_article_images[$section_position])) {
|
if ($index < 4 && isset($image_distribution[$index])) {
|
||||||
$img_data = $in_article_images[$section_position];
|
$dist = $image_distribution[$index];
|
||||||
if (isset($img_data['attachment_id'])) {
|
$img_position = $dist['position'];
|
||||||
$img_url = wp_get_attachment_image_url($img_data['attachment_id'], 'large');
|
$img_type = $dist['type'];
|
||||||
$img_prompt = isset($img_data['prompt']) ? $img_data['prompt'] : '';
|
$img_align = $dist['align'];
|
||||||
} elseif (isset($img_data['url'])) {
|
|
||||||
$img_url = $img_data['url'];
|
|
||||||
$img_prompt = isset($img_data['prompt']) ? $img_data['prompt'] : '';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Source 2: Check gallery images meta
|
|
||||||
if (!$img_url) {
|
|
||||||
$gallery = get_post_meta(get_the_ID(), '_igny8_gallery_images', true);
|
|
||||||
if ($gallery && is_array($gallery) && isset($gallery[$index])) {
|
|
||||||
$img_url = wp_get_attachment_image_url($gallery[$index], 'large');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
$has_image = !empty($img_url);
|
|
||||||
?>
|
|
||||||
|
|
||||||
<div class="igny8-section-content<?php echo $has_image ? ' igny8-has-image' : ''; ?>">
|
|
||||||
<div class="igny8-section-text">
|
|
||||||
<div class="igny8-prose">
|
|
||||||
<?php echo $section['content']; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<?php if ($has_image):
|
if (isset($in_article_images[$img_position])) {
|
||||||
$img_alt = '';
|
$img_data = $in_article_images[$img_position];
|
||||||
if (isset($img_data['attachment_id'])) {
|
if (isset($img_data['attachment_id'])) {
|
||||||
$img_alt = get_post_meta($img_data['attachment_id'], '_wp_attachment_image_alt', true);
|
$img_url = wp_get_attachment_image_url($img_data['attachment_id'], 'large');
|
||||||
|
$img_prompt = isset($img_data['prompt']) ? $img_data['prompt'] : '';
|
||||||
|
} elseif (isset($img_data['url'])) {
|
||||||
|
$img_url = $img_data['url'];
|
||||||
|
$img_prompt = isset($img_data['prompt']) ? $img_data['prompt'] : '';
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Sections 5+: reuse images without descriptions
|
||||||
|
elseif ($index >= 4) {
|
||||||
|
$reuse_index = ($index - 4) % count($reuse_pattern);
|
||||||
|
$img_position = $reuse_pattern[$reuse_index];
|
||||||
|
|
||||||
|
// Position 1 is featured image, others are in-article
|
||||||
|
if ($img_position === 1 && $featured_image_id) {
|
||||||
|
$img_url = wp_get_attachment_image_url($featured_image_id, 'large');
|
||||||
|
$img_type = 'landscape';
|
||||||
|
$img_align = 'full';
|
||||||
|
} elseif (isset($in_article_images[$img_position])) {
|
||||||
|
$img_data = $in_article_images[$img_position];
|
||||||
|
if (isset($img_data['attachment_id'])) {
|
||||||
|
$img_url = wp_get_attachment_image_url($img_data['attachment_id'], 'large');
|
||||||
|
} elseif (isset($img_data['url'])) {
|
||||||
|
$img_url = $img_data['url'];
|
||||||
|
}
|
||||||
|
$img_type = igny8_get_image_aspect($img_position);
|
||||||
|
$img_align = ($img_type === 'square') ? (($reuse_index % 2 === 0) ? 'right' : 'left') : 'full';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if section has table
|
||||||
|
$has_table = igny8_section_has_table($section['content']);
|
||||||
|
if ($has_table && $img_url) {
|
||||||
|
// Place full-width image before table
|
||||||
|
$img_type = 'landscape';
|
||||||
|
$img_align = 'full';
|
||||||
|
$img_class = 'igny8-image-landscape igny8-image-before-table';
|
||||||
|
if (!$show_description) {
|
||||||
|
$img_class .= ' igny8-image-reuse';
|
||||||
|
}
|
||||||
?>
|
?>
|
||||||
<div class="igny8-section-image">
|
<div class="igny8-section-content">
|
||||||
<figure class="igny8-image-figure">
|
<figure class="igny8-image-figure">
|
||||||
<img src="<?php echo esc_url($img_url); ?>"
|
<img src="<?php echo esc_url($img_url); ?>"
|
||||||
alt="<?php echo esc_attr($img_alt ?: $section['heading']); ?>"
|
alt="<?php echo esc_attr($section['heading']); ?>"
|
||||||
class="igny8-in-article-image"
|
class="<?php echo esc_attr($img_class); ?>"
|
||||||
loading="lazy">
|
loading="lazy">
|
||||||
<?php if ($img_prompt): ?>
|
<?php if ($show_description && $img_prompt): ?>
|
||||||
<figcaption class="igny8-image-caption">
|
<figcaption class="igny8-image-caption">
|
||||||
<p class="igny8-caption-label">Visual Direction</p>
|
<p class="igny8-caption-label">Visual Direction</p>
|
||||||
<p class="igny8-caption-text"><?php echo esc_html($img_prompt); ?></p>
|
<p class="igny8-caption-text"><?php echo esc_html($img_prompt); ?></p>
|
||||||
</figcaption>
|
</figcaption>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</figure>
|
</figure>
|
||||||
|
<div class="igny8-prose">
|
||||||
|
<?php echo $section['content']; ?>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php
|
||||||
</div>
|
}
|
||||||
|
// Square image (left or right aligned)
|
||||||
|
elseif ($img_url && $img_type === 'square') {
|
||||||
|
$img_class = 'igny8-image-square-' . $img_align;
|
||||||
|
if (!$show_description) {
|
||||||
|
$img_class .= ' igny8-image-reuse';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="igny8-section-content">
|
||||||
|
<figure class="igny8-image-figure <?php echo esc_attr($img_class); ?>">
|
||||||
|
<img src="<?php echo esc_url($img_url); ?>"
|
||||||
|
alt="<?php echo esc_attr($section['heading']); ?>"
|
||||||
|
class="igny8-in-article-image"
|
||||||
|
loading="lazy">
|
||||||
|
<?php if ($show_description && $img_prompt): ?>
|
||||||
|
<figcaption class="igny8-image-caption">
|
||||||
|
<p class="igny8-caption-label">Visual Direction</p>
|
||||||
|
<p class="igny8-caption-text"><?php echo esc_html($img_prompt); ?></p>
|
||||||
|
</figcaption>
|
||||||
|
<?php endif; ?>
|
||||||
|
</figure>
|
||||||
|
<div class="igny8-prose">
|
||||||
|
<?php echo $section['content']; ?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
// Widget placeholder below square images
|
||||||
|
igny8_render_widget_placeholder($img_align, $index);
|
||||||
|
?>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
// Landscape image (full width)
|
||||||
|
elseif ($img_url && $img_type === 'landscape') {
|
||||||
|
$img_class = 'igny8-image-landscape';
|
||||||
|
if (!$show_description) {
|
||||||
|
$img_class .= ' igny8-image-reuse';
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
<div class="igny8-section-content">
|
||||||
|
<figure class="igny8-image-figure">
|
||||||
|
<img src="<?php echo esc_url($img_url); ?>"
|
||||||
|
alt="<?php echo esc_attr($section['heading']); ?>"
|
||||||
|
class="<?php echo esc_attr($img_class); ?>"
|
||||||
|
loading="lazy">
|
||||||
|
<?php if ($show_description && $img_prompt): ?>
|
||||||
|
<figcaption class="igny8-image-caption">
|
||||||
|
<p class="igny8-caption-label">Visual Direction</p>
|
||||||
|
<p class="igny8-caption-text"><?php echo esc_html($img_prompt); ?></p>
|
||||||
|
</figcaption>
|
||||||
|
<?php endif; ?>
|
||||||
|
</figure>
|
||||||
|
<div class="igny8-prose">
|
||||||
|
<?php echo $section['content']; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
// No image
|
||||||
|
else {
|
||||||
|
?>
|
||||||
|
<div class="igny8-section-content">
|
||||||
|
<div class="igny8-prose">
|
||||||
|
<?php echo $section['content']; ?>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|||||||
@@ -27,7 +27,8 @@ if (!$image_url) {
|
|||||||
<div class="igny8-featured-image-wrapper">
|
<div class="igny8-featured-image-wrapper">
|
||||||
<img src="<?php echo esc_url($image_url); ?>"
|
<img src="<?php echo esc_url($image_url); ?>"
|
||||||
alt="<?php echo esc_attr($image_alt ?: get_the_title()); ?>"
|
alt="<?php echo esc_attr($image_alt ?: get_the_title()); ?>"
|
||||||
class="igny8-featured-image"
|
class="igny8-featured-image igny8-image-landscape"
|
||||||
|
style="max-width: 1024px;"
|
||||||
loading="lazy">
|
loading="lazy">
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,9 @@ $status_class = igny8_get_status_class($status);
|
|||||||
<div class="igny8-metadata-row">
|
<div class="igny8-metadata-row">
|
||||||
<!-- Created Date -->
|
<!-- Created Date -->
|
||||||
<div class="igny8-meta-item">
|
<div class="igny8-meta-item">
|
||||||
<span class="igny8-meta-icon">📅</span>
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M6 2a1 1 0 00-1 1v1H4a2 2 0 00-2 2v10a2 2 0 002 2h12a2 2 0 002-2V6a2 2 0 00-2-2h-1V3a1 1 0 10-2 0v1H7V3a1 1 0 00-1-1zm0 5a1 1 0 000 2h8a1 1 0 100-2H6z"/>
|
||||||
|
</svg>
|
||||||
<span class="igny8-meta-label">Posted:</span>
|
<span class="igny8-meta-label">Posted:</span>
|
||||||
<span class="igny8-meta-value"><?php echo get_the_date(); ?></span>
|
<span class="igny8-meta-value"><?php echo get_the_date(); ?></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -44,7 +46,9 @@ $status_class = igny8_get_status_class($status);
|
|||||||
<!-- Word Count -->
|
<!-- Word Count -->
|
||||||
<?php if ($word_count > 0): ?>
|
<?php if ($word_count > 0): ?>
|
||||||
<div class="igny8-meta-item">
|
<div class="igny8-meta-item">
|
||||||
<span class="igny8-meta-icon">📝</span>
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M4 4a2 2 0 012-2h4.586A2 2 0 0112 2.586L15.414 6A2 2 0 0116 7.414V16a2 2 0 01-2 2H6a2 2 0 01-2-2V4zm2 6a1 1 0 011-1h6a1 1 0 110 2H7a1 1 0 01-1-1zm1 3a1 1 0 100 2h6a1 1 0 100-2H7z"/>
|
||||||
|
</svg>
|
||||||
<span class="igny8-meta-label">Words:</span>
|
<span class="igny8-meta-label">Words:</span>
|
||||||
<span class="igny8-meta-value"><?php echo number_format($word_count); ?></span>
|
<span class="igny8-meta-value"><?php echo number_format($word_count); ?></span>
|
||||||
</div>
|
</div>
|
||||||
@@ -52,15 +56,30 @@ $status_class = igny8_get_status_class($status);
|
|||||||
|
|
||||||
<!-- Author -->
|
<!-- Author -->
|
||||||
<div class="igny8-meta-item">
|
<div class="igny8-meta-item">
|
||||||
<span class="igny8-meta-icon">✍️</span>
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M10 9a3 3 0 100-6 3 3 0 000 6zm-7 9a7 7 0 1114 0H3z"/>
|
||||||
|
</svg>
|
||||||
<span class="igny8-meta-label">Author:</span>
|
<span class="igny8-meta-label">Author:</span>
|
||||||
<span class="igny8-meta-value"><?php the_author(); ?></span>
|
<span class="igny8-meta-value"><?php the_author(); ?></span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Topic (Cluster Name) -->
|
||||||
|
<?php if ($cluster_name): ?>
|
||||||
|
<div class="igny8-meta-item">
|
||||||
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M5.05 4.05a7 7 0 119.9 9.9L10 18.9l-4.95-4.95a7 7 0 010-9.9zM10 11a2 2 0 100-4 2 2 0 000 4z"/>
|
||||||
|
</svg>
|
||||||
|
<span class="igny8-meta-label">Topic:</span>
|
||||||
|
<span class="igny8-meta-value"><?php echo esc_html($cluster_name); ?></span>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<!-- Categories -->
|
<!-- Categories -->
|
||||||
<?php if ($categories && !is_wp_error($categories)): ?>
|
<?php if ($categories && !is_wp_error($categories)): ?>
|
||||||
<div class="igny8-meta-item">
|
<div class="igny8-meta-item">
|
||||||
<span class="igny8-meta-icon">📁</span>
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M2 6a2 2 0 012-2h5l2 2h5a2 2 0 012 2v6a2 2 0 01-2 2H4a2 2 0 01-2-2V6z"/>
|
||||||
|
</svg>
|
||||||
<span class="igny8-meta-label">Categories:</span>
|
<span class="igny8-meta-label">Categories:</span>
|
||||||
<div class="igny8-meta-badges">
|
<div class="igny8-meta-badges">
|
||||||
<?php foreach ($categories as $cat): ?>
|
<?php foreach ($categories as $cat): ?>
|
||||||
@@ -73,7 +92,9 @@ $status_class = igny8_get_status_class($status);
|
|||||||
<!-- Tags -->
|
<!-- Tags -->
|
||||||
<?php if ($tags && !is_wp_error($tags)): ?>
|
<?php if ($tags && !is_wp_error($tags)): ?>
|
||||||
<div class="igny8-meta-item">
|
<div class="igny8-meta-item">
|
||||||
<span class="igny8-meta-icon">🏷️</span>
|
<svg class="igny8-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path fill-rule="evenodd" d="M17.707 9.293a1 1 0 010 1.414l-7 7a1 1 0 01-1.414 0l-7-7A.997.997 0 012 10V5a3 3 0 013-3h5c.256 0 .512.098.707.293l7 7zM5 6a1 1 0 100-2 1 1 0 000 2z"/>
|
||||||
|
</svg>
|
||||||
<span class="igny8-meta-label">Tags:</span>
|
<span class="igny8-meta-label">Tags:</span>
|
||||||
<div class="igny8-meta-badges">
|
<div class="igny8-meta-badges">
|
||||||
<?php foreach ($tags as $tag): ?>
|
<?php foreach ($tags as $tag): ?>
|
||||||
@@ -83,69 +104,4 @@ $status_class = igny8_get_status_class($status);
|
|||||||
</div>
|
</div>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- SEO Metadata Section -->
|
|
||||||
<?php if (($meta_title && $meta_title !== get_the_title()) || $meta_description): ?>
|
|
||||||
<div class="igny8-seo-section">
|
|
||||||
<div class="igny8-seo-header">SEO Metadata</div>
|
|
||||||
|
|
||||||
<?php if ($meta_title && $meta_title !== get_the_title()): ?>
|
|
||||||
<div class="igny8-seo-item">
|
|
||||||
<label class="igny8-seo-label">SEO Title:</label>
|
|
||||||
<div class="igny8-seo-value"><?php echo esc_html($meta_title); ?></div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($meta_description): ?>
|
|
||||||
<div class="igny8-seo-item">
|
|
||||||
<label class="igny8-seo-label">Meta Description:</label>
|
|
||||||
<div class="igny8-seo-value"><?php echo esc_html($meta_description); ?></div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<!-- IGNY8 Content Info -->
|
|
||||||
<?php if ($cluster_name || $primary_keyword || $content_type): ?>
|
|
||||||
<div class="igny8-info-section">
|
|
||||||
<div class="igny8-info-header">Content Information</div>
|
|
||||||
<div class="igny8-info-grid">
|
|
||||||
|
|
||||||
<?php if ($content_type): ?>
|
|
||||||
<div class="igny8-info-item">
|
|
||||||
<label>Type:</label>
|
|
||||||
<span><?php echo esc_html(ucfirst($content_type)); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($structure): ?>
|
|
||||||
<div class="igny8-info-item">
|
|
||||||
<label>Structure:</label>
|
|
||||||
<span><?php echo esc_html(ucfirst($structure)); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($cluster_name): ?>
|
|
||||||
<div class="igny8-info-item">
|
|
||||||
<label>Cluster:</label>
|
|
||||||
<span><?php echo esc_html($cluster_name); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($primary_keyword): ?>
|
|
||||||
<div class="igny8-info-item">
|
|
||||||
<label>Primary Keyword:</label>
|
|
||||||
<span><?php echo esc_html($primary_keyword); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
|
|
||||||
<?php if ($source): ?>
|
|
||||||
<div class="igny8-info-item">
|
|
||||||
<label>Source:</label>
|
|
||||||
<span><?php echo esc_html(ucfirst($source)); ?></span>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<?php endif; ?>
|
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
/**
|
/**
|
||||||
* IGNY8 Metadata Footer
|
* IGNY8 Metadata Footer
|
||||||
* Shows IGNY8-specific metadata in collapsible format
|
* Shows IGNY8-specific metadata in collapsible format
|
||||||
|
* Visible only to users with edit_posts capability (Editor, Administrator, Author for own posts)
|
||||||
*
|
*
|
||||||
* @package Igny8Bridge
|
* @package Igny8Bridge
|
||||||
*/
|
*/
|
||||||
@@ -11,8 +12,8 @@ if (!defined('ABSPATH')) {
|
|||||||
exit;
|
exit;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Only show if we have IGNY8 content ID
|
// Only show if we have IGNY8 content ID and user has edit permissions
|
||||||
if (!$content_id) {
|
if (!$content_id || !current_user_can('edit_posts')) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
?>
|
?>
|
||||||
@@ -53,6 +54,12 @@ if (!$content_id) {
|
|||||||
<td><?php echo esc_html(ucfirst($source)); ?></td>
|
<td><?php echo esc_html(ucfirst($source)); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php if ($cluster_name): ?>
|
||||||
|
<tr>
|
||||||
|
<th>Cluster Name:</th>
|
||||||
|
<td><?php echo esc_html($cluster_name); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if ($cluster_id): ?>
|
<?php if ($cluster_id): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Cluster ID:</th>
|
<th>Cluster ID:</th>
|
||||||
@@ -65,6 +72,24 @@ if (!$content_id) {
|
|||||||
<td><?php echo esc_html($sector_id); ?></td>
|
<td><?php echo esc_html($sector_id); ?></td>
|
||||||
</tr>
|
</tr>
|
||||||
<?php endif; ?>
|
<?php endif; ?>
|
||||||
|
<?php if ($meta_title): ?>
|
||||||
|
<tr>
|
||||||
|
<th>Meta Title:</th>
|
||||||
|
<td><?php echo esc_html($meta_title); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($meta_description): ?>
|
||||||
|
<tr>
|
||||||
|
<th>Meta Description:</th>
|
||||||
|
<td><?php echo esc_html($meta_description); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
|
<?php if ($primary_keyword): ?>
|
||||||
|
<tr>
|
||||||
|
<th>Primary Keyword:</th>
|
||||||
|
<td><?php echo esc_html($primary_keyword); ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endif; ?>
|
||||||
<?php if ($keywords_array): ?>
|
<?php if ($keywords_array): ?>
|
||||||
<tr>
|
<tr>
|
||||||
<th>Secondary Keywords:</th>
|
<th>Secondary Keywords:</th>
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* IGNY8 Table of Contents
|
||||||
|
* Displays clickable list of H2 headings with smooth scroll
|
||||||
|
*
|
||||||
|
* @package Igny8Bridge
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Exit if accessed directly
|
||||||
|
if (!defined('ABSPATH')) {
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Generate TOC from content
|
||||||
|
$toc_items = igny8_generate_table_of_contents($content);
|
||||||
|
|
||||||
|
// Only show if we have at least 3 headings (configurable via settings)
|
||||||
|
$min_headings = get_option('igny8_toc_min_headings', 3);
|
||||||
|
|
||||||
|
if (count($toc_items) < $min_headings) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
?>
|
||||||
|
|
||||||
|
<nav class="igny8-table-of-contents">
|
||||||
|
<div class="igny8-toc-header">
|
||||||
|
<svg class="igny8-toc-icon" viewBox="0 0 20 20" fill="currentColor">
|
||||||
|
<path d="M9 4.804A7.968 7.968 0 005.5 4c-1.255 0-2.443.29-3.5.804v10A7.969 7.969 0 015.5 14c1.669 0 3.218.51 4.5 1.385A7.962 7.962 0 0114.5 14c1.255 0 2.443.29 3.5.804v-10A7.968 7.968 0 0014.5 4c-1.255 0-2.443.29-3.5.804V12a1 1 0 11-2 0V4.804z"/>
|
||||||
|
</svg>
|
||||||
|
<h3>Table of Contents</h3>
|
||||||
|
</div>
|
||||||
|
<ol class="igny8-toc-list">
|
||||||
|
<?php foreach ($toc_items as $item): ?>
|
||||||
|
<li class="igny8-toc-item">
|
||||||
|
<a href="#<?php echo esc_attr($item['id']); ?>" class="igny8-toc-link">
|
||||||
|
<span class="igny8-toc-number"><?php echo $item['number']; ?></span>
|
||||||
|
<span class="igny8-toc-text"><?php echo esc_html($item['text']); ?></span>
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</ol>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
// Smooth scroll for TOC links
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const tocLinks = document.querySelectorAll('.igny8-toc-link');
|
||||||
|
|
||||||
|
tocLinks.forEach(link => {
|
||||||
|
link.addEventListener('click', function(e) {
|
||||||
|
e.preventDefault();
|
||||||
|
const targetId = this.getAttribute('href').substring(1);
|
||||||
|
const targetElement = document.getElementById(targetId);
|
||||||
|
|
||||||
|
if (targetElement) {
|
||||||
|
targetElement.scrollIntoView({
|
||||||
|
behavior: 'smooth',
|
||||||
|
block: 'start'
|
||||||
|
});
|
||||||
|
|
||||||
|
// Update URL without jumping
|
||||||
|
if (history.pushState) {
|
||||||
|
history.pushState(null, null, '#' + targetId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
@@ -80,6 +80,9 @@ if (defined('WP_DEBUG') && WP_DEBUG) {
|
|||||||
include plugin_dir_path(__FILE__) . 'parts/igny8-featured-image.php';
|
include plugin_dir_path(__FILE__) . 'parts/igny8-featured-image.php';
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Table of Contents
|
||||||
|
include plugin_dir_path(__FILE__) . 'parts/igny8-table-of-contents.php';
|
||||||
|
|
||||||
// Content sections
|
// Content sections
|
||||||
include plugin_dir_path(__FILE__) . 'parts/igny8-content-sections.php';
|
include plugin_dir_path(__FILE__) . 'parts/igny8-content-sections.php';
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user