PREVIEW.md•18.3 kB
# LinkedIn Post Preview System
The preview system generates pixel-perfect HTML previews of LinkedIn posts before publishing. See exactly how your content will appear on LinkedIn with real-time analytics and optimization recommendations.
## Table of Contents
- [Features](#features)
- [Installation](#installation)
- [Quick Start](#quick-start)
- [Text Post Previews](#text-post-previews)
- [Document Post Previews](#document-post-previews)
- [Media Post Previews](#media-post-previews)
- [Preview Components](#preview-components)
- [Advanced Usage](#advanced-usage)
- [API Reference](#api-reference)
## Features
### Visual Preview
- **Authentic LinkedIn UI**: Accurate post card styling including avatar, header, actions
- **Proper formatting**: Line breaks, hashtag highlighting, emoji rendering
- **"See more" indicator**: Visual marker at 210 characters (LinkedIn's truncation point)
- **Document pages**: Actual PDF/PowerPoint/Word pages rendered as images
- **Interactive carousels**: Navigate through multi-page documents
- **Media attachments**: Images, videos, and document files
### Analytics Dashboard
- **Character analysis**: Total count, optimal length indicators, remaining characters
- **Word count**: Track post length
- **Hashtag optimization**: Count analysis with optimal range (3-5)
- **Component detection**: Identifies hooks, CTAs, and engagement elements
- **Performance indicators**: Warnings for too short/long posts
### Document Preview (with `[preview]` dependencies)
- **PDF rendering**: Converts PDF pages to images using pdf2image + poppler
- **PowerPoint support**: Renders PPTX slides as carousel
- **Word documents**: Displays DOCX pages
- **Smart caching**: Converts once, reuses on subsequent previews
- **LinkedIn-accurate**: Matches LinkedIn's actual document post rendering
## Installation
### Basic Installation
```bash
pip install chuk-mcp-linkedin
```
### With Document Preview Support
For rendering actual document pages (PDF, PowerPoint, Word):
```bash
# Install with preview dependencies
pip install chuk-mcp-linkedin[preview]
# System requirements for PDF support (poppler)
# macOS
brew install poppler
# Ubuntu/Debian
sudo apt-get install poppler-utils
# Windows
# Download from: https://github.com/oschwartz10612/poppler-windows/releases
```
The `[preview]` dependencies include:
- `pdf2image` - PDF to image conversion (requires poppler)
- `Pillow` - Image processing and manipulation
- `python-pptx` - PowerPoint file support
- `python-docx` - Word document support
- `PyPDF2` - PDF utilities and page counting
## Quick Start
### Preview Your First Post (60 seconds)
```python
from chuk_mcp_linkedin.posts import ComposablePost
from chuk_mcp_linkedin.preview import LinkedInPreview
from chuk_mcp_linkedin.themes import ThemeManager
# Create a post
theme_mgr = ThemeManager()
theme = theme_mgr.get_theme("thought_leader")
post = ComposablePost("text", theme=theme)
post.add_hook("stat", "80% of B2B buyers prefer thought leadership over ads")
post.add_body("Yet most companies just promote. Here's what works instead...")
post.add_cta("curiosity", "What's your take?")
post.add_hashtags(["ThoughtLeadership", "B2B", "ContentStrategy"])
# Generate preview
text = post.compose()
draft_data = {
"name": "Thought Leadership Post",
"post_type": "text",
"content": {"composed_text": text},
"theme": theme.name
}
stats = {
"char_count": len(text),
"word_count": len(text.split()),
"char_remaining": 3000 - len(text),
"hashtag_count": 3,
"has_hook": True,
"has_cta": True
}
html = LinkedInPreview.generate_html(draft_data, stats=stats, composable_post=post)
preview_path = LinkedInPreview.save_preview(html, ".linkedin_drafts/previews/my_post.html")
print(f"Preview saved: {preview_path}")
# Opens automatically in browser
```
### CLI Preview Tool
The fastest way to preview existing drafts:
```bash
# Preview current draft
python preview_post.py
# Preview specific draft
python preview_post.py draft_id_here
# List all available drafts
python preview_post.py --list
```
## Text Post Previews
Text posts include the post text, hashtags, and engagement analytics.
```python
from chuk_mcp_linkedin.posts import ComposablePost
from chuk_mcp_linkedin.preview import LinkedInPreview
post = ComposablePost("text")
post.add_hook("question", "What's your biggest LinkedIn challenge?")
post.add_body("Most professionals struggle with:\n\n→ Creating consistent content\n→ Growing engagement\n→ Converting leads")
post.add_cta("action", "Comment below!")
post.add_hashtags(["LinkedIn", "ContentMarketing"])
text = post.compose()
draft_data = {
"name": "Engagement Question",
"post_type": "text",
"content": {"composed_text": text}
}
html = LinkedInPreview.generate_html(draft_data)
LinkedInPreview.save_preview(html, ".linkedin_drafts/previews/question_post.html")
```
## Document Post Previews
Document posts render actual PDF, PowerPoint, or Word pages as images in an interactive carousel.
### PDF Document
```python
from pathlib import Path
from chuk_mcp_linkedin.posts import ComposablePost
from chuk_mcp_linkedin.preview import LinkedInPreview
from chuk_mcp_linkedin.utils.document_converter import DocumentConverter
# Create post
post = ComposablePost("document")
post.add_hook("question", "How do you share detailed insights with your network?")
post.add_body("PDFs are perfect for sharing research findings and strategic frameworks.")
post.add_cta("curiosity", "What's your preferred format for long-form content?")
post.add_hashtags(["ContentStrategy", "ThoughtLeadership"])
text = post.compose()
# Set up document
pdf_path = Path("./reports/Q4_strategy.pdf")
page_count = DocumentConverter.get_page_count(str(pdf_path))
draft_data = {
"name": "Q4 Strategy Report",
"post_type": "document",
"content": {
"composed_text": text,
"document_file": {
"filename": pdf_path.name,
"filepath": str(pdf_path),
"title": "Q4 Strategy Framework",
"pages": page_count,
"file_type": "pdf"
}
}
}
# Generate preview with rendered pages
html = LinkedInPreview.generate_html(draft_data, composable_post=post)
preview_path = LinkedInPreview.save_preview(html, ".linkedin_drafts/previews/pdf_post.html")
# Opens browser showing:
# - Post text with commentary
# - Interactive carousel with actual PDF pages as images
# - Navigation controls (prev/next buttons)
# - Page counter (1 / 12)
# - Page indicators
```
### PowerPoint Presentation
```python
pptx_path = Path("./presentations/pitch_deck.pptx")
slide_count = DocumentConverter.get_page_count(str(pptx_path))
draft_data = {
"name": "Pitch Deck",
"post_type": "document",
"content": {
"composed_text": post.compose(),
"document_file": {
"filename": pptx_path.name,
"filepath": str(pptx_path),
"title": "Company Pitch Deck 2025",
"pages": slide_count,
"file_type": "pptx"
}
}
}
html = LinkedInPreview.generate_html(draft_data, composable_post=post)
LinkedInPreview.save_preview(html, ".linkedin_drafts/previews/pptx_post.html")
```
### Word Document
```python
docx_path = Path("./reports/whitepaper.docx")
page_count = DocumentConverter.get_page_count(str(docx_path))
draft_data = {
"name": "Industry Whitepaper",
"post_type": "document",
"content": {
"composed_text": post.compose(),
"document_file": {
"filename": docx_path.name,
"filepath": str(docx_path),
"title": "AI in Enterprise: 2025 Trends",
"pages": page_count,
"file_type": "docx"
}
}
}
html = LinkedInPreview.generate_html(draft_data, composable_post=post)
LinkedInPreview.save_preview(html, ".linkedin_drafts/previews/docx_post.html")
```
## Media Post Previews
### Single Image
```python
draft_data = {
"name": "Product Launch",
"post_type": "image",
"content": {
"composed_text": "Excited to announce our new product! 🚀",
"images": [{
"filepath": "/path/to/product.jpg",
"alt_text": "Product screenshot"
}]
}
}
html = LinkedInPreview.generate_html(draft_data)
LinkedInPreview.save_preview(html, ".linkedin_drafts/previews/image_post.html")
```
### Multiple Images
```python
draft_data = {
"name": "Event Recap",
"post_type": "image",
"content": {
"composed_text": "Great event yesterday! Here are some highlights 📸",
"images": [
{"filepath": "/path/to/photo1.jpg", "alt_text": "Team photo"},
{"filepath": "/path/to/photo2.jpg", "alt_text": "Keynote"},
{"filepath": "/path/to/photo3.jpg", "alt_text": "Networking"},
{"filepath": "/path/to/photo4.jpg", "alt_text": "Panel discussion"}
]
}
}
html = LinkedInPreview.generate_html(draft_data)
LinkedInPreview.save_preview(html, ".linkedin_drafts/previews/multi_image_post.html")
```
### Video Post
```python
draft_data = {
"name": "Product Demo",
"post_type": "video",
"content": {
"composed_text": "Watch our latest product demo 🎥",
"video": {
"title": "Product Demo v2.0",
"duration": "2:30",
"thumbnail": "/path/to/thumbnail.jpg"
}
}
}
html = LinkedInPreview.generate_html(draft_data)
LinkedInPreview.save_preview(html, ".linkedin_drafts/previews/video_post.html")
```
## Preview Components
### Post Card Elements
The preview includes all LinkedIn post card elements:
**Header**:
- Profile avatar (circular, gradient background)
- Author name ("Your Name" placeholder)
- Author headline ("Your Headline • 1st")
- Post timestamp ("Just now • 🌍")
**Content Area**:
- Post text with proper formatting
- Line breaks preserved
- Hashtags highlighted in LinkedIn blue (#0a66c2)
- "See more" link at 210 characters
- Expandable/collapsible full text
**Media Attachments**:
- Images (single or grid layout)
- Videos (with play button and duration)
- Documents (carousel with page navigation)
**Actions Bar**:
- Like button (👍)
- Comment button (💬)
- Repost button (🔄)
- Send button (📤)
### Analytics Dashboard
**Character Analysis**:
- Total characters with optimal range indicators
- Too short warning (< 150 chars)
- Optimal length badge (300-800 chars)
- Long post warning (> 2000 chars)
**Engagement Metrics**:
- Word count
- Characters remaining (out of 3000)
- Hashtag count with optimization (3-5 optimal)
- Hook detection (✓ Yes / ❌ No)
- CTA detection (✓ Yes / ❌ No)
## Advanced Usage
### Custom Styling
Override default LinkedIn styling:
```python
# Custom preview with your brand colors
custom_css = """
<style>
.post-card { border: 2px solid #yourcolor; }
.author-name { color: #yourcolor; }
</style>
"""
html = LinkedInPreview.generate_html(draft_data)
html = html.replace('</head>', f'{custom_css}</head>')
LinkedInPreview.save_preview(html, ".linkedin_drafts/previews/branded.html")
```
### Document Conversion Options
Control document rendering quality and caching:
```python
from chuk_mcp_linkedin.utils.document_converter import DocumentConverter
# High-quality rendering (larger files, better quality)
page_images = DocumentConverter.convert_to_images(
filepath="report.pdf",
max_pages=20, # LinkedIn's limit
dpi=300 # High quality (default: 150)
)
# Clear cache for a specific document
cache_key = DocumentConverter._get_cache_key("report.pdf")
DocumentConverter.clear_cache(cache_key)
# Clear all cached documents
DocumentConverter.clear_cache()
```
### Batch Preview Generation
Generate previews for multiple posts:
```python
from pathlib import Path
posts = [
{"name": "Post 1", "content": {...}},
{"name": "Post 2", "content": {...}},
{"name": "Post 3", "content": {...}}
]
preview_dir = Path(".linkedin_drafts/previews/batch")
preview_dir.mkdir(parents=True, exist_ok=True)
for i, post_data in enumerate(posts):
html = LinkedInPreview.generate_html(post_data)
preview_path = preview_dir / f"post_{i+1}.html"
LinkedInPreview.save_preview(html, str(preview_path))
print(f"✓ Generated: {preview_path}")
```
## API Reference
### `LinkedInPreview`
Main class for generating HTML previews.
#### `generate_html(draft_data, stats=None, composable_post=None)`
Generate HTML preview of a LinkedIn post.
**Parameters**:
- `draft_data` (Dict[str, Any]): Draft data containing post content
- `name` (str): Draft name
- `post_type` (str): Type of post (text, document, image, video, etc.)
- `content` (Dict): Post content including text and media
- `theme` (str, optional): Theme name
- `stats` (Dict[str, Any], optional): Analytics data
- `char_count` (int): Character count
- `word_count` (int): Word count
- `char_remaining` (int): Characters remaining
- `hashtag_count` (int): Number of hashtags
- `has_hook` (bool): Whether post has a hook
- `has_cta` (bool): Whether post has a CTA
- `composable_post` (ComposablePost, optional): ComposablePost instance for document embeds
**Returns**: `str` - HTML content
**Example**:
```python
html = LinkedInPreview.generate_html(
draft_data={
"name": "My Post",
"post_type": "text",
"content": {"composed_text": "Hello LinkedIn!"}
},
stats={
"char_count": 15,
"word_count": 2,
"char_remaining": 2985,
"hashtag_count": 0,
"has_hook": False,
"has_cta": False
}
)
```
#### `save_preview(html_content, output_path)`
Save HTML preview to file.
**Parameters**:
- `html_content` (str): HTML content to save
- `output_path` (str): Path to save file (relative or absolute)
**Returns**: `str` - Absolute path to saved file
**Example**:
```python
path = LinkedInPreview.save_preview(
html_content=html,
output_path=".linkedin_drafts/previews/my_post.html"
)
print(f"Saved to: {path}")
```
### `DocumentConverter`
Utility class for converting documents to images.
#### `convert_to_images(filepath, max_pages=None, dpi=150)`
Convert document to images.
**Parameters**:
- `filepath` (str): Path to document file
- `max_pages` (int, optional): Maximum pages to convert (None for all)
- `dpi` (int): DPI for image conversion (higher = better quality, larger files)
**Returns**: `List[str]` - List of paths to generated images
**Example**:
```python
images = DocumentConverter.convert_to_images(
filepath="report.pdf",
max_pages=10,
dpi=150
)
print(f"Generated {len(images)} page images")
```
#### `get_page_count(filepath)`
Get number of pages in document.
**Parameters**:
- `filepath` (str): Path to document file
**Returns**: `int` - Number of pages
**Example**:
```python
pages = DocumentConverter.get_page_count("presentation.pptx")
print(f"Document has {pages} slides")
```
#### `clear_cache(cache_key=None)`
Clear document conversion cache.
**Parameters**:
- `cache_key` (str, optional): Specific document cache key to clear. If None, clears all cache.
**Example**:
```python
# Clear specific document
DocumentConverter.clear_cache("abc123")
# Clear all cached documents
DocumentConverter.clear_cache()
```
## Best Practices
### Performance
1. **Use caching**: Don't clear cache unless documents change
2. **Reasonable DPI**: Use 150 DPI for previews (default), 300 for final
3. **Limit pages**: Set `max_pages=20` to match LinkedIn's limit
4. **Batch generation**: Generate multiple previews in one session
### Preview Workflow
1. **Draft locally**: Write and edit post text
2. **Generate preview**: Create HTML preview
3. **Review in browser**: Check formatting, length, appearance
4. **Iterate**: Make edits and regenerate preview
5. **Publish**: When satisfied, publish to LinkedIn
### Document Attachments
1. **Optimize PDFs**: Keep under 100MB, under 20 pages
2. **Readable fonts**: Use minimum 18pt font size
3. **Square format**: 1920x1920 for best engagement
4. **Test rendering**: Preview before publishing
5. **Fallback text**: Include meaningful post text, don't rely solely on document
## Troubleshooting
### "pdf2image is required" Error
**Problem**: PDF conversion fails with import error.
**Solution**:
```bash
# Install preview dependencies
pip install chuk-mcp-linkedin[preview]
# Install poppler (system dependency)
# macOS
brew install poppler
# Ubuntu
sudo apt-get install poppler-utils
```
### Document Pages Not Rendering
**Problem**: Document preview shows placeholders instead of actual pages.
**Solutions**:
1. Check file path is correct and file exists
2. Ensure preview dependencies are installed
3. Verify poppler is installed (for PDFs)
4. Check error messages in console output
### "Module 'pdf2image' has no attribute '__version__'" Error
**Problem**: Import error even though pdf2image is installed.
**Solution**: This is normal - pdf2image doesn't have a `__version__` attribute. The module is working correctly.
### Cache Issues
**Problem**: Old document pages showing after updating document.
**Solution**:
```python
from chuk_mcp_linkedin.utils.document_converter import DocumentConverter
# Clear cache for specific document
cache_key = DocumentConverter._get_cache_key("your_document.pdf")
DocumentConverter.clear_cache(cache_key)
# Or clear all cache
DocumentConverter.clear_cache()
```
### Preview Not Opening in Browser
**Problem**: Preview file created but doesn't open automatically.
**Solution**:
```python
import webbrowser
from pathlib import Path
preview_path = Path(".linkedin_drafts/previews/my_post.html")
webbrowser.open(f"file://{preview_path.absolute()}")
```
## Examples
See the `examples/` directory for complete working examples:
- `examples/preview_example.py` - Basic text post preview
- `examples/demo_document_page_preview.py` - Document rendering with PDF and PowerPoint
- `examples/demo_document_attachment.py` - Document attachment workflow
- `examples/showcase_media_types.py` - Image and video post previews
## Related Documentation
- [Design Tokens](./TOKENS.md) - Token system for consistent styling
- [Themes](./THEMES.md) - Theme system for different LinkedIn personas
- [README](../README.md) - Main project documentation