rsm-roguchi commited on
Commit
3242f2a
·
1 Parent(s): 74f1fe2

pokemon center

Browse files
Files changed (3) hide show
  1. app.py +4 -4
  2. server/general_blog.py +184 -164
  3. ui/general_blog.py +1 -2
app.py CHANGED
@@ -5,7 +5,7 @@ import os
5
 
6
  from ui import (
7
  blog,
8
- #general_blog,
9
  meta,
10
  twitter,
11
  price_matching
@@ -13,7 +13,7 @@ from ui import (
13
 
14
  from server import (
15
  blog as blog_srv,
16
- #general_blog as general_blog_srv,
17
  meta as meta_srv,
18
  twitter as twitter_srv,
19
  price_matching as price_matching_srv
@@ -23,7 +23,7 @@ from server import (
23
  ui = ui.page_fluid(
24
  ui.page_navbar(
25
  blog.ui,
26
- #general_blog.ui,
27
  meta.ui,
28
  twitter.ui,
29
  price_matching.ui,
@@ -36,7 +36,7 @@ ui = ui.page_fluid(
36
 
37
  def server(input, output, session):
38
  blog_srv.server(input, output, session)
39
- #general_blog_srv.server(input, output, session)
40
  meta_srv.server(input, output, session)
41
  twitter_srv.server(input, output, session)
42
  price_matching_srv.server(input, output, session)
 
5
 
6
  from ui import (
7
  blog,
8
+ general_blog,
9
  meta,
10
  twitter,
11
  price_matching
 
13
 
14
  from server import (
15
  blog as blog_srv,
16
+ general_blog as general_blog_srv,
17
  meta as meta_srv,
18
  twitter as twitter_srv,
19
  price_matching as price_matching_srv
 
23
  ui = ui.page_fluid(
24
  ui.page_navbar(
25
  blog.ui,
26
+ general_blog.ui,
27
  meta.ui,
28
  twitter.ui,
29
  price_matching.ui,
 
36
 
37
  def server(input, output, session):
38
  blog_srv.server(input, output, session)
39
+ general_blog_srv.server(input, output, session)
40
  meta_srv.server(input, output, session)
41
  twitter_srv.server(input, output, session)
42
  price_matching_srv.server(input, output, session)
server/general_blog.py CHANGED
@@ -2,8 +2,8 @@ import os, sys, re, ast, time, requests
2
  from bs4 import BeautifulSoup
3
  from pytrends.request import TrendReq
4
  from shiny import ui, reactive, render
 
5
  from dotenv import load_dotenv
6
- import tweepy
7
 
8
  # === LLM Connect ===
9
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "code")))
@@ -16,53 +16,29 @@ SHOPIFY_TOKEN = os.getenv("SHOPIFY_TOKEN")
16
  SHOPIFY_API_VERSION = "2024-04"
17
  BLOG_ID = "73667707064"
18
 
19
- API_KEY = os.getenv("TWITTER_ACC_API_KEY")
20
- API_SECRET = os.getenv("TWITTER_ACC_API_SECRET")
21
- ACCESS_TOKEN = os.getenv("TWITTER_ACC_ACCESS_TOKEN")
22
- ACCESS_TOKEN_SECRET = os.getenv("TWITTER_ACC_ACCESS_TOKEN_SECRET")
23
-
24
- client = tweepy.Client(
25
- consumer_key=API_KEY,
26
- consumer_secret=API_SECRET,
27
- access_token=ACCESS_TOKEN,
28
- access_token_secret=ACCESS_TOKEN_SECRET
29
- )
30
-
31
- generated_tweet = reactive.Value("")
32
-
33
- def generate_tweet_from_topic(topic: str) -> str:
34
- prompt = (
35
- f"You are a social media manager for a hobby e-commerce company called 'Ultima Supply'.\n"
36
- f"Write a detailed, engaging Twitter post (min 200 characters max 280 characters) about this new blog post: '{topic}'.\n"
37
- f"Include emojis and/or 3-5 SEO relevant hashtags. Use casual, fun language."
38
- f"Also include a link to 'ultimasupply.com/blogs/news' to notify users of the new post."
39
- )
40
-
41
- return get_response(
42
- input=prompt,
43
- template=lambda x: x.strip(),
44
- llm='gemini',
45
- md=False,
46
- temperature=0.9,
47
- max_tokens=500
48
- )
49
-
50
  # === Static scraper for pokemon.com ===
51
  def scrape_section_content_from_url(url: str) -> str:
52
  try:
53
- resp = requests.get(url, timeout=10, headers={"User-Agent": "Mozilla/5.0"})
 
 
 
 
54
  if not resp.ok:
55
  print(f"[ERROR] Request failed: {resp.status_code}")
56
  return ""
57
 
58
  soup = BeautifulSoup(resp.text, "html.parser")
59
- sections = soup.find_all("section", class_="bodytext", itemprop="articleBody")
60
 
61
- if not sections:
62
- print("[WARN] No <section class='bodytext' itemprop='articleBody'> found.")
 
 
 
63
  return ""
64
 
65
- texts = [section.get_text(separator=" ", strip=True) for section in sections]
 
66
  return "\n\n".join(texts)
67
 
68
  except Exception as e:
@@ -70,33 +46,56 @@ def scrape_section_content_from_url(url: str) -> str:
70
  return ""
71
 
72
 
 
73
  # === Keyword generation + scraping ===
74
- def get_keywords_and_content(url: str, top_n=5, llm_n=25):
75
  scraped_text = scrape_section_content_from_url(url)
76
  if not scraped_text:
77
  print("[ERROR] No scraped content. Cannot proceed.")
78
  return [], ""
79
 
 
80
  try:
81
  condensed_prompt = (
82
- "From the content below, extract 5 to 7 mid-specific Google search phrases that reflect real user intent. "
83
- "They should describe product types, use cases, or collector topics — not brand names alone. "
84
- "Avoid single-word topics and overly broad terms like 'pokemon'. Each phrase should be 2–5 words, lowercase, and ASCII only.\n\n"
85
- "Return ONLY a valid Python list of strings. No bullets or explanation.\n"
 
 
 
 
 
 
 
 
86
  f"Content:\n{scraped_text}"
87
  )
88
- condensed_topic_raw = get_response(condensed_prompt,
89
- template=lambda x: x.strip(),
90
- llm="gemini",
91
- md=False,
92
- temperature=0.6)
93
- match = re.search(r"\[.*?\]", condensed_topic_raw, re.DOTALL)
94
- condensed_topic = ast.literal_eval(match.group(0)) if match else ["trading cards"]
95
- print(f"Topics: {condensed_topic}")
 
 
 
 
 
 
 
 
 
 
 
 
96
  except Exception as e:
97
- print(f"[WARN] Keyword condensation failed: {e}")
98
  condensed_topic = ["trading cards"]
99
 
 
100
  all_suggestions = set()
101
  try:
102
  pytrends = TrendReq(hl="en-US", tz=360, timeout=10)
@@ -106,21 +105,31 @@ def get_keywords_and_content(url: str, top_n=5, llm_n=25):
106
  if suggestions:
107
  titles = [s["title"] for s in suggestions]
108
  all_suggestions.update(titles)
 
109
  except Exception as e:
110
- print(f"[WARN] PyTrends failed: {e}")
111
 
 
 
 
112
  filtered_keywords = []
113
  if all_suggestions:
114
  filter_prompt = (
115
- f"Scraped article:\n\n{scraped_text[:1500]}\n\n"
116
- f"Keyword suggestions:\n{list(all_suggestions)}\n\n"
117
- "Return only relevant keywords as a Python list of strings."
 
 
 
 
 
 
 
 
 
 
118
  )
119
- raw_filtered = get_response(filter_prompt,
120
- template=lambda x: x.strip(),
121
- llm="gemini",
122
- temperature=0.3,
123
- md=False)
124
  match = re.search(r"\[.*?\]", raw_filtered)
125
  if match:
126
  try:
@@ -128,71 +137,67 @@ def get_keywords_and_content(url: str, top_n=5, llm_n=25):
128
  except:
129
  filtered_keywords = []
130
 
 
131
  if not filtered_keywords:
132
  fallback_prompt = (
133
- f"Generate {llm_n} niche SEO keyword phrases for this article:\n\n{scraped_text}\n\n"
134
- "Comma-separated, lowercase 2–5 word phrases. No formatting."
 
 
 
 
 
 
 
 
 
135
  )
136
- fallback_keywords_raw = get_response(fallback_prompt,
137
- template=lambda x: x.strip(),
138
- llm="gemini",
139
- md=False,
140
- temperature=0.7)
141
  filtered_keywords = [kw.strip() for kw in fallback_keywords_raw.split(",") if kw.strip()]
 
142
 
143
- combined_keywords = list(dict.fromkeys(filtered_keywords))
 
144
  if len(combined_keywords) < 30:
145
  needed = 30 - len(combined_keywords)
 
 
146
  pad_prompt = (
147
- f"Generate {needed} additional relevant SEO phrases for this content:\n\n{scraped_text}\n\n"
148
- f"Do NOT create links as relevant SEO phrases."
149
- "List of 2–5 word, lowercase ASCII keyword phrases. Return a Python list."
 
 
 
 
 
 
 
 
 
150
  )
151
- pad_raw = get_response(pad_prompt,
152
- template=lambda x: x.strip(),
153
- llm="gemini",
154
- md=False,
155
- temperature=0.7)
156
- pad_match = re.search(r"\[.*?\]", pad_raw)
157
- pad_keywords = ast.literal_eval(pad_match.group(0)) if pad_match else []
 
 
 
 
 
 
 
 
 
 
 
 
158
  combined_keywords = list(dict.fromkeys(combined_keywords + pad_keywords))
159
-
160
- print(f"Combined Keywords: {combined_keywords}")
161
- return combined_keywords[:30], scraped_text
162
-
163
-
164
- # === Blog generation ===
165
- def generate_blog_post(scraped_text: str, keywords: list[str]) -> tuple[str, str]:
166
- keyword_str = ", ".join(keywords)
167
-
168
- title_prompt = (
169
- f"Based on the following article:\n\n{scraped_text[:2000]}\n\n"
170
- f"Return a short, descriptive blog post title (max 70 characters). Just the title."
171
- )
172
- title = get_response(title_prompt,
173
- template=lambda x: x.strip().replace('"', ''),
174
- llm="gemini",
175
- temperature=0.5,
176
- md=False
177
- )
178
-
179
- blog_prompt = (
180
- f"You are a content writer for a collectibles brand called 'Ultima Supply'.\n"
181
- f"Adapt the following scraped content into a detailed, SEO-optimized HTML blog post.\n\n"
182
- f"Scraped content:\n{scraped_text}\n\n"
183
- f"Inject the following keywords naturally:\n{keyword_str}\n\n"
184
- f"Use proper HTML: <h1> for title, <h2> for headers, <p> for text.\n"
185
- f"Do NOT include markdown, images, code blocks, or backlinks to other websites.\n"
186
- f"End with a call-to-action:\n<p>Visit <a href='https://ultima-supply.myshopify.com'>Ultima Supply</a> to explore more collectibles.</p>"
187
- )
188
- blog_html = get_response(blog_prompt,
189
- template=lambda x: x.strip(),
190
- llm="gemini",
191
- temperature=0.9,
192
- md=False)
193
- blog_html = re.sub(r"```[a-zA-Z]*\n?", "", blog_html).replace("```", "").strip()
194
-
195
- return title, blog_html
196
 
197
 
198
  # === Shopify publisher ===
@@ -208,93 +213,108 @@ def publish_blog_post(title: str, html_body: str, blog_id: str = BLOG_ID):
208
  "body_html": html_body
209
  }
210
  }
211
- response = requests.post(url, json=data, headers=headers)
212
- return (True, response.json()) if response.status_code == 201 else (False, response.text)
213
-
214
- def post_tweet(text: str) -> str:
215
- try:
216
- user = client.get_me()
217
- print(f"[✅] Authenticated as: {user.data['username']} (ID: {user.data['id']})")
218
- except Exception as e:
219
- print(f"[❌] Failed to authenticate: {e}")
220
-
221
- try:
222
- print(text)
223
- response = client.create_tweet(text=text)
224
- return f"✅ Tweet posted (ID: {response.data['id']})"
225
- except Exception as e:
226
- return f"❌ Failed to post tweet: {e}"
227
 
 
 
 
 
 
228
 
229
- # === Shiny Server ===
230
  # === Shiny Server ===
231
  def server(input, output, session):
232
  related_keywords = reactive.Value([])
233
  generated_blog = reactive.Value(("", "")) # (title, html_content)
234
- twitter_status = reactive.Value("")
235
 
236
  @output
237
  @render.ui
238
  @reactive.event(input.blog_generate_btn)
239
- def blog_result_gen():
240
  url = input.blog_url()
241
  if not url:
242
  return ui.HTML("<p><strong>⚠️ Please enter a URL.</strong></p>")
243
 
244
- keywords, scraped = get_keywords_and_content(url)
245
- if not scraped:
246
- return ui.HTML("<p><strong>❌ Failed to scrape or extract content.</strong></p>")
247
-
248
  related_keywords.set(keywords)
249
- title, blog_html = generate_blog_post(scraped, keywords)
250
- generated_blog.set((title, blog_html))
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
 
252
- tweet = generate_tweet_from_topic(blog_html)
253
- generated_tweet.set(tweet)
 
 
 
 
 
 
 
 
 
 
 
254
 
255
  return ui.HTML(
256
- f"<p><strong>✅ Blog generated with title:</strong> {title}</p>"
257
  f"<p>Click 'Post to Shopify' to publish.</p>{blog_html}"
258
- f"<p><strong>Generated Tweet:</strong><br>{tweet}</p>"
259
  )
260
-
261
- @output
262
- @render.text
263
- def tweet_post_status_gen_blog():
264
- return twitter_status()
265
 
266
  @output
267
  @render.ui
268
- def blog_keywords_used_gen():
269
  kws = related_keywords()
270
  if not kws:
271
  return ui.HTML("<p><strong>No SEO keywords retrieved yet.</strong></p>")
 
272
  return ui.HTML(
273
- f"<p><strong>✅ SEO Keywords Injected ({len(kws)}):</strong></p><ul>" +
274
- "".join(f"<li>{kw}</li>" for kw in kws) +
275
  "</ul>"
276
  )
277
 
278
  @reactive.effect
279
  @reactive.event(input.blog_post_btn)
280
  def post_to_shopify():
281
- title, html = generated_blog()
 
282
  if not html:
283
  ui.notification_show("⚠️ No blog generated yet.", type="warning")
284
  return
285
- success, response = publish_blog_post(title, html)
 
 
286
  if success:
287
  ui.notification_show("✅ Blog posted to Shopify successfully!", type="message")
288
  else:
289
  ui.notification_show(f"❌ Failed to publish: {response}", type="error")
290
 
291
-
292
- @reactive.effect
293
- @reactive.event(input.blog_post_btn)
294
- def _():
295
- tweet = generated_tweet()
296
- if not tweet:
297
- twitter_status.set("⚠️ No tweet generated yet.")
298
- else:
299
- twitter_result = post_tweet(tweet)
300
- twitter_status.set(twitter_result)
 
2
  from bs4 import BeautifulSoup
3
  from pytrends.request import TrendReq
4
  from shiny import ui, reactive, render
5
+ from playwright.async_api import async_playwright
6
  from dotenv import load_dotenv
 
7
 
8
  # === LLM Connect ===
9
  sys.path.append(os.path.abspath(os.path.join(os.path.dirname(__file__), "..", "code")))
 
16
  SHOPIFY_API_VERSION = "2024-04"
17
  BLOG_ID = "73667707064"
18
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
19
  # === Static scraper for pokemon.com ===
20
  def scrape_section_content_from_url(url: str) -> str:
21
  try:
22
+ resp = requests.get(url, timeout=10, headers={
23
+ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)",
24
+ "Accept-Language": "en-US,en;q=0.9",
25
+ "Referer": "https://www.google.com/"
26
+ })
27
  if not resp.ok:
28
  print(f"[ERROR] Request failed: {resp.status_code}")
29
  return ""
30
 
31
  soup = BeautifulSoup(resp.text, "html.parser")
 
32
 
33
+ # Match all divs that contain the full class string
34
+ content_blocks = soup.find_all("div")
35
+
36
+ if not content_blocks:
37
+ print("[WARN] No content blocks matched.")
38
  return ""
39
 
40
+ texts = [div.get_text(separator=" ", strip=True) for div in content_blocks]
41
+ print(f"[INFO] Extracted {len(texts)} content blocks.")
42
  return "\n\n".join(texts)
43
 
44
  except Exception as e:
 
46
  return ""
47
 
48
 
49
+
50
  # === Keyword generation + scraping ===
51
+ async def get_keywords_and_content(url: str, top_n=5, llm_n=25):
52
  scraped_text = scrape_section_content_from_url(url)
53
  if not scraped_text:
54
  print("[ERROR] No scraped content. Cannot proceed.")
55
  return [], ""
56
 
57
+ # === Step 1: Extract condensed topic keywords ===
58
  try:
59
  condensed_prompt = (
60
+ "Extract exactly 5 to 7 Google search phrases from the content below that reflect real user search intent. "
61
+ "Each phrase should describe a specific product, use case, or collector topic — not generic brands or categories.\n\n"
62
+ "⚠️ Rules:\n"
63
+ "- Each phrase must be 2 to 5 words\n"
64
+ "- All phrases must be lowercase and ASCII-only\n"
65
+ "- Do NOT include apostrophes, single quotes, or quotation marks — rewrite or skip any phrases that contain them\n"
66
+ "- Do NOT include single words or overly broad terms like 'pokemon'\n"
67
+ "- Do NOT return line breaks, bullet points, or list formatting\n\n"
68
+ "✅ Output format:\n"
69
+ "Return a single comma-separated string of keyword phrases, with no brackets, no quotes, and no explanation.\n"
70
+ "Example output:\n"
71
+ "vintage charizard value, graded card pricing, rare booster packs, psa 10 umbreon, tcg price trends\n\n"
72
  f"Content:\n{scraped_text}"
73
  )
74
+
75
+
76
+ condensed_topic_raw = get_response(
77
+ input=condensed_prompt,
78
+ template=lambda x: x.strip(),
79
+ llm="gemini",
80
+ md=False,
81
+ temperature=0.6,
82
+ max_tokens=100,
83
+ model_name="gemini-2.0-flash-lite"
84
+ )
85
+ print(condensed_topic_raw)
86
+
87
+ # Parse comma-separated string
88
+ condensed_topic = [kw.strip() for kw in condensed_topic_raw.split(",") if kw.strip()]
89
+
90
+ if not condensed_topic:
91
+ condensed_topic = ["trading cards"]
92
+
93
+ print(f"[INFO] Condensed topic keywords: {condensed_topic}")
94
  except Exception as e:
95
+ print(f"[WARN] Could not infer topics: {e}")
96
  condensed_topic = ["trading cards"]
97
 
98
+ # === Step 2: Pull suggestions from PyTrends ===
99
  all_suggestions = set()
100
  try:
101
  pytrends = TrendReq(hl="en-US", tz=360, timeout=10)
 
105
  if suggestions:
106
  titles = [s["title"] for s in suggestions]
107
  all_suggestions.update(titles)
108
+ print(f"[INFO] Suggestions for '{topic}': {titles[:3]}")
109
  except Exception as e:
110
+ print(f"[WARN] PyTrends suggestions failed: {e}")
111
 
112
+ all_suggestions = list(all_suggestions)
113
+
114
+ # === Step 3: Let Gemini filter suggestions for relevance ===
115
  filtered_keywords = []
116
  if all_suggestions:
117
  filter_prompt = (
118
+ f"The following article was scraped:\n\n{scraped_text[:1500]}\n\n"
119
+ f"Here is a list of keyword suggestions:\n{all_suggestions}\n\n"
120
+ "Return only the keywords that are clearly relevant to the article topic. "
121
+ "Return a valid Python list of strings only. No explanation, bullets, or formatting."
122
+ )
123
+
124
+ raw_filtered = get_response(
125
+ input=filter_prompt,
126
+ template=lambda x: x.strip(),
127
+ llm="gemini",
128
+ md=False,
129
+ temperature=0.3,
130
+ max_tokens=200
131
  )
132
+
 
 
 
 
133
  match = re.search(r"\[.*?\]", raw_filtered)
134
  if match:
135
  try:
 
137
  except:
138
  filtered_keywords = []
139
 
140
+ # === Step 4: Fallback to Gemini keyword generation if needed ===
141
  if not filtered_keywords:
142
  fallback_prompt = (
143
+ f"You are an SEO expert. Generate {llm_n} niche-relevant SEO keywords "
144
+ f"based on this content:\n\n{scraped_text}\n\n"
145
+ "Return a comma-separated list of lowercase 2–5 word search phrases. No formatting."
146
+ )
147
+ fallback_keywords_raw = get_response(
148
+ input=fallback_prompt,
149
+ template=lambda x: x.strip(),
150
+ llm="gemini",
151
+ md=False,
152
+ temperature=0.7,
153
+ max_tokens=400
154
  )
 
 
 
 
 
155
  filtered_keywords = [kw.strip() for kw in fallback_keywords_raw.split(",") if kw.strip()]
156
+ print(f"[INFO] Fallback keywords used: {filtered_keywords[:top_n]}")
157
 
158
+ # === Step 5: Enforce minimum of 30 keywords ===
159
+ combined_keywords = list(dict.fromkeys(filtered_keywords)) # remove duplicates
160
  if len(combined_keywords) < 30:
161
  needed = 30 - len(combined_keywords)
162
+ print(f"[INFO] Need {needed} more keywords to reach 30. Using Gemini to pad.")
163
+
164
  pad_prompt = (
165
+ f"The following article content is missing SEO keyword coverage:\n\n"
166
+ f"{scraped_text}\n\n"
167
+ f"Generate exactly {needed} additional SEO keyword phrases.\n"
168
+ "Each keyword must:\n"
169
+ "- be 2 to 5 words long\n"
170
+ "- be lowercase only\n"
171
+ "- use ASCII characters only (no symbols or accents)\n"
172
+ "- be clearly relevant to the article\n"
173
+ "- avoid generic terms like 'pokemon'\n\n"
174
+ "Return only the keywords as a single comma-separated string, with no extra formatting or explanation.\n"
175
+ "Example output:\n"
176
+ "keyword one, keyword two, keyword three"
177
  )
178
+
179
+ pad_raw = get_response(
180
+ input=pad_prompt,
181
+ template=lambda x: x.strip(),
182
+ llm="gemini",
183
+ md=False,
184
+ temperature=0.7,
185
+ max_tokens=200
186
+ )
187
+
188
+ pad_keywords = []
189
+ print(pad_raw)
190
+
191
+ try:
192
+ pad_keywords = [kw.strip() for kw in pad_raw.split(",") if kw.strip()]
193
+ except Exception as e:
194
+ print(f"[WARN] Keyword parsing failed: {e}")
195
+ pad_keywords = []
196
+
197
  combined_keywords = list(dict.fromkeys(combined_keywords + pad_keywords))
198
+ print(f"[INFO] Padded {len(pad_keywords)} keywords:", pad_keywords)
199
+
200
+ return combined_keywords[:30], scraped_text
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
201
 
202
 
203
  # === Shopify publisher ===
 
213
  "body_html": html_body
214
  }
215
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
216
 
217
+ response = requests.post(url, json=data, headers=headers)
218
+ if response.status_code == 201:
219
+ return True, response.json()
220
+ else:
221
+ return False, response.text
222
 
 
223
  # === Shiny Server ===
224
  def server(input, output, session):
225
  related_keywords = reactive.Value([])
226
  generated_blog = reactive.Value(("", "")) # (title, html_content)
 
227
 
228
  @output
229
  @render.ui
230
  @reactive.event(input.blog_generate_btn)
231
+ async def blog_result_gen():
232
  url = input.blog_url()
233
  if not url:
234
  return ui.HTML("<p><strong>⚠️ Please enter a URL.</strong></p>")
235
 
236
+ keywords, scraped = await get_keywords_and_content(url)
 
 
 
237
  related_keywords.set(keywords)
238
+ keyword_str = ", ".join(keywords)
239
+
240
+ # Title generation from scraped text
241
+ infer_topic_prompt = (
242
+ f"Based on the following article content:\n\n{scraped[:2000]}\n\n"
243
+ f"Return a short, descriptive blog post title (max 70 characters)."
244
+ f"Return ONLY the TITLE"
245
+ )
246
+ seo_title = get_response(
247
+ input=infer_topic_prompt,
248
+ template=lambda x: x.strip().replace('"', ''),
249
+ llm="gemini",
250
+ md=False,
251
+ temperature=0.5,
252
+ max_tokens=20
253
+ )
254
+
255
+ # Blog generation with injected SEO
256
+ prompt = (
257
+ f"You are a content writer for a collectibles brand called 'Ultima Supply'.\n"
258
+ f"Given the following scraped content:\n\n{scraped}\n\n"
259
+ f"Rewrite this in an engaging, original, and heavily detailed SEO-optimized blog post.\n"
260
+ f"Naturally and organically integrate the following SEO keywords throughout the content:\n{keyword_str}\n\n"
261
+ f"⚠️ STRICT FORMATTING RULES (must be followed exactly):\n"
262
+ f"- Use <h1> for the blog title\n"
263
+ f"- Use <h2> for section headers\n"
264
+ f"- Use <p> for all paragraphs\n"
265
+ f"- NO Markdown, NO triple backticks, NO code blocks, NO formatting fences\n"
266
+ f"- DO NOT include any hyperlinks, URLs, web addresses, or references to any external sites or brands — no exceptions\n"
267
+ f"- DO NOT include any <a> tags except for the final line below\n\n"
268
+ f"✅ FINAL LINE ONLY:\n"
269
+ f"Add this exact call-to-action at the very end of the post inside its own <p> tag:\n"
270
+ f"Visit <a href='https://ultima-supply.myshopify.com'>Ultima Supply</a> to explore more collectibles."
271
+ )
272
 
273
+ blog_html = get_response(
274
+ input=prompt,
275
+ template=lambda x: x.strip(),
276
+ llm="gemini",
277
+ md=False,
278
+ temperature=0.9,
279
+ max_tokens=5000
280
+ )
281
+
282
+ blog_html = re.sub(r"```[a-zA-Z]*\n?", "", blog_html).strip()
283
+ blog_html = blog_html.replace("```", "").strip()
284
+
285
+ generated_blog.set((seo_title, blog_html))
286
 
287
  return ui.HTML(
288
+ f"<p><strong>✅ Blog generated with title:</strong> {seo_title}</p>"
289
  f"<p>Click 'Post to Shopify' to publish.</p>{blog_html}"
 
290
  )
 
 
 
 
 
291
 
292
  @output
293
  @render.ui
294
+ def keywords_used_gen():
295
  kws = related_keywords()
296
  if not kws:
297
  return ui.HTML("<p><strong>No SEO keywords retrieved yet.</strong></p>")
298
+
299
  return ui.HTML(
300
+ f"<p><strong>✅ SEO Keywords Injected ({len(kws)}):</strong></p><ul>"
301
+ + "".join(f"<li>{kw}</li>" for kw in kws) +
302
  "</ul>"
303
  )
304
 
305
  @reactive.effect
306
  @reactive.event(input.blog_post_btn)
307
  def post_to_shopify():
308
+ seo_title, html = generated_blog()
309
+
310
  if not html:
311
  ui.notification_show("⚠️ No blog generated yet.", type="warning")
312
  return
313
+
314
+ success, response = publish_blog_post(title=seo_title, html_body=html)
315
+
316
  if success:
317
  ui.notification_show("✅ Blog posted to Shopify successfully!", type="message")
318
  else:
319
  ui.notification_show(f"❌ Failed to publish: {response}", type="error")
320
 
 
 
 
 
 
 
 
 
 
 
ui/general_blog.py CHANGED
@@ -15,6 +15,5 @@ ui = ui.nav_panel(
15
 
16
  # Scoped outputs
17
  ui.output_ui("blog_result_gen"),
18
- ui.output_ui("blog_keywords_used_gen"),
19
- ui.output_text('tweet_post_status_gen_blog')
20
  )
 
15
 
16
  # Scoped outputs
17
  ui.output_ui("blog_result_gen"),
18
+ ui.output_ui("blog_keywords_used_gen")
 
19
  )