Pamudu13 commited on
Commit
9384799
·
verified ·
1 Parent(s): 8ef8961

Update blog_generator.py

Browse files
Files changed (1) hide show
  1. blog_generator.py +255 -42
blog_generator.py CHANGED
@@ -1,8 +1,9 @@
1
  import requests
 
2
  from typing import Dict, List
3
  import re
4
  import json
5
- from web_scraper import get_cover_image
6
 
7
  class BlogGenerator:
8
  def __init__(self, openai_key: str, openrouter_key: str, serpapi_key: str = None):
@@ -24,6 +25,20 @@ class BlogGenerator:
24
  print(f"Error in get_cover_image: {e}")
25
  return None
26
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
  def create_detailed_plan(self, cluster_data: Dict, preliminary_plan: str, research: str) -> str:
28
  try:
29
  response = requests.post(
@@ -37,34 +52,34 @@ class BlogGenerator:
37
  'model': 'google/gemini-2.0-flash-thinking-exp:free',
38
  'messages': [{
39
  'role': 'user',
40
- 'content': f"""You are part of a team that creates world class blog posts.
41
 
42
- For each new blog post project, you are provided with a list of keywords, a primary keyword, search intent, research findings, and a preliminary blog post plan. Here's a definition of each of the inputs:
43
 
44
- - Keywords: These are the keywords which the blog post is meant to rank for on SEO. They should be scattered throughout the blog post intelligently to help with SEO.
45
 
46
- - Search intent: The search intent recognises the intent of the user when searching up the keyword. Our goal is to optimise the blog post to be highly relevant and valuable to the user, as such the search intent should be satisfied within the blog post.
47
 
48
- - Research findings: This is research found from very reputable resources in relation to the blog post. You must intelligently use this research to make your blog post more reputable.
49
 
50
- - Preliminary plan: A very basic plan set out by your colleague to kick off the blog post.
51
 
52
- - Primary keyword: Out of the keywords, there is one keyword known as the primary keyword. The primary keyword is the keyword which has the highest SEO importance and as such must go in the title and first few sentences of the blog post. It is important that the blog post is highly relevant to the primary keyword, so that it could be placed naturally into the title and introduction sections.
53
 
54
- Given the above info, you must create a detailed plan for the blog post.
55
 
56
- Your output must:
57
 
58
  - Include a plan for the blog post.
59
  - Be in dot point format.
60
- - In each part of the blog post, you must mention which keywords should be placed.
61
- - All keywords must be placed inside the blog post. For each section, mention which keywords to include. The keyword placement must feel natural and must make sense.
62
- - You must include all research points in the blog post. When including the research points, make sure to also include their source URL so that the copywriter can use them as hyperlinks.
63
- - Your plan must satisfy the search intent and revolve directly around the given keywords.
64
- - Your plan must be very detailed.
65
- - Keep in mind the copywriter that will use your plan to write the blog post is not an expert in the topic of the blog post. So you should give them all the detail required so they can just turn it into nicely formatted paragraphs. For example, instead of saying "define X", you must have "define X as ...".
66
- - The plan must have a flow that makes sense.
67
- - Ensure the blog post will be highly detailed and satisfies the most important concepts regarding the topic.
68
 
69
  A new project has just come across your desk with the following details:
70
 
@@ -79,19 +94,59 @@ Create the detailed plan."""
79
  },
80
  timeout=60
81
  )
82
-
83
  if response.status_code != 200:
84
  raise Exception(f"OpenRouter API error: {response.text}")
85
-
86
  response_data = response.json()
87
  if 'choices' not in response_data:
88
  raise Exception(f"Unexpected API response format: {response_data}")
89
-
90
  return response_data['choices'][0]['message']['content']
91
  except Exception as e:
92
  print(f"Error in create_detailed_plan: {e}")
93
  raise
94
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
95
  def write_blog_post(self, detailed_plan: str, cluster_data: Dict) -> str:
96
  try:
97
  response = requests.post(
@@ -141,19 +196,56 @@ Write the blog post."""
141
  },
142
  timeout=120
143
  )
144
-
145
  if response.status_code != 200:
146
  raise Exception(f"OpenRouter API error: {response.text}")
147
-
148
  response_data = response.json()
149
  if 'choices' not in response_data:
150
  raise Exception(f"Unexpected API response format: {response_data}")
151
-
152
  return response_data['choices'][0]['message']['content']
153
  except Exception as e:
154
  print(f"Error in write_blog_post: {e}")
155
  raise
156
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
157
  def add_internal_links(self, blog_content: str, previous_posts: List[Dict]) -> str:
158
  try:
159
  response = requests.post(
@@ -206,30 +298,66 @@ Your output must be the complete blog post with new internal links added. Don't
206
  },
207
  timeout=120
208
  )
209
-
210
  if response.status_code != 200:
211
  raise Exception(f"OpenAI API error: {response.text}")
212
-
213
  response_data = response.json()
214
  if 'choices' not in response_data:
215
  raise Exception(f"Unexpected API response format: {response_data}")
216
-
217
  return response_data['choices'][0]['message']['content']
218
  except Exception as e:
219
  print(f"Error in add_internal_links: {e}")
220
  # If there's an error, return the original content
221
  return blog_content
222
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
223
  def convert_to_html(self, blog_content: str, image_url: str = None) -> str:
224
  try:
225
  # First replace newlines with <br> tags
226
  formatted_content = blog_content.replace('\n', '<br>')
227
-
228
  # Add image URL to the prompt
229
  image_instruction = ""
230
  if image_url:
231
  image_instruction = f"Add this image at the top of the post after the title: {image_url}"
232
-
233
  response = requests.post(
234
  # 'https://api.openai.com/v1/chat/completions',
235
  # headers={
@@ -253,7 +381,7 @@ Your output must be the complete blog post with new internal links added. Don't
253
  The blog post should have:
254
  - Title
255
  {f"- Featured image: <img src='{image_url}' alt='Featured image' style='width: 100%; height: auto; margin: 20px 0;'>" if image_url else ""}
256
- - Estimated reading time
257
  - Key takeaways
258
  - Table of contents
259
  - Body
@@ -275,10 +403,10 @@ Blog post content:
275
  },
276
  timeout=120
277
  )
278
-
279
  if response.status_code != 200:
280
  raise Exception(f"OpenAI API error: {response.text}")
281
-
282
  response_data = response.json()
283
  if 'choices' not in response_data:
284
  raise Exception(f"Unexpected API response format: {response_data}")
@@ -290,6 +418,41 @@ Blog post content:
290
  # If there's an error, return the original content
291
  return blog_content
292
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
293
  def generate_metadata(self, blog_content: str, primary_keyword: str, cluster_data: Dict) -> Dict:
294
  try:
295
  # Generate slug
@@ -324,14 +487,14 @@ Your output must be the slug and nothing else so that I can copy and paste your
324
  },
325
  timeout=60
326
  )
327
-
328
  if slug_response.status_code != 200:
329
  raise Exception(f"OpenAI API error: {slug_response.text}")
330
-
331
  slug_data = slug_response.json()
332
  if 'choices' not in slug_data:
333
  raise Exception(f"Unexpected API response format: {slug_data}")
334
-
335
  slug = slug_data['choices'][0]['message']['content'].strip().lower()
336
 
337
  # Generate title
@@ -366,14 +529,14 @@ Your output must only be the blog post title and nothing else."""
366
  },
367
  timeout=60
368
  )
369
-
370
  if title_response.status_code != 200:
371
  raise Exception(f"OpenAI API error: {title_response.text}")
372
-
373
  title_data = title_response.json()
374
  if 'choices' not in title_data:
375
  raise Exception(f"Unexpected API response format: {title_data}")
376
-
377
  title = title_data['choices'][0]['message']['content'].strip()
378
 
379
  # Generate meta description
@@ -416,14 +579,14 @@ Your output must only be the meta description and nothing else."""
416
  },
417
  timeout=60
418
  )
419
-
420
  if meta_desc_response.status_code != 200:
421
  raise Exception(f"OpenAI API error: {meta_desc_response.text}")
422
-
423
  meta_desc_data = meta_desc_response.json()
424
  if 'choices' not in meta_desc_data:
425
  raise Exception(f"Unexpected API response format: {meta_desc_data}")
426
-
427
  meta_desc = meta_desc_data['choices'][0]['message']['content'].strip()
428
 
429
  # Validate the results
@@ -437,4 +600,54 @@ Your output must only be the meta description and nothing else."""
437
  }
438
  except Exception as e:
439
  print(f"Error in generate_metadata: {e}")
440
- raise
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
  import requests
2
+ import aiohttp
3
  from typing import Dict, List
4
  import re
5
  import json
6
+ from web_scraper import get_cover_image, get_cover_image_async
7
 
8
  class BlogGenerator:
9
  def __init__(self, openai_key: str, openrouter_key: str, serpapi_key: str = None):
 
25
  print(f"Error in get_cover_image: {e}")
26
  return None
27
 
28
+ async def get_cover_image_async(self, title: str) -> str:
29
+ """Get a cover image URL for the blog post asynchronously"""
30
+ try:
31
+ # Use our custom image search function
32
+ image_url = await get_cover_image_async(title)
33
+ if not image_url:
34
+ print("No image found, trying with modified query...")
35
+ # Try again with a more generic query if specific title fails
36
+ image_url = await get_cover_image_async(title + " high quality cover image")
37
+ return image_url
38
+ except Exception as e:
39
+ print(f"Error in get_cover_image_async: {e}")
40
+ return None
41
+
42
  def create_detailed_plan(self, cluster_data: Dict, preliminary_plan: str, research: str) -> str:
43
  try:
44
  response = requests.post(
 
52
  'model': 'google/gemini-2.0-flash-thinking-exp:free',
53
  'messages': [{
54
  'role': 'user',
55
+ 'content': f"""You are part of a team that creates world class blog posts.
56
 
57
+ For each new blog post project, you are provided with a list of keywords, a primary keyword, search intent, research findings, and a preliminary blog post plan. Here's a definition of each of the inputs:
58
 
59
+ - Keywords: These are the keywords which the blog post is meant to rank for on SEO. They should be scattered throughout the blog post intelligently to help with SEO.
60
 
61
+ - Search intent: The search intent recognises the intent of the user when searching up the keyword. Our goal is to optimise the blog post to be highly relevant and valuable to the user, as such the search intent should be satisfied within the blog post.
62
 
63
+ - Research findings: This is research found from very reputable resources in relation to the blog post. You must intelligently use this research to make your blog post more reputable.
64
 
65
+ - Preliminary plan: A very basic plan set out by your colleague to kick off the blog post.
66
 
67
+ - Primary keyword: Out of the keywords, there is one keyword known as the primary keyword. The primary keyword is the keyword which has the highest SEO importance and as such must go in the title and first few sentences of the blog post. It is important that the blog post is highly relevant to the primary keyword, so that it could be placed naturally into the title and introduction sections.
68
 
69
+ Given the above info, you must create a detailed plan for the blog post.
70
 
71
+ Your output must:
72
 
73
  - Include a plan for the blog post.
74
  - Be in dot point format.
75
+ - In each part of the blog post, you must mention which keywords should be placed.
76
+ - All keywords must be placed inside the blog post. For each section, mention which keywords to include. The keyword placement must feel natural and must make sense.
77
+ - You must include all research points in the blog post. When including the research points, make sure to also include their source URL so that the copywriter can use them as hyperlinks.
78
+ - Your plan must satisfy the search intent and revolve directly around the given keywords.
79
+ - Your plan must be very detailed.
80
+ - Keep in mind the copywriter that will use your plan to write the blog post is not an expert in the topic of the blog post. So you should give them all the detail required so they can just turn it into nicely formatted paragraphs. For example, instead of saying "define X", you must have "define X as ...".
81
+ - The plan must have a flow that makes sense.
82
+ - Ensure the blog post will be highly detailed and satisfies the most important concepts regarding the topic.
83
 
84
  A new project has just come across your desk with the following details:
85
 
 
94
  },
95
  timeout=60
96
  )
97
+
98
  if response.status_code != 200:
99
  raise Exception(f"OpenRouter API error: {response.text}")
100
+
101
  response_data = response.json()
102
  if 'choices' not in response_data:
103
  raise Exception(f"Unexpected API response format: {response_data}")
104
+
105
  return response_data['choices'][0]['message']['content']
106
  except Exception as e:
107
  print(f"Error in create_detailed_plan: {e}")
108
  raise
109
 
110
+ async def create_detailed_plan_async(self, cluster_data: Dict, plan: str, research: str) -> str:
111
+ """Create a detailed plan for the blog post asynchronously"""
112
+ try:
113
+ async with aiohttp.ClientSession() as session:
114
+ async with session.post(
115
+ 'https://openrouter.ai/api/v1/chat/completions',
116
+ headers={
117
+ 'Authorization': f'Bearer {self.openrouter_key}',
118
+ 'HTTP-Referer': 'http://localhost:5001',
119
+ 'X-Title': 'Blog Generator'
120
+ },
121
+ json={
122
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
123
+ 'messages': [{
124
+ 'role': 'user',
125
+ 'content': f"""Based on the preliminary plan and research, create a detailed outline for the blog post.
126
+
127
+ Keywords: {cluster_data['Keywords']}
128
+ Primary Keyword: {cluster_data['Primary Keyword']}
129
+ Search Intent: {cluster_data['Intent']}
130
+
131
+ Preliminary Plan:
132
+ {plan}
133
+
134
+ Research:
135
+ {research}
136
+
137
+ Create a detailed outline that incorporates the research findings and ensures all keywords are used naturally."""
138
+ }]
139
+ }
140
+ ) as response:
141
+ if response.status != 200:
142
+ raise Exception(f"OpenRouter API error: {await response.text()}")
143
+
144
+ response_data = await response.json()
145
+ return response_data['choices'][0]['message']['content']
146
+ except Exception as e:
147
+ print(f"Error in create_detailed_plan_async: {e}")
148
+ raise
149
+
150
  def write_blog_post(self, detailed_plan: str, cluster_data: Dict) -> str:
151
  try:
152
  response = requests.post(
 
196
  },
197
  timeout=120
198
  )
199
+
200
  if response.status_code != 200:
201
  raise Exception(f"OpenRouter API error: {response.text}")
202
+
203
  response_data = response.json()
204
  if 'choices' not in response_data:
205
  raise Exception(f"Unexpected API response format: {response_data}")
206
+
207
  return response_data['choices'][0]['message']['content']
208
  except Exception as e:
209
  print(f"Error in write_blog_post: {e}")
210
  raise
211
 
212
+ async def write_blog_post_async(self, detailed_plan: str, cluster_data: Dict) -> str:
213
+ """Write the blog post content asynchronously"""
214
+ try:
215
+ async with aiohttp.ClientSession() as session:
216
+ async with session.post(
217
+ 'https://openrouter.ai/api/v1/chat/completions',
218
+ headers={
219
+ 'Authorization': f'Bearer {self.openrouter_key}',
220
+ 'HTTP-Referer': 'http://localhost:5001',
221
+ 'X-Title': 'Blog Generator'
222
+ },
223
+ json={
224
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
225
+ 'messages': [{
226
+ 'role': 'user',
227
+ 'content': f"""Write a comprehensive blog post based on the detailed plan.
228
+
229
+ Keywords: {cluster_data['Keywords']}
230
+ Primary Keyword: {cluster_data['Primary Keyword']}
231
+ Search Intent: {cluster_data['Intent']}
232
+
233
+ Detailed Plan:
234
+ {detailed_plan}
235
+
236
+ Write a high-quality blog post that naturally incorporates all keywords and satisfies the search intent."""
237
+ }]
238
+ }
239
+ ) as response:
240
+ if response.status != 200:
241
+ raise Exception(f"OpenRouter API error: {await response.text()}")
242
+
243
+ response_data = await response.json()
244
+ return response_data['choices'][0]['message']['content']
245
+ except Exception as e:
246
+ print(f"Error in write_blog_post_async: {e}")
247
+ raise
248
+
249
  def add_internal_links(self, blog_content: str, previous_posts: List[Dict]) -> str:
250
  try:
251
  response = requests.post(
 
298
  },
299
  timeout=120
300
  )
301
+
302
  if response.status_code != 200:
303
  raise Exception(f"OpenAI API error: {response.text}")
304
+
305
  response_data = response.json()
306
  if 'choices' not in response_data:
307
  raise Exception(f"Unexpected API response format: {response_data}")
308
+
309
  return response_data['choices'][0]['message']['content']
310
  except Exception as e:
311
  print(f"Error in add_internal_links: {e}")
312
  # If there's an error, return the original content
313
  return blog_content
314
 
315
+ async def add_internal_links_async(self, content: str, previous_posts: List[Dict]) -> str:
316
+ """Add internal links to the blog post content asynchronously"""
317
+ try:
318
+ async with aiohttp.ClientSession() as session:
319
+ async with session.post(
320
+ 'https://openrouter.ai/api/v1/chat/completions',
321
+ headers={
322
+ 'Authorization': f'Bearer {self.openrouter_key}',
323
+ 'HTTP-Referer': 'http://localhost:5001',
324
+ 'X-Title': 'Blog Generator'
325
+ },
326
+ json={
327
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
328
+ 'messages': [{
329
+ 'role': 'user',
330
+ 'content': f"""Add relevant internal links to the blog post content.
331
+
332
+ Current Content:
333
+ {content}
334
+
335
+ Previous Posts:
336
+ {json.dumps(previous_posts, indent=2)}
337
+
338
+ Add internal links where relevant, maintaining natural flow and readability."""
339
+ }]
340
+ }
341
+ ) as response:
342
+ if response.status != 200:
343
+ raise Exception(f"OpenRouter API error: {await response.text()}")
344
+
345
+ response_data = await response.json()
346
+ return response_data['choices'][0]['message']['content']
347
+ except Exception as e:
348
+ print(f"Error in add_internal_links_async: {e}")
349
+ raise
350
+
351
  def convert_to_html(self, blog_content: str, image_url: str = None) -> str:
352
  try:
353
  # First replace newlines with <br> tags
354
  formatted_content = blog_content.replace('\n', '<br>')
355
+
356
  # Add image URL to the prompt
357
  image_instruction = ""
358
  if image_url:
359
  image_instruction = f"Add this image at the top of the post after the title: {image_url}"
360
+
361
  response = requests.post(
362
  # 'https://api.openai.com/v1/chat/completions',
363
  # headers={
 
381
  The blog post should have:
382
  - Title
383
  {f"- Featured image: <img src='{image_url}' alt='Featured image' style='width: 100%; height: auto; margin: 20px 0;'>" if image_url else ""}
384
+ - Estimated reading time
385
  - Key takeaways
386
  - Table of contents
387
  - Body
 
403
  },
404
  timeout=120
405
  )
406
+
407
  if response.status_code != 200:
408
  raise Exception(f"OpenAI API error: {response.text}")
409
+
410
  response_data = response.json()
411
  if 'choices' not in response_data:
412
  raise Exception(f"Unexpected API response format: {response_data}")
 
418
  # If there's an error, return the original content
419
  return blog_content
420
 
421
+ async def convert_to_html_async(self, content: str, cover_image_url: str) -> str:
422
+ """Convert markdown content to HTML asynchronously"""
423
+ try:
424
+ async with aiohttp.ClientSession() as session:
425
+ async with session.post(
426
+ 'https://openrouter.ai/api/v1/chat/completions',
427
+ headers={
428
+ 'Authorization': f'Bearer {self.openrouter_key}',
429
+ 'HTTP-Referer': 'http://localhost:5001',
430
+ 'X-Title': 'Blog Generator'
431
+ },
432
+ json={
433
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
434
+ 'messages': [{
435
+ 'role': 'user',
436
+ 'content': f"""Convert this markdown content to clean, semantic HTML.
437
+
438
+ Markdown Content:
439
+ {content}
440
+
441
+ Cover Image URL: {cover_image_url}
442
+
443
+ Convert to HTML with proper semantic structure and include the cover image."""
444
+ }]
445
+ }
446
+ ) as response:
447
+ if response.status != 200:
448
+ raise Exception(f"OpenRouter API error: {await response.text()}")
449
+
450
+ response_data = await response.json()
451
+ return response_data['choices'][0]['message']['content']
452
+ except Exception as e:
453
+ print(f"Error in convert_to_html_async: {e}")
454
+ raise
455
+
456
  def generate_metadata(self, blog_content: str, primary_keyword: str, cluster_data: Dict) -> Dict:
457
  try:
458
  # Generate slug
 
487
  },
488
  timeout=60
489
  )
490
+
491
  if slug_response.status_code != 200:
492
  raise Exception(f"OpenAI API error: {slug_response.text}")
493
+
494
  slug_data = slug_response.json()
495
  if 'choices' not in slug_data:
496
  raise Exception(f"Unexpected API response format: {slug_data}")
497
+
498
  slug = slug_data['choices'][0]['message']['content'].strip().lower()
499
 
500
  # Generate title
 
529
  },
530
  timeout=60
531
  )
532
+
533
  if title_response.status_code != 200:
534
  raise Exception(f"OpenAI API error: {title_response.text}")
535
+
536
  title_data = title_response.json()
537
  if 'choices' not in title_data:
538
  raise Exception(f"Unexpected API response format: {title_data}")
539
+
540
  title = title_data['choices'][0]['message']['content'].strip()
541
 
542
  # Generate meta description
 
579
  },
580
  timeout=60
581
  )
582
+
583
  if meta_desc_response.status_code != 200:
584
  raise Exception(f"OpenAI API error: {meta_desc_response.text}")
585
+
586
  meta_desc_data = meta_desc_response.json()
587
  if 'choices' not in meta_desc_data:
588
  raise Exception(f"Unexpected API response format: {meta_desc_data}")
589
+
590
  meta_desc = meta_desc_data['choices'][0]['message']['content'].strip()
591
 
592
  # Validate the results
 
600
  }
601
  except Exception as e:
602
  print(f"Error in generate_metadata: {e}")
603
+ raise
604
+
605
+ async def generate_metadata_async(self, content: str, primary_keyword: str, cluster_data: Dict) -> Dict:
606
+ """Generate metadata for the blog post asynchronously"""
607
+ try:
608
+ async with aiohttp.ClientSession() as session:
609
+ async with session.post(
610
+ 'https://openrouter.ai/api/v1/chat/completions',
611
+ headers={
612
+ 'Authorization': f'Bearer {self.openrouter_key}',
613
+ 'HTTP-Referer': 'http://localhost:5001',
614
+ 'X-Title': 'Blog Generator'
615
+ },
616
+ json={
617
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
618
+ 'messages': [{
619
+ 'role': 'user',
620
+ 'content': f"""Generate SEO metadata for this blog post.
621
+
622
+ Content:
623
+ {content}
624
+
625
+ Primary Keyword: {primary_keyword}
626
+ Keywords: {cluster_data['Keywords']}
627
+ Search Intent: {cluster_data['Intent']}
628
+
629
+ Generate a title, slug, and meta description optimized for SEO."""
630
+ }]
631
+ }
632
+ ) as response:
633
+ if response.status != 200:
634
+ raise Exception(f"OpenRouter API error: {await response.text()}")
635
+
636
+ response_data = await response.json()
637
+ metadata_text = response_data['choices'][0]['message']['content']
638
+
639
+ # Parse the metadata text into a dictionary
640
+ metadata = {}
641
+ for line in metadata_text.split('\n'):
642
+ if ':' in line:
643
+ key, value = line.split(':', 1)
644
+ metadata[key.strip().lower()] = value.strip()
645
+
646
+ return {
647
+ 'title': metadata.get('title', ''),
648
+ 'slug': metadata.get('slug', ''),
649
+ 'meta_description': metadata.get('meta description', '')
650
+ }
651
+ except Exception as e:
652
+ print(f"Error in generate_metadata_async: {e}")
653
+ raise