Skip to main content
Glama

LinkedIn Content Creation MCP Server

by chrishayuk
PREVIEW.md18.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

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/chrishayuk/chuk-mcp-linkedin'

If you have feedback or need assistance with the MCP directory API, please join our Discord server