Spaces:
Sleeping
Sleeping
| # seo_blog_writer_app.py | |
| import gradio as gr | |
| import google.generativeai as genai | |
| import json | |
| from datetime import datetime | |
| import re | |
| import os | |
| # Example system prompt for Gemini | |
| SYSTEM_PROMPT = ''' | |
| You are an expert SEO blog writer for a fermentation-focused website. | |
| Write a long-form SEO blog post targeting the keyword: "{KEYWORD}". | |
| Use the following writing guidelines: | |
| - Tone: {TONE} (e.g., Informative, Conversational, Friendly) | |
| - Point of View: {POV} (e.g., Third Person, Second Person) | |
| - Audience: Beginners interested in fermentation | |
| Content structure: | |
| - Start with a compelling introduction (hook) | |
| - Use <h2> and <h3> tags for subheadings | |
| - Include a bullet-point section with beginner-friendly tips | |
| - Add a step-by-step guide (if applicable) | |
| - Include an FAQ section with at least 3 relevant questions and answers | |
| - Add a strong conclusion with a CTA (Call to Action) | |
| - Add meta title and meta description at the beginning | |
| - End with <script type="application/ld+json"> containing valid FAQ schema | |
| Formatting: | |
| - Output only valid WordPress-ready HTML (no markdown) | |
| - Use readable paragraphs and short sentences | |
| - Emphasize clarity and structure | |
| ### π INTERNAL LINK MAP | |
| The following keywords should be hyperlinked when they appear naturally in the content: | |
| {LINKS_JSON} | |
| Use this format for links: | |
| <a href="[URL]">[Keyword]</a> | |
| ''' | |
| # Helper to parse keyword:url text into JSON | |
| def parse_links_to_json(raw_text): | |
| try: | |
| link_map = {} | |
| if raw_text.strip(): | |
| lines = raw_text.strip().splitlines() | |
| for line in lines: | |
| if ":" in line: | |
| parts = line.split(":", 1) | |
| keyword = parts[0].strip() | |
| url = parts[1].strip() | |
| if keyword and url: | |
| link_map[keyword] = url | |
| return json.dumps(link_map, indent=2) | |
| except Exception as e: | |
| return "{}" | |
| # Function to insert internal links from user-defined JSON | |
| def insert_internal_links(text, internal_links_json): | |
| try: | |
| if not internal_links_json or internal_links_json == "{}": | |
| return text | |
| links = json.loads(internal_links_json) | |
| for keyword, url in links.items(): | |
| # Only replace the first occurrence to avoid duplicate links | |
| if keyword in text and f'href="{url}"' not in text: | |
| text = text.replace(keyword, f'<a href="{url}">{keyword}</a>', 1) | |
| except Exception as e: | |
| return text + f"\n<!-- Error in internal linking: {str(e)} -->" | |
| return text | |
| def generate_blog(api_key, keyword, tone, pov, internal_links_raw): | |
| try: | |
| # Validate inputs | |
| if not api_key.strip(): | |
| return "Error: Please provide a Gemini API key.", "<p>Please provide a Gemini API key.</p>", gr.File(visible=False) | |
| if not keyword.strip(): | |
| return "Error: Please provide a keyword.", "<p>Please provide a keyword.</p>", gr.File(visible=False) | |
| # Configure Gemini | |
| genai.configure(api_key=api_key) | |
| model = genai.GenerativeModel("gemini-1.5-pro") | |
| # Parse internal links | |
| internal_links_json = parse_links_to_json(internal_links_raw) | |
| # Format links for the prompt | |
| formatted_links = "No internal links provided." | |
| if internal_links_json != "{}": | |
| try: | |
| links_dict = json.loads(internal_links_json) | |
| formatted_links = "\n".join([f"- {k}: {v}" for k, v in links_dict.items()]) | |
| except: | |
| formatted_links = "Error parsing internal links." | |
| # Create the prompt | |
| prompt = SYSTEM_PROMPT.format( | |
| KEYWORD=keyword, | |
| TONE=tone, | |
| POV=pov, | |
| LINKS_JSON=formatted_links | |
| ) | |
| # Generate content | |
| response = model.generate_content(prompt) | |
| raw_html = response.text | |
| # Insert internal links | |
| linked_html = insert_internal_links(raw_html, internal_links_json) | |
| # Create clean preview HTML without custom styling | |
| preview = f""" | |
| <div style="max-height: 500px; overflow-y: auto; padding: 10px;"> | |
| <p><strong>Keyword:</strong> {keyword} | <strong>Tone:</strong> {tone} | <strong>POV:</strong> {pov}</p> | |
| <hr> | |
| {linked_html} | |
| </div> | |
| """ | |
| # Create full HTML file for download (clean version) | |
| full_html = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <meta charset='utf-8'> | |
| <title>{keyword.title()}</title> | |
| <meta name="viewport" content="width=device-width, initial-scale=1.0"> | |
| </head> | |
| <body> | |
| <h1>SEO Blog Post: {keyword.title()}</h1> | |
| <p><em>Tone: {tone} | Point of View: {pov}</em></p> | |
| <hr> | |
| {linked_html} | |
| </body> | |
| </html> | |
| """ | |
| # Create filename and save file | |
| filename = f"seo_post_{datetime.now().strftime('%Y%m%d_%H%M%S')}.html" | |
| # Save to current directory | |
| with open(filename, "w", encoding="utf-8") as f: | |
| f.write(full_html) | |
| return linked_html, preview, gr.File(value=filename, visible=True) | |
| except Exception as e: | |
| error_msg = f"Error generating blog post: {str(e)}" | |
| return error_msg, f"<p>{error_msg}</p>", gr.File(visible=False) | |
| def test_faq_schema(blog_html): | |
| if not blog_html or "Error" in blog_html[:100]: | |
| return "<p>No content to analyze.</p>" | |
| try: | |
| if "application/ld+json" in blog_html: | |
| # Extract schema | |
| start_tag = "<script type=\"application/ld+json\">" | |
| end_tag = "</script>" | |
| start_idx = blog_html.find(start_tag) | |
| if start_idx != -1: | |
| start_idx += len(start_tag) | |
| end_idx = blog_html.find(end_tag, start_idx) | |
| if end_idx != -1: | |
| schema_content = blog_html[start_idx:end_idx].strip() | |
| return f""" | |
| <div> | |
| <h4>β FAQ Schema Detected</h4> | |
| <details> | |
| <summary>View Schema Code</summary> | |
| <pre>{schema_content}</pre> | |
| </details> | |
| </div> | |
| """ | |
| return "<div><h4>β οΈ No FAQ Schema Found</h4><p>The generated content doesn't include FAQ structured data.</p></div>" | |
| except Exception as e: | |
| return f"<p>Error analyzing schema: {str(e)}</p>" | |
| # Create the Gradio interface | |
| with gr.Blocks(theme=gr.themes.Soft(), title="SEO Blog Generator") as demo: | |
| gr.Markdown(""" | |
| # π§ SEO Blog Article Generator | |
| Generate WordPress-ready blog content with metadata, FAQ, internal links, and schema using Gemini 1.5 Pro. | |
| **Quick Start:** | |
| 1. π Enter your Gemini API key | |
| 2. π― Specify your target keyword | |
| 3. π Choose tone and point of view | |
| 4. π Add internal links (optional) | |
| 5. π Click Generate! | |
| """) | |
| # API Key Section | |
| gr.Markdown("### π API Configuration") | |
| api_key_input = gr.Textbox( | |
| label="Gemini API Key", | |
| type="password", | |
| placeholder="Enter your Google Gemini API key...", | |
| info="Get your API key from Google AI Studio" | |
| ) | |
| # Content Settings | |
| gr.Markdown("### βοΈ Content Settings") | |
| with gr.Row(): | |
| keyword_input = gr.Textbox( | |
| label="Target SEO Keyword", | |
| placeholder="e.g., how to ferment carrots at home", | |
| scale=2 | |
| ) | |
| tone_input = gr.Dropdown( | |
| ["Informative", "Conversational", "Friendly", "Professional"], | |
| value="Friendly", | |
| label="Writing Tone", | |
| scale=1 | |
| ) | |
| pov_input = gr.Dropdown( | |
| ["First Person", "Second Person", "Third Person"], | |
| value="Second Person", | |
| label="Point of View", | |
| scale=1 | |
| ) | |
| # Internal Links Section | |
| gr.Markdown("### π Internal Links (Optional)") | |
| internal_links_box = gr.Textbox( | |
| label="Internal Links", | |
| lines=3, | |
| placeholder="kefir:https://example.com/kefir-guide\nkombucha:https://example.com/kombucha-basics", | |
| info="Format: keyword:url (one per line)" | |
| ) | |
| # Generate Button | |
| generate_btn = gr.Button("π Generate Blog Post", variant="primary", size="lg") | |
| # Results Section | |
| gr.Markdown("### π Generated Content") | |
| with gr.Row(): | |
| with gr.Column(): | |
| result_html = gr.Textbox( | |
| label="Generated HTML Code", | |
| lines=12, | |
| info="Copy this HTML to use in WordPress", | |
| max_lines=20, | |
| show_copy_button=True | |
| ) | |
| with gr.Column(): | |
| preview_html = gr.HTML(label="Live Preview") | |
| # Analysis & Download | |
| gr.Markdown("### π Analysis & Download") | |
| with gr.Row(): | |
| with gr.Column(): | |
| faq_test = gr.HTML(label="Schema Analysis") | |
| with gr.Column(): | |
| download_file = gr.File( | |
| label="Download HTML File", | |
| interactive=False, | |
| visible=True | |
| ) | |
| # Event handlers | |
| generate_btn.click( | |
| fn=generate_blog, | |
| inputs=[api_key_input, keyword_input, tone_input, pov_input, internal_links_box], | |
| outputs=[result_html, preview_html, download_file] | |
| ) | |
| result_html.change( | |
| fn=test_faq_schema, | |
| inputs=[result_html], | |
| outputs=[faq_test] | |
| ) | |
| # Add examples | |
| gr.Markdown("### π‘ Example Inputs") | |
| gr.Examples( | |
| examples=[ | |
| ["how to ferment vegetables at home", "Conversational", "Second Person", "fermentation:https://example.com/fermentation-guide\nvegetables:https://example.com/vegetable-guide"], | |
| ["benefits of drinking kefir daily", "Informative", "Third Person", "kefir:https://example.com/kefir-guide\nprobiotics:https://example.com/probiotics-benefits"], | |
| ["making kombucha for beginners", "Friendly", "Second Person", "kombucha:https://example.com/kombucha-basics\nSCOBY:https://example.com/what-is-scoby"] | |
| ], | |
| inputs=[keyword_input, tone_input, pov_input, internal_links_box] | |
| ) | |
| if __name__ == "__main__": | |
| demo.launch( | |
| share=True, | |
| server_name="0.0.0.0", | |
| inbrowser=False, | |
| show_error=True, | |
| quiet=False | |
| ) |