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"
|
||||
)
|
||||
|
||||
# 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
|
||||
max_tokens = models.IntegerField(
|
||||
null=True,
|
||||
@@ -885,6 +906,21 @@ class AIModelConfig(models.Model):
|
||||
is_active=True
|
||||
).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):
|
||||
"""
|
||||
|
||||
@@ -800,6 +800,11 @@ class AIModelConfigAdmin(SimpleHistoryAdmin, Igny8ModelAdmin):
|
||||
'description': 'For IMAGE models only',
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Image Model Sizes', {
|
||||
'fields': ('landscape_size', 'square_size', 'valid_sizes'),
|
||||
'description': 'For IMAGE models: specify supported image dimensions',
|
||||
'classes': ('collapse',)
|
||||
}),
|
||||
('Capabilities', {
|
||||
'fields': ('capabilities',),
|
||||
'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)
|
||||
return success_response(data={
|
||||
'key': setting.key,
|
||||
'config': setting.config,
|
||||
'is_active': setting.is_active,
|
||||
'config': setting.value, # Model uses 'value', frontend expects 'config'
|
||||
'is_active': getattr(setting, 'is_active', True),
|
||||
}, request=request)
|
||||
except AccountSettings.DoesNotExist:
|
||||
# Return default settings if not yet saved
|
||||
@@ -234,17 +234,17 @@ class ContentSettingsViewSet(viewsets.ViewSet):
|
||||
# Filter to only 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(
|
||||
account=account,
|
||||
key=pk,
|
||||
defaults={'config': filtered_config, 'is_active': True}
|
||||
defaults={'value': filtered_config}
|
||||
)
|
||||
|
||||
return success_response(data={
|
||||
'key': setting.key,
|
||||
'config': setting.config,
|
||||
'is_active': setting.is_active,
|
||||
'config': setting.value, # Model uses 'value', frontend expects 'config'
|
||||
'is_active': getattr(setting, 'is_active', True),
|
||||
'message': 'Settings saved successfully',
|
||||
}, request=request)
|
||||
|
||||
@@ -607,8 +607,28 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
|
||||
account=account,
|
||||
key='ai.image_quality_tier'
|
||||
).first()
|
||||
if tier_setting and tier_setting.config:
|
||||
selected_tier = tier_setting.config.get('value', 'quality')
|
||||
if tier_setting and tier_setting.value: # Model uses 'value' field
|
||||
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 = {
|
||||
'content_generation': {
|
||||
@@ -622,6 +642,12 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
|
||||
'selected_style': image_style,
|
||||
'max_images': max_images,
|
||||
'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/
|
||||
|
||||
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,
|
||||
"max_tokens": 4096,
|
||||
@@ -662,6 +693,19 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
|
||||
data = request.data
|
||||
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
|
||||
key_mappings = {
|
||||
'temperature': 'ai.temperature',
|
||||
@@ -672,11 +716,12 @@ class ContentGenerationSettingsViewSet(viewsets.ViewSet):
|
||||
}
|
||||
|
||||
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(
|
||||
account=account,
|
||||
key=account_key,
|
||||
defaults={'config': {'value': data[field]}}
|
||||
defaults={'value': {'value': value}} # Model uses 'value' field
|
||||
)
|
||||
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
|
||||
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({
|
||||
key: `in_article_${i}`,
|
||||
label: `In-Article ${i}`,
|
||||
key: `in_article_${displayIndex}`,
|
||||
label: `In-Article ${displayIndex}`,
|
||||
sortable: false,
|
||||
width: '150px',
|
||||
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 (
|
||||
<ContentImageCell
|
||||
image={image || null}
|
||||
|
||||
@@ -94,6 +94,11 @@ export default function SiteSettings() {
|
||||
const [aiSettingsLoading, setAiSettingsLoading] = 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
|
||||
const [industries, setIndustries] = useState<Industry[]>([]);
|
||||
const [selectedIndustry, setSelectedIndustry] = useState<string>('');
|
||||
@@ -364,6 +369,11 @@ export default function SiteSettings() {
|
||||
setSelectedStyle(response.image_generation.selected_style || 'photorealistic');
|
||||
setMaxImages(response.image_generation.max_images ?? 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) {
|
||||
console.error('Error loading AI settings:', error);
|
||||
@@ -986,6 +996,22 @@ export default function SiteSettings() {
|
||||
className="w-full"
|
||||
/>
|
||||
</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>
|
||||
)}
|
||||
</Card>
|
||||
|
||||
@@ -455,7 +455,8 @@ export default function Images() {
|
||||
|
||||
if (imageType === 'featured' && 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;
|
||||
}
|
||||
|
||||
|
||||
@@ -295,10 +295,12 @@ const SectionImageBlock = ({
|
||||
image,
|
||||
loading,
|
||||
heading,
|
||||
showPrompt = true,
|
||||
}: {
|
||||
image: ImageRecord | null;
|
||||
loading: boolean;
|
||||
heading: string;
|
||||
showPrompt?: boolean;
|
||||
}) => {
|
||||
if (!image && !loading) return null;
|
||||
|
||||
@@ -323,7 +325,7 @@ const SectionImageBlock = ({
|
||||
<ImageStatusPill status={image?.status} />
|
||||
</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">
|
||||
<p className="font-semibold uppercase tracking-[0.25em] text-gray-400 dark:text-gray-500">
|
||||
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:
|
||||
* - Single landscape image: 100% width (full width)
|
||||
* - Single square image: 50% width (centered)
|
||||
* - Two square images (paired): Side by side (50% each)
|
||||
* Layout rules (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
|
||||
*/
|
||||
const ContentSectionBlock = ({
|
||||
section,
|
||||
@@ -404,32 +408,33 @@ const ContentSectionBlock = ({
|
||||
loading,
|
||||
index,
|
||||
aspectRatio = 'square',
|
||||
pairedSquareImage = null,
|
||||
imageAlign = 'full',
|
||||
showDescription = true,
|
||||
}: {
|
||||
section: ArticleSection;
|
||||
image: ImageRecord | null;
|
||||
loading: boolean;
|
||||
index: number;
|
||||
aspectRatio?: 'square' | 'landscape';
|
||||
pairedSquareImage?: ImageRecord | null;
|
||||
imageAlign?: 'left' | 'right' | 'full';
|
||||
showDescription?: boolean;
|
||||
}) => {
|
||||
const hasImage = Boolean(image);
|
||||
const hasPairedImage = Boolean(pairedSquareImage);
|
||||
const headingLabel = section.heading || `Section ${index + 1}`;
|
||||
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 = () => {
|
||||
if (hasPairedImage) {
|
||||
// Two squares side by side
|
||||
return 'w-full';
|
||||
}
|
||||
if (aspectRatio === 'landscape') {
|
||||
// Landscape: 100% width
|
||||
return 'w-full';
|
||||
return 'w-full max-w-[1024px] mx-auto';
|
||||
}
|
||||
// Single square: 50% width centered
|
||||
return 'w-full max-w-[50%]';
|
||||
if (imageAlign === 'left') {
|
||||
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 (
|
||||
@@ -460,25 +465,12 @@ const ContentSectionBlock = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Image section - layout depends on aspect ratio */}
|
||||
{/* Image section - layout depends on aspect ratio and alignment */}
|
||||
{hasImage && (
|
||||
<div className="flex justify-center">
|
||||
{hasPairedImage ? (
|
||||
// Two squares side by side (50% each)
|
||||
<div className="grid w-full grid-cols-2 gap-6">
|
||||
<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} />
|
||||
<SectionImageBlock image={image} loading={loading} heading={headingLabel} showPrompt={showDescription} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -513,21 +505,22 @@ interface ArticleBodyProps {
|
||||
const ArticleBody = ({ introHtml, sections, sectionImages, imagesLoading, rawHtml }: ArticleBodyProps) => {
|
||||
const hasStructuredSections = sections.length > 0;
|
||||
|
||||
// Determine image aspect ratio from record or fallback to position-based calculation
|
||||
// Position 0, 2 = square (1024x1024), Position 1, 3 = landscape (model-specific)
|
||||
const getImageAspectRatio = (image: ImageRecord | null, index: number): 'square' | 'landscape' => {
|
||||
if (image?.aspect_ratio) return image.aspect_ratio;
|
||||
// Fallback: even positions (0, 2) are square, odd positions (1, 3) are landscape
|
||||
return index % 2 === 0 ? 'square' : 'landscape';
|
||||
};
|
||||
// Image distribution mapping for first 4 sections (matches WordPress template)
|
||||
const imageDistribution = [
|
||||
{ position: 0, type: 'square' as const, align: 'right' as const }, // Section 1
|
||||
{ position: 3, type: 'landscape' as const, align: 'full' as const }, // Section 2
|
||||
{ position: 2, type: 'square' as const, align: 'left' as const }, // Section 3
|
||||
{ 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)
|
||||
const getNextSquareImage = (currentIndex: number): ImageRecord | null => {
|
||||
const nextImage = sectionImages[currentIndex + 1];
|
||||
if (nextImage && getImageAspectRatio(nextImage, currentIndex + 1) === 'square') {
|
||||
return nextImage;
|
||||
}
|
||||
return null;
|
||||
// Reuse pattern for sections 5+ (without descriptions)
|
||||
const reusePattern = [1, 0, 3, 2];
|
||||
|
||||
// Get image aspect ratio from record or fallback to position-based calculation
|
||||
const getImageAspectRatio = (image: ImageRecord | null, position: number): 'square' | 'landscape' => {
|
||||
if (image?.aspect_ratio) return image.aspect_ratio;
|
||||
// Fallback: positions 0, 2 are square, positions 1, 3 are landscape
|
||||
return position === 0 || position === 2 ? 'square' : 'landscape';
|
||||
};
|
||||
|
||||
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 (
|
||||
<div className="space-y-12">
|
||||
{introHtml && <IntroBlock html={introHtml} />}
|
||||
{sections.map((section, index) => {
|
||||
// Skip if this image was already rendered as part of a pair
|
||||
if (renderedPairIndices.has(index)) {
|
||||
return null;
|
||||
}
|
||||
{sections.map((section, sectionIndex) => {
|
||||
let image: ImageRecord | null = null;
|
||||
let aspectRatio: 'square' | 'landscape' = 'landscape';
|
||||
let imageAlign: 'left' | 'right' | 'full' = 'full';
|
||||
let showDescription = true;
|
||||
|
||||
const currentImage = sectionImages[index] ?? null;
|
||||
const currentAspectRatio = getImageAspectRatio(currentImage, index);
|
||||
|
||||
// Check if current is square and next is also square for side-by-side layout
|
||||
let pairedSquareImage: ImageRecord | null = null;
|
||||
if (currentAspectRatio === 'square') {
|
||||
pairedSquareImage = getNextSquareImage(index);
|
||||
if (pairedSquareImage) {
|
||||
renderedPairIndices.add(index + 1); // Mark next as rendered
|
||||
// First 4 sections: use distribution pattern
|
||||
if (sectionIndex < 4) {
|
||||
const dist = imageDistribution[sectionIndex];
|
||||
const imgPosition = dist.position;
|
||||
image = sectionImages[imgPosition] ?? null;
|
||||
aspectRatio = dist.type;
|
||||
imageAlign = dist.align;
|
||||
showDescription = true;
|
||||
}
|
||||
// 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 (
|
||||
<ContentSectionBlock
|
||||
key={section.id || `section-${index}`}
|
||||
key={section.id || `section-${sectionIndex}`}
|
||||
section={section}
|
||||
image={currentImage}
|
||||
image={image}
|
||||
loading={imagesLoading}
|
||||
index={index}
|
||||
aspectRatio={currentAspectRatio}
|
||||
pairedSquareImage={pairedSquareImage}
|
||||
index={sectionIndex}
|
||||
aspectRatio={aspectRatio}
|
||||
imageAlign={imageAlign}
|
||||
showDescription={showDescription}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
@@ -749,7 +746,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
||||
if (loading) {
|
||||
return (
|
||||
<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="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>
|
||||
@@ -766,7 +763,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
||||
if (!content) {
|
||||
return (
|
||||
<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">
|
||||
<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>
|
||||
@@ -834,7 +831,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
||||
|
||||
return (
|
||||
<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 */}
|
||||
{onBack && (
|
||||
<Button
|
||||
@@ -1103,7 +1100,7 @@ export default function ContentViewTemplate({ content, loading, onBack }: Conten
|
||||
|
||||
{/* Featured Image */}
|
||||
{shouldShowFeaturedBlock && (
|
||||
<div className="mb-12 max-w-[800px] mx-auto">
|
||||
<div className="mb-12 max-w-[1024px] mx-auto">
|
||||
<FeaturedImageBlock image={resolvedFeaturedImage} loading={imagesLoading} />
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
* Plugin Name: IGNY8 WordPress Bridge
|
||||
* Plugin URI: https://igny8.com/igny8-wp-bridge
|
||||
* 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 URI: https://igny8.com/
|
||||
* License: GPL v2 or later
|
||||
@@ -22,7 +22,7 @@ if (!defined('ABSPATH')) {
|
||||
}
|
||||
|
||||
// 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_URL', plugin_dir_url(__FILE__));
|
||||
define('IGNY8_BRIDGE_PLUGIN_FILE', __FILE__);
|
||||
|
||||
@@ -199,3 +199,156 @@ function igny8_parse_keywords($keywords) {
|
||||
// Remove empty values
|
||||
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 === */
|
||||
:root {
|
||||
--igny8-max-width: 1200px;
|
||||
--igny8-max-width: 1280px;
|
||||
--igny8-spacing: 2rem;
|
||||
--igny8-border-radius: 24px;
|
||||
--igny8-border-radius-md: 16px;
|
||||
--igny8-border-radius-sm: 12px;
|
||||
--igny8-border-radius-xs: 8px;
|
||||
--igny8-theme-color: rgba(59, 130, 246, 1);
|
||||
}
|
||||
|
||||
@media (min-width: 1600px) {
|
||||
:root {
|
||||
--igny8-max-width: 1530px;
|
||||
}
|
||||
}
|
||||
|
||||
/* === Main Wrapper === */
|
||||
@@ -255,8 +262,10 @@
|
||||
|
||||
.igny8-featured-image {
|
||||
width: 100%;
|
||||
max-width: 1024px;
|
||||
height: auto;
|
||||
display: block;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.igny8-image-prompt {
|
||||
@@ -498,6 +507,59 @@
|
||||
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 {
|
||||
padding: 1.25rem;
|
||||
border-top: 1px solid rgba(0, 0, 0, 0.08);
|
||||
@@ -604,6 +666,120 @@
|
||||
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 === */
|
||||
@media (max-width: 768px) {
|
||||
:root {
|
||||
|
||||
@@ -1,7 +1,14 @@
|
||||
<?php
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
@@ -10,6 +17,17 @@
|
||||
if (!defined('ABSPATH')) {
|
||||
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">
|
||||
@@ -33,23 +51,40 @@ if (!defined('ABSPATH')) {
|
||||
<div class="igny8-section-header">
|
||||
<span class="igny8-section-number"><?php echo $index + 1; ?></span>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
// Get image for this section (position = section index + 1)
|
||||
$section_position = $index + 1;
|
||||
|
||||
// Try multiple sources for in-article images
|
||||
// Determine which image to use
|
||||
$img_data = null;
|
||||
$img_url = null;
|
||||
$img_prompt = '';
|
||||
$img_align = 'full';
|
||||
$img_type = 'landscape';
|
||||
$show_description = igny8_show_image_description($index);
|
||||
|
||||
// Source 1: From $in_article_images array
|
||||
if (isset($in_article_images[$section_position])) {
|
||||
$img_data = $in_article_images[$section_position];
|
||||
// First 4 sections: use distribution pattern
|
||||
if ($index < 4 && isset($image_distribution[$index])) {
|
||||
$dist = $image_distribution[$index];
|
||||
$img_position = $dist['position'];
|
||||
$img_type = $dist['type'];
|
||||
$img_align = $dist['align'];
|
||||
|
||||
if (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');
|
||||
$img_prompt = isset($img_data['prompt']) ? $img_data['prompt'] : '';
|
||||
@@ -58,47 +93,126 @@ if (!defined('ABSPATH')) {
|
||||
$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');
|
||||
}
|
||||
}
|
||||
// Sections 5+: reuse images without descriptions
|
||||
elseif ($index >= 4) {
|
||||
$reuse_index = ($index - 4) % count($reuse_pattern);
|
||||
$img_position = $reuse_pattern[$reuse_index];
|
||||
|
||||
$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):
|
||||
$img_alt = '';
|
||||
// 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_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');
|
||||
} 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">
|
||||
<img src="<?php echo esc_url($img_url); ?>"
|
||||
alt="<?php echo esc_attr($img_alt ?: $section['heading']); ?>"
|
||||
class="igny8-in-article-image"
|
||||
alt="<?php echo esc_attr($section['heading']); ?>"
|
||||
class="<?php echo esc_attr($img_class); ?>"
|
||||
loading="lazy">
|
||||
<?php if ($img_prompt): ?>
|
||||
<?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
|
||||
}
|
||||
// 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>
|
||||
</section>
|
||||
|
||||
@@ -27,7 +27,8 @@ if (!$image_url) {
|
||||
<div class="igny8-featured-image-wrapper">
|
||||
<img src="<?php echo esc_url($image_url); ?>"
|
||||
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">
|
||||
</div>
|
||||
|
||||
|
||||
@@ -36,7 +36,9 @@ $status_class = igny8_get_status_class($status);
|
||||
<div class="igny8-metadata-row">
|
||||
<!-- Created Date -->
|
||||
<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-value"><?php echo get_the_date(); ?></span>
|
||||
</div>
|
||||
@@ -44,7 +46,9 @@ $status_class = igny8_get_status_class($status);
|
||||
<!-- Word Count -->
|
||||
<?php if ($word_count > 0): ?>
|
||||
<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-value"><?php echo number_format($word_count); ?></span>
|
||||
</div>
|
||||
@@ -52,15 +56,30 @@ $status_class = igny8_get_status_class($status);
|
||||
|
||||
<!-- Author -->
|
||||
<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-value"><?php the_author(); ?></span>
|
||||
</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 -->
|
||||
<?php if ($categories && !is_wp_error($categories)): ?>
|
||||
<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>
|
||||
<div class="igny8-meta-badges">
|
||||
<?php foreach ($categories as $cat): ?>
|
||||
@@ -73,7 +92,9 @@ $status_class = igny8_get_status_class($status);
|
||||
<!-- Tags -->
|
||||
<?php if ($tags && !is_wp_error($tags)): ?>
|
||||
<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>
|
||||
<div class="igny8-meta-badges">
|
||||
<?php foreach ($tags as $tag): ?>
|
||||
@@ -83,69 +104,4 @@ $status_class = igny8_get_status_class($status);
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
</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>
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
/**
|
||||
* IGNY8 Metadata Footer
|
||||
* Shows IGNY8-specific metadata in collapsible format
|
||||
* Visible only to users with edit_posts capability (Editor, Administrator, Author for own posts)
|
||||
*
|
||||
* @package Igny8Bridge
|
||||
*/
|
||||
@@ -11,8 +12,8 @@ if (!defined('ABSPATH')) {
|
||||
exit;
|
||||
}
|
||||
|
||||
// Only show if we have IGNY8 content ID
|
||||
if (!$content_id) {
|
||||
// Only show if we have IGNY8 content ID and user has edit permissions
|
||||
if (!$content_id || !current_user_can('edit_posts')) {
|
||||
return;
|
||||
}
|
||||
?>
|
||||
@@ -53,6 +54,12 @@ if (!$content_id) {
|
||||
<td><?php echo esc_html(ucfirst($source)); ?></td>
|
||||
</tr>
|
||||
<?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): ?>
|
||||
<tr>
|
||||
<th>Cluster ID:</th>
|
||||
@@ -65,6 +72,24 @@ if (!$content_id) {
|
||||
<td><?php echo esc_html($sector_id); ?></td>
|
||||
</tr>
|
||||
<?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): ?>
|
||||
<tr>
|
||||
<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';
|
||||
}
|
||||
|
||||
// Table of Contents
|
||||
include plugin_dir_path(__FILE__) . 'parts/igny8-table-of-contents.php';
|
||||
|
||||
// Content sections
|
||||
include plugin_dir_path(__FILE__) . 'parts/igny8-content-sections.php';
|
||||
|
||||
|
||||
Reference in New Issue
Block a user