Pamudu13 commited on
Commit
53e65b7
·
verified ·
1 Parent(s): 66d6743

Upload 16 files

Browse files
ZZ_auto_blogger_workflow.json ADDED
@@ -0,0 +1,1244 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "name": "ZZ_auto_blogger_workflow",
3
+ "nodes": [
4
+ {
5
+ "parameters": {
6
+ "documentId": {
7
+ "__rl": true,
8
+ "value": "17gUjLgjmJpXHcwsYPkZRG3salzsFLw3ZC3l4GT9liEc",
9
+ "mode": "list",
10
+ "cachedResultName": "Cluster Template",
11
+ "cachedResultUrl": "https://docs.google.com/spreadsheets/d/17gUjLgjmJpXHcwsYPkZRG3salzsFLw3ZC3l4GT9liEc/edit?usp=drivesdk"
12
+ },
13
+ "sheetName": {
14
+ "__rl": true,
15
+ "value": 1281006823,
16
+ "mode": "list",
17
+ "cachedResultName": "Pillar #1",
18
+ "cachedResultUrl": "https://docs.google.com/spreadsheets/d/17gUjLgjmJpXHcwsYPkZRG3salzsFLw3ZC3l4GT9liEc/edit#gid=1281006823"
19
+ },
20
+ "filtersUI": {
21
+ "values": [
22
+ {
23
+ "lookupColumn": "Status",
24
+ "lookupValue": "no"
25
+ }
26
+ ]
27
+ },
28
+ "options": {
29
+ "returnFirstMatch": true
30
+ }
31
+ },
32
+ "type": "n8n-nodes-base.googleSheets",
33
+ "typeVersion": 4.5,
34
+ "position": [
35
+ 320,
36
+ 500
37
+ ],
38
+ "id": "7d5168e8-627c-4a8e-b92f-525060bbd7b8",
39
+ "name": "Grab New Cluster",
40
+ "credentials": {
41
+ "googleSheetsOAuth2Api": {
42
+ "id": "hYWAsZKxwadxEltt",
43
+ "name": "Google Sheets account 2"
44
+ }
45
+ }
46
+ },
47
+ {
48
+ "parameters": {
49
+ "assignments": {
50
+ "assignments": [
51
+ {
52
+ "id": "23b8e8c4-9191-415a-9661-1b60d413528a",
53
+ "name": "research",
54
+ "value": "={{ $json.choices[0].message.content.replaceAll(\"[1]\", \" - source: \" +$json.citations[0]).replaceAll(\"[2]\",\" source:\" +$json.citations[1]).replaceAll(\"[3]\",\" - source: \" +$json.citations[2]).replaceAll(\"[4]\",\" - source: \"+$json.citations[3]).replaceAll(\"[5]\",\" - source: \"+$json.citations[4]).replaceAll(\"[6]\",\" - source: \"+$json.citations[5]).replaceAll(\"[7]\",\" - source: \"+$json.citations[6]).replaceAll(\"[8]\",\" - source: \"+$json.citations[7]).replaceAll(\"[9]\",\" - source: \"+$json.citations[8]).replaceAll(\"[10]\",\" - source: \"+$json.citations[9]) }}",
55
+ "type": "string"
56
+ }
57
+ ]
58
+ },
59
+ "options": {}
60
+ },
61
+ "type": "n8n-nodes-base.set",
62
+ "typeVersion": 3.4,
63
+ "position": [
64
+ 1060,
65
+ 500
66
+ ],
67
+ "id": "8b7b68a5-7c40-408e-ad25-9c849a4e1322",
68
+ "name": "Fix Links"
69
+ },
70
+ {
71
+ "parameters": {
72
+ "modelId": {
73
+ "__rl": true,
74
+ "value": "o1-preview",
75
+ "mode": "list",
76
+ "cachedResultName": "O1-PREVIEW"
77
+ },
78
+ "messages": {
79
+ "values": [
80
+ {
81
+ "content": "=You are part of a team that creates world class blog posts. \n\nFor 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: \n\n- Keywords: These are the keywordswhich the blog post is meant to rank for on SEO. They should be scattered throughout the blog post intelligently to help with SEO. \n\n- 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. \n\n- 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. \n\n- Preliminary plan: Very basic plan set out by your colleague to kick off the blog post. \n\n- 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. \n\n\nGiven the said info, you must create a detailed plan for the blog post. \n \nYour output must: \n\n- Include a plan for the blog post.\n\n- Be in dot point format. \n\n- In each part of the blog post, you must mention which keywords should be placed. \n\nHere are the other things you must consider: \n\n- 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. \n\n- 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. \n\n- You must ensure that the plan created satisfies the search intent and revolves directly around the given keywords. \n\n- Your plan must be very detailed. \n\n- 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. So your plan should include all technical detail regarding each point to be in the blog post. For example instead of saying \"define X\", you must have \"define X as ...\". \n\n- The plan you create must have a flow that makes sense. \n\n- You must ensure the bog post will be highly detailed and satisfy the most important concepts regarding the topic. \n\nA new project has just came across your desk with below details:\n\nKeywords: \n{{ $('Grab New Cluster').item.json.Keywords }} \n\nSearch intent: \n{{ $('Grab New Cluster').item.json.Intent }}\n\nPreliminary plan: \n{{ $('AI Agent').item.json.output }}\n\nResearch findings: \n{{ $json.research }}\n\nPrimary keyword: \n{{ $('Grab New Cluster').item.json['Primary Keyword'] }}\n\nCreate the detailed plan. \n\nYour output must only be the plan and nothing else. \n"
82
+ }
83
+ ]
84
+ },
85
+ "options": {}
86
+ },
87
+ "type": "@n8n/n8n-nodes-langchain.openAi",
88
+ "typeVersion": 1.8,
89
+ "position": [
90
+ 1260,
91
+ 340
92
+ ],
93
+ "id": "f27b622a-2b34-45cb-bcdb-1b39b9582353",
94
+ "name": "Create plan",
95
+ "credentials": {
96
+ "openAiApi": {
97
+ "id": "EF5N3ckj25m5tqQ1",
98
+ "name": "OpenAi account"
99
+ }
100
+ }
101
+ },
102
+ {
103
+ "parameters": {
104
+ "modelId": {
105
+ "__rl": true,
106
+ "value": "anthropic/claude-3.5-sonnet:beta",
107
+ "mode": "id"
108
+ },
109
+ "messages": {
110
+ "values": [
111
+ {
112
+ "content": "=You are part of a team that creates world class blog posts. \n\nYou are the teams best copywriter and are responsible for writing out the actual blog post. \n\nFor each new blog post project you are provided with a detailed plan and research findings. \n\nYour job is to create the blog post by closely following the detailed plan. \n\nThe blog post you create must: \n\n- Follow the plan bit by bit. \n\n- Use short paragraphs. \n\n- Use bullet points and subheadings with keywords where appropriate. \n\n- Not have any fluff. The content of the blog must be value dense and direct. \n\n- Be very detailed. \n\n- Include the keywords mentioned in each section within that section. \n\n- Use the research as advised by the plan. Make sure to include the link associated with each point you extract from the research at the end of that section in the form of a URL. \n\n- Place the primary keyword in the blog title, H1 header and early in the introduction. \n\n- Place one keyword for each section in the heading of that section. \n\n- When possible pepper synonyms of the keywords throughout each section. \n\n- When possible use Latent Semantic Indexing (LSI) keywords and related terms to enhance context (e.g., “robotic process automation” for RPA). \n\n- Be at minimum 2000 to 2500 words long. \n\n- Be suitable for a year 5 reading level. \n\nMake sure to create the entire blog post draft in your first output. Don't stop or cut it short. \n\nHere are the details of your next blog post project: \n\nDetailed Plan: \n{{ $json.message.content }}\n\nDetailed Research: \n{{ $('Fix Links').item.json.research }}\n\n\nWrite the blog post."
113
+ }
114
+ ]
115
+ },
116
+ "options": {}
117
+ },
118
+ "type": "@n8n/n8n-nodes-langchain.openAi",
119
+ "typeVersion": 1.8,
120
+ "position": [
121
+ 1560,
122
+ 340
123
+ ],
124
+ "id": "9d123969-dd00-4e3c-936d-f878fcb4391a",
125
+ "name": "Write Blog"
126
+ },
127
+ {
128
+ "parameters": {
129
+ "aggregate": "aggregateAllItemData",
130
+ "destinationFieldName": "previous-posts",
131
+ "options": {}
132
+ },
133
+ "type": "n8n-nodes-base.aggregate",
134
+ "typeVersion": 1,
135
+ "position": [
136
+ 2040,
137
+ 560
138
+ ],
139
+ "id": "64e6c0ac-1b87-4084-9b38-ff97353e9097",
140
+ "name": "Aggregate"
141
+ },
142
+ {
143
+ "parameters": {
144
+ "documentId": {
145
+ "__rl": true,
146
+ "value": "1CL0L4V288SEygm0BieMRM8t8h7MbcV9bYyzkDc0zInU",
147
+ "mode": "list",
148
+ "cachedResultName": "Completed Keywords - Template",
149
+ "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1CL0L4V288SEygm0BieMRM8t8h7MbcV9bYyzkDc0zInU/edit?usp=drivesdk"
150
+ },
151
+ "sheetName": {
152
+ "__rl": true,
153
+ "value": 2094827997,
154
+ "mode": "list",
155
+ "cachedResultName": "Sheet1",
156
+ "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1CL0L4V288SEygm0BieMRM8t8h7MbcV9bYyzkDc0zInU/edit#gid=2094827997"
157
+ },
158
+ "options": {}
159
+ },
160
+ "type": "n8n-nodes-base.googleSheets",
161
+ "typeVersion": 4.5,
162
+ "position": [
163
+ 1880,
164
+ 560
165
+ ],
166
+ "id": "d7d5d589-fe19-4000-a485-8e9aaa9b9bf2",
167
+ "name": "Previous Posts",
168
+ "credentials": {
169
+ "googleSheetsOAuth2Api": {
170
+ "id": "hYWAsZKxwadxEltt",
171
+ "name": "Google Sheets account 2"
172
+ }
173
+ }
174
+ },
175
+ {
176
+ "parameters": {
177
+ "modelId": {
178
+ "__rl": true,
179
+ "value": "o1-mini-2024-09-12",
180
+ "mode": "list",
181
+ "cachedResultName": "O1-MINI-2024-09-12"
182
+ },
183
+ "messages": {
184
+ "values": [
185
+ {
186
+ "content": "=You are part of a team that creates world class blog posts. \n\nYou are in charge of internal linking between blog posts. \n\nFor each new blog post that comes across your desk, your job is to look through previously posted blogs and make atleast 5 internal links. \n\nTo choose the best internal linking opportunities you must: \n\n- Read the previous blog post summaries and look through their keywords. If there is a match where the previous blog post is highly relevant, then this is an internal linking opportunity. \n\n- Do not link if it is not highly relevant. Only make a link if it makes sense and adds value for the reader. \n\nOnce you've found the best linking opportunities, you must update the blog post with the internal links. To do this you must: \n\n- Add the link of the previous blog post at the relevant section of the new blog post. Drop the URL at the place which makes most sense. Later we will hyperlink the URL to the word in the blog post which it is placed next to. So your placing is very important. \n\nMake sure to not delete any existing URLs or change anything about the blog post provided to you. You must only add new internal linking URLs and output the revised blog post. \n\nYour output must be the blog post given to you plus the new urls. Don't remove any info. \n\nDon't return the previous blog posts. Only return the current blog post with the internal links added.\n\nCurrent blog Post: \n{{ $('write blog').item.json.output }}\n\nPrevious Blog Posts: \n{{ $json['previous-posts'].toJsonString().split() }}\n\n"
187
+ }
188
+ ]
189
+ },
190
+ "options": {}
191
+ },
192
+ "type": "@n8n/n8n-nodes-langchain.openAi",
193
+ "typeVersion": 1.8,
194
+ "position": [
195
+ 2160,
196
+ 400
197
+ ],
198
+ "id": "7db6c59e-8fc9-4220-88ae-b991ea6f0ad3",
199
+ "name": "Add internal links",
200
+ "credentials": {
201
+ "openAiApi": {
202
+ "id": "EF5N3ckj25m5tqQ1",
203
+ "name": "OpenAi account"
204
+ }
205
+ }
206
+ },
207
+ {
208
+ "parameters": {
209
+ "modelId": {
210
+ "__rl": true,
211
+ "value": "gpt-4o-mini",
212
+ "mode": "list",
213
+ "cachedResultName": "GPT-4O-MINI"
214
+ },
215
+ "messages": {
216
+ "values": [
217
+ {
218
+ "content": "=Create a slug for the following blog post: \n{{ $('Add internal links').item.json.message.content }}\n\nA slug in a blog post is the part of the URL that comes after the domain name and identifies a specific page. It is typically a short, descriptive phrase that summarizes the content of the post, making it easier for users and search engines to understand what the page is about. For example, in the URL www.example.com/intelligent-agents, the slug is intelligent-agents. A good slug is concise, contains relevant keywords, and avoids unnecessary words to improve readability and SEO. \n\nThe slug must be 4 or 5 words max and must include the primary keyword of the blog post which is {{ $('Grab New Cluster').last().json['Primary Keyword'] }}.\n\nYour output must be the slug and nothing else so that I can copy and paste your output and put it at the end of my blog post URL to post it right away. "
219
+ }
220
+ ]
221
+ },
222
+ "options": {}
223
+ },
224
+ "type": "@n8n/n8n-nodes-langchain.openAi",
225
+ "typeVersion": 1.8,
226
+ "position": [
227
+ 2820,
228
+ 560
229
+ ],
230
+ "id": "5defb2d1-ed32-4dc7-a8cc-a0281589421f",
231
+ "name": "Slug",
232
+ "credentials": {
233
+ "openAiApi": {
234
+ "id": "EF5N3ckj25m5tqQ1",
235
+ "name": "OpenAi account"
236
+ }
237
+ }
238
+ },
239
+ {
240
+ "parameters": {
241
+ "modelId": {
242
+ "__rl": true,
243
+ "value": "gpt-4o-2024-11-20",
244
+ "mode": "list",
245
+ "cachedResultName": "GPT-4O-2024-11-20"
246
+ },
247
+ "messages": {
248
+ "values": [
249
+ {
250
+ "content": "=Extract the blog post title from the following blog post: \n{{ $('Add internal links').item.json.message.content }}\n\n\n\nThe blog post title must include the primary keyword {{ $('Grab New Cluster').last().json['Primary Keyword'] }} and must inform the users right away of what they can expect from reading the blog post. \n\n- Don't put the output in \"\". The output should just text with no markdown or formatting. \n\nYour output must only be the blog post title and nothing else. "
251
+ }
252
+ ]
253
+ },
254
+ "options": {}
255
+ },
256
+ "type": "@n8n/n8n-nodes-langchain.openAi",
257
+ "typeVersion": 1.8,
258
+ "position": [
259
+ 3120,
260
+ 560
261
+ ],
262
+ "id": "bebffc20-05dd-4d47-b4b6-a1b2210035da",
263
+ "name": "Title",
264
+ "credentials": {
265
+ "openAiApi": {
266
+ "id": "EF5N3ckj25m5tqQ1",
267
+ "name": "OpenAi account"
268
+ }
269
+ }
270
+ },
271
+ {
272
+ "parameters": {
273
+ "modelId": {
274
+ "__rl": true,
275
+ "value": "gpt-4o-2024-11-20",
276
+ "mode": "list",
277
+ "cachedResultName": "GPT-4O-2024-11-20"
278
+ },
279
+ "messages": {
280
+ "values": [
281
+ {
282
+ "content": "=Create a proper meta description for the following blog post: \n\n{{ $('Add internal links').item.json.message.content }}\n\nA good meta description for a blog post that is SEO-optimized should:\n- Be Concise: Stick to 150-160 characters to ensure the full description displays in search results. \n- Include Keywords: Incorporate primary keywords naturally to improve visibility and relevance to search queries.\n\nPrimary keyword = {{ $('Grab New Cluster').last().json['Primary Keyword'] }}\n\nMore keywords to include if possible = [{{ $('Grab New Cluster').last().json.Keywords }}]\n\n- Provide Value: Clearly describe what the reader will learn or gain by clicking the link. \n\n- Be Engaging: Use persuasive language, such as action verbs or a question, to encourage clicks. \n\n- Align with Content: Ensure the description accurately reflects the blog post to meet user expectations and reduce bounce rates. \n\nYour output must only be the meta description and nothing else. \n"
283
+ }
284
+ ]
285
+ },
286
+ "options": {}
287
+ },
288
+ "type": "@n8n/n8n-nodes-langchain.openAi",
289
+ "typeVersion": 1.8,
290
+ "position": [
291
+ 3420,
292
+ 560
293
+ ],
294
+ "id": "b47c137a-41ff-4741-88a0-15752627cc5d",
295
+ "name": "Meta description",
296
+ "credentials": {
297
+ "openAiApi": {
298
+ "id": "EF5N3ckj25m5tqQ1",
299
+ "name": "OpenAi account"
300
+ }
301
+ }
302
+ },
303
+ {
304
+ "parameters": {
305
+ "modelId": {
306
+ "__rl": true,
307
+ "value": "o1-preview",
308
+ "mode": "list",
309
+ "cachedResultName": "O1-PREVIEW"
310
+ },
311
+ "messages": {
312
+ "values": [
313
+ {
314
+ "content": "=DO NOT OUTPUT ANYTHING OTHER THAN THE CODE. I want you to follow the layout of the template as a guide to generate the WordPress code for a blog post. Here are the rules to follow:\n\nThe blog post should have a title, estimated reading time, key takeaways, table of contents, body, and FAQ in this order.\nMake it engaging by using italics, dot points, quotes, bold, spaces, and sometimes new lines. Never use emojis.\nThe blog post will have some URLs referenced next to certain keyphrases to show where the info came from. You must hyperlink the keyphrase with the provided URL so that the reader can click on the referenced URL. It is critical you get this right.\nWrap the entire content in a container <div> with inline CSS that sets the text color to white (#ffffff), uses a legible font such as Arial, sans-serif, and sets line-height to 1.6.\nEnsure that all non-heading text elements (e.g., paragraphs, list items) have an inline style or embedded style that sets their font size to 20px and color to white (#ffffff) using !important to override theme styles.\nHyperlinks, points in the table of contents, and FAQ questions must be styled in blue (#00c2ff).\nAll headings and subheadings should have an underline implemented via a bottom border in blue (#00c2ff) with appropriate padding.\nPlace a double break (<br><br>) between each section to improve readability.\nDo not output any extra text or mention code tags like HTML''' around the output; just output the HTML code.\nBlog post: \n\n{{ $json.message.content }}\n\nHere's an example of a well formatted output:\n\n<div style=\"color: #ffffff; font-family: Arial, sans-serif; line-height: 1.6;\"> <style> p, .wp-block-paragraph, ul.wp-block-list, li { color: #ffffff !important; font-size: 20px !important; } a { color: #00c2ff !important; } </style> <h1 id=\"h-devin-ai-the-hype-and-reality-of-an-ai-software-engineer\" class=\"wp-block-heading\" style=\"border-bottom: 2px solid #00c2ff; padding-bottom: 5px;\">Devin AI: The Hype and Reality of an AI Software Engineer</h1> <br><br> <p class=\"estimated-reading-time\" style=\"color: #ffffff; font-size: 20px !important;\">Estimated reading time: 5 minutes</p> <br><br> <h2 id=\"h-key-takeaways\" class=\"wp-block-heading\" style=\"border-bottom: 2px solid #00c2ff; padding-bottom: 5px;\"><strong>Key Takeaways</strong></h2> <br><br> <ul class=\"wp-block-list\"> <li><mark style=\"background-color: #ffd966;\"><strong>Devin AI</strong></mark> claims to be the world's first fully autonomous AI software engineer.</li> <br><br> <li>Initial demos and claims have generated significant <mark style=\"background-color: #ffff00;\">hype</mark> and interest.</li> <br><br> <li>Critics argue some capabilities may be exaggerated or misleading.</li> <br><br> <li>Real-world testing reveals both <em>strengths</em> and <em>limitations</em>.</li> <br><br> <li>The true impact on software engineering remains to be seen.</li> </ul> <br><br> <div class=\"wp-block-yoast-seo-table-of-contents yoast-table-of-contents\"> <h2 style=\"color: #ffffff; border-bottom: 2px solid #00c2ff; padding-bottom: 5px;\">Table of contents</h2> <br><br> <ul> <li><a href=\"#h-devin-ai-the-hype-and-reality-of-an-ai-software-engineer\" data-level=\"1\">Devin AI: The Hype and Reality of an AI Software Engineer</a></li> <br><br> <li><a href=\"#h-key-takeaways\" data-level=\"2\">Key Takeaways</a></li> <br><br> <li><a href=\"#h-what-is-devin-ai\" data-level=\"2\">What is Devin AI?</a></li> <br><br> <li><a href=\"#h-the-hype-around-devin-ai\" data-level=\"2\">The Hype Around Devin AI</a></li> <br><br> <li><a href=\"#h-putting-devin-to-the-test\" data-level=\"2\">Putting Devin to the Test</a></li> <br><br> <li><a href=\"#h-the-reality-check\" data-level=\"2\">The Reality Check</a></li> <br><br> <li><a href=\"#h-the-future-of-ai-in-software-development\" data-level=\"2\">The Future of AI in Software Development</a></li> <br><br> <li><a href=\"#h-frequently-asked-questions\" data-level=\"2\">Frequently Asked Questions</a></li> </ul> </div> <br><br> <p>Devin AI has burst onto the tech scene, promising to revolutionize software development as we know it. But does this AI-powered coding assistant live up to the hype? Let's dive into what Devin AI really is, what it can do, and what developers are saying after putting it to the test.</p> <br><br> <!-- Rest of blog post content goes here --> </div>\n"
315
+ }
316
+ ]
317
+ },
318
+ "options": {}
319
+ },
320
+ "type": "@n8n/n8n-nodes-langchain.openAi",
321
+ "typeVersion": 1.8,
322
+ "position": [
323
+ 2500,
324
+ 400
325
+ ],
326
+ "id": "23500c58-54c0-4ded-bae8-4819e8af1cde",
327
+ "name": "HTML version",
328
+ "credentials": {
329
+ "openAiApi": {
330
+ "id": "EF5N3ckj25m5tqQ1",
331
+ "name": "OpenAi account"
332
+ }
333
+ }
334
+ },
335
+ {
336
+ "parameters": {
337
+ "url": "https://serpapi.com/search?engine=google_images",
338
+ "authentication": "genericCredentialType",
339
+ "genericAuthType": "httpQueryAuth",
340
+ "sendQuery": true,
341
+ "queryParameters": {
342
+ "parameters": [
343
+ {
344
+ "name": "q",
345
+ "value": "={{ $('Title').item.json.message.content }}"
346
+ },
347
+ {
348
+ "name": "gl",
349
+ "value": "us"
350
+ }
351
+ ]
352
+ },
353
+ "options": {}
354
+ },
355
+ "type": "n8n-nodes-base.httpRequest",
356
+ "typeVersion": 4.2,
357
+ "position": [
358
+ 3740,
359
+ 560
360
+ ],
361
+ "id": "50308aaf-b6e3-4e5e-9e9d-d1fa7d2e1cb3",
362
+ "name": "Image Covers",
363
+ "credentials": {
364
+ "httpQueryAuth": {
365
+ "id": "1NakbreMhEpbJspl",
366
+ "name": "SerpAPI"
367
+ }
368
+ }
369
+ },
370
+ {
371
+ "parameters": {
372
+ "assignments": {
373
+ "assignments": [
374
+ {
375
+ "id": "1f0541df-05ab-4e3d-a5d8-3904579fc8a9",
376
+ "name": "image-url",
377
+ "value": "={{ $json.images_results[1].original }}",
378
+ "type": "string"
379
+ }
380
+ ]
381
+ },
382
+ "options": {}
383
+ },
384
+ "type": "n8n-nodes-base.set",
385
+ "typeVersion": 3.4,
386
+ "position": [
387
+ 3900,
388
+ 560
389
+ ],
390
+ "id": "b6f5e04b-841b-4ade-94f6-8213a2370939",
391
+ "name": "Edit Fields"
392
+ },
393
+ {
394
+ "parameters": {
395
+ "operation": "appendOrUpdate",
396
+ "documentId": {
397
+ "__rl": true,
398
+ "value": "17gUjLgjmJpXHcwsYPkZRG3salzsFLw3ZC3l4GT9liEc",
399
+ "mode": "list",
400
+ "cachedResultName": "Cluster Template",
401
+ "cachedResultUrl": "https://docs.google.com/spreadsheets/d/17gUjLgjmJpXHcwsYPkZRG3salzsFLw3ZC3l4GT9liEc/edit?usp=drivesdk"
402
+ },
403
+ "sheetName": {
404
+ "__rl": true,
405
+ "value": 1281006823,
406
+ "mode": "list",
407
+ "cachedResultName": "Pillar #1",
408
+ "cachedResultUrl": "https://docs.google.com/spreadsheets/d/17gUjLgjmJpXHcwsYPkZRG3salzsFLw3ZC3l4GT9liEc/edit#gid=1281006823"
409
+ },
410
+ "columns": {
411
+ "mappingMode": "defineBelow",
412
+ "value": {},
413
+ "matchingColumns": [
414
+ "Status"
415
+ ],
416
+ "schema": [
417
+ {
418
+ "id": "Cluster",
419
+ "displayName": "Cluster",
420
+ "required": false,
421
+ "defaultMatch": false,
422
+ "display": true,
423
+ "type": "string",
424
+ "canBeUsedToMatch": true
425
+ },
426
+ {
427
+ "id": "Intent",
428
+ "displayName": "Intent",
429
+ "required": false,
430
+ "defaultMatch": false,
431
+ "display": true,
432
+ "type": "string",
433
+ "canBeUsedToMatch": true
434
+ },
435
+ {
436
+ "id": "Keywords",
437
+ "displayName": "Keywords",
438
+ "required": false,
439
+ "defaultMatch": false,
440
+ "display": true,
441
+ "type": "string",
442
+ "canBeUsedToMatch": true
443
+ },
444
+ {
445
+ "id": "Primary Keyword",
446
+ "displayName": "Primary Keyword",
447
+ "required": false,
448
+ "defaultMatch": false,
449
+ "display": true,
450
+ "type": "string",
451
+ "canBeUsedToMatch": true
452
+ },
453
+ {
454
+ "id": "Status",
455
+ "displayName": "Status",
456
+ "required": false,
457
+ "defaultMatch": false,
458
+ "display": true,
459
+ "type": "string",
460
+ "canBeUsedToMatch": true,
461
+ "removed": false
462
+ },
463
+ {
464
+ "id": "row_number",
465
+ "displayName": "row_number",
466
+ "required": false,
467
+ "defaultMatch": false,
468
+ "display": true,
469
+ "type": "string",
470
+ "canBeUsedToMatch": true,
471
+ "readOnly": true,
472
+ "removed": true
473
+ }
474
+ ],
475
+ "attemptToConvertTypes": false,
476
+ "convertFieldsToString": false
477
+ },
478
+ "options": {}
479
+ },
480
+ "type": "n8n-nodes-base.googleSheets",
481
+ "typeVersion": 4.5,
482
+ "position": [
483
+ 4380,
484
+ 560
485
+ ],
486
+ "id": "7caa939e-eca3-4a03-8673-383fab605b6f",
487
+ "name": "Check as completed on Sheets",
488
+ "credentials": {
489
+ "googleSheetsOAuth2Api": {
490
+ "id": "hYWAsZKxwadxEltt",
491
+ "name": "Google Sheets account 2"
492
+ }
493
+ }
494
+ },
495
+ {
496
+ "parameters": {
497
+ "operation": "append",
498
+ "documentId": {
499
+ "__rl": true,
500
+ "value": "1t-J2x22lsG6TpktQLBkwaS-ewuJI2WQvB-g4vQspbYU",
501
+ "mode": "list",
502
+ "cachedResultName": "Completed Keywords",
503
+ "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1t-J2x22lsG6TpktQLBkwaS-ewuJI2WQvB-g4vQspbYU/edit?usp=drivesdk"
504
+ },
505
+ "sheetName": {
506
+ "__rl": true,
507
+ "value": "gid=0",
508
+ "mode": "list",
509
+ "cachedResultName": "Sheet1",
510
+ "cachedResultUrl": "https://docs.google.com/spreadsheets/d/1t-J2x22lsG6TpktQLBkwaS-ewuJI2WQvB-g4vQspbYU/edit#gid=0"
511
+ },
512
+ "columns": {
513
+ "mappingMode": "defineBelow",
514
+ "value": {
515
+ "Blog Title ": "={{ $('Title').item.json.message.content }}",
516
+ "Keywords": "={{ $('Grab New Cluster').last().json.Keywords }}",
517
+ "Summary ": "={{ $('Meta description').item.json.message.content }}",
518
+ "URL": "=https://YourURL/{{ $('Slug').item.json.message.content }}"
519
+ },
520
+ "matchingColumns": [],
521
+ "schema": [
522
+ {
523
+ "id": "Blog Title ",
524
+ "displayName": "Blog Title ",
525
+ "required": false,
526
+ "defaultMatch": false,
527
+ "display": true,
528
+ "type": "string",
529
+ "canBeUsedToMatch": true
530
+ },
531
+ {
532
+ "id": "Keywords",
533
+ "displayName": "Keywords",
534
+ "required": false,
535
+ "defaultMatch": false,
536
+ "display": true,
537
+ "type": "string",
538
+ "canBeUsedToMatch": true
539
+ },
540
+ {
541
+ "id": "Summary ",
542
+ "displayName": "Summary ",
543
+ "required": false,
544
+ "defaultMatch": false,
545
+ "display": true,
546
+ "type": "string",
547
+ "canBeUsedToMatch": true
548
+ },
549
+ {
550
+ "id": "URL",
551
+ "displayName": "URL",
552
+ "required": false,
553
+ "defaultMatch": false,
554
+ "display": true,
555
+ "type": "string",
556
+ "canBeUsedToMatch": true
557
+ }
558
+ ],
559
+ "attemptToConvertTypes": false,
560
+ "convertFieldsToString": false
561
+ },
562
+ "options": {}
563
+ },
564
+ "type": "n8n-nodes-base.googleSheets",
565
+ "typeVersion": 4.5,
566
+ "position": [
567
+ 4580,
568
+ 560
569
+ ],
570
+ "id": "e64ef24c-f842-4349-a021-b225a523013d",
571
+ "name": "Google Sheets"
572
+ },
573
+ {
574
+ "parameters": {
575
+ "content": "## Schedule a trigger\n \n",
576
+ "height": 240,
577
+ "width": 260
578
+ },
579
+ "type": "n8n-nodes-base.stickyNote",
580
+ "position": [
581
+ 20,
582
+ 400
583
+ ],
584
+ "typeVersion": 1,
585
+ "id": "4a45ccd8-70ef-4637-9e59-e2377d098956",
586
+ "name": "Sticky Note"
587
+ },
588
+ {
589
+ "parameters": {
590
+ "content": "## New Post Info\n \n",
591
+ "height": 240
592
+ },
593
+ "type": "n8n-nodes-base.stickyNote",
594
+ "position": [
595
+ 280,
596
+ 400
597
+ ],
598
+ "typeVersion": 1,
599
+ "id": "d6d3858c-05ef-40f1-ab03-e1d7cad7222a",
600
+ "name": "Sticky Note1"
601
+ },
602
+ {
603
+ "parameters": {
604
+ "content": "## Preliminary Post Plan \n \n",
605
+ "height": 680,
606
+ "width": 300
607
+ },
608
+ "type": "n8n-nodes-base.stickyNote",
609
+ "position": [
610
+ 520,
611
+ 400
612
+ ],
613
+ "typeVersion": 1,
614
+ "id": "effc3813-a919-4a5b-816e-bb669a296276",
615
+ "name": "Sticky Note2"
616
+ },
617
+ {
618
+ "parameters": {
619
+ "content": "## Research\n \n \n",
620
+ "height": 240,
621
+ "width": 180
622
+ },
623
+ "type": "n8n-nodes-base.stickyNote",
624
+ "position": [
625
+ 820,
626
+ 400
627
+ ],
628
+ "typeVersion": 1,
629
+ "id": "43e9adff-1cfd-41a6-ab0f-e4063759259d",
630
+ "name": "Sticky Note3"
631
+ },
632
+ {
633
+ "parameters": {
634
+ "content": "## Post Plan \n \n",
635
+ "height": 580,
636
+ "width": 300
637
+ },
638
+ "type": "n8n-nodes-base.stickyNote",
639
+ "position": [
640
+ 1240,
641
+ 260
642
+ ],
643
+ "typeVersion": 1,
644
+ "id": "ce402266-f43d-41ad-a7bc-b249ce1753f2",
645
+ "name": "Sticky Note4"
646
+ },
647
+ {
648
+ "parameters": {
649
+ "content": "## Writing Blog\n \n \n",
650
+ "height": 580,
651
+ "width": 300
652
+ },
653
+ "type": "n8n-nodes-base.stickyNote",
654
+ "position": [
655
+ 1540,
656
+ 260
657
+ ],
658
+ "typeVersion": 1,
659
+ "id": "b0e13017-ca7d-4f1a-8fac-c0f0857f1bd1",
660
+ "name": "Sticky Note5"
661
+ },
662
+ {
663
+ "parameters": {
664
+ "method": "POST",
665
+ "url": "https://api.perplexity.ai/chat/completions",
666
+ "authentication": "genericCredentialType",
667
+ "genericAuthType": "httpHeaderAuth",
668
+ "sendBody": true,
669
+ "specifyBody": "json",
670
+ "jsonBody": "={\n \"model\": \"sonar-pro\",\n \"messages\": [\n {\n \"role\": \"system\",\n \"content\": \"Act as a professional news researcher who is capable of finding detailed summaries about a news topic from highly reputable sources.\"\n },\n {\n \"role\": \"user\",\n \"content\": \" You are helping research for a blog article. Please provide in depth research for each of the titles of the following blog plan:{{ $json.output.split(/\\n+/).join(' ')}}\"\n }\n ]\n}",
671
+ "options": {}
672
+ },
673
+ "type": "n8n-nodes-base.httpRequest",
674
+ "typeVersion": 4.2,
675
+ "position": [
676
+ 860,
677
+ 500
678
+ ],
679
+ "id": "d6256dfe-516e-441d-b426-beaa68cd0796",
680
+ "name": "Research",
681
+ "credentials": {
682
+ "httpHeaderAuth": {
683
+ "id": "oEBlGZGtMD64v00n",
684
+ "name": "Perplexity Auth"
685
+ }
686
+ }
687
+ },
688
+ {
689
+ "parameters": {
690
+ "content": "## Internal Links\n \n \n",
691
+ "height": 240,
692
+ "width": 300
693
+ },
694
+ "type": "n8n-nodes-base.stickyNote",
695
+ "position": [
696
+ 2140,
697
+ 320
698
+ ],
699
+ "typeVersion": 1,
700
+ "id": "a658fcbd-cd00-4734-a456-2ec888238a8e",
701
+ "name": "Sticky Note6"
702
+ },
703
+ {
704
+ "parameters": {
705
+ "content": "## HTML\n\n \n \n",
706
+ "height": 240,
707
+ "width": 340
708
+ },
709
+ "type": "n8n-nodes-base.stickyNote",
710
+ "position": [
711
+ 2440,
712
+ 320
713
+ ],
714
+ "typeVersion": 1,
715
+ "id": "55806213-1d1b-4d82-96df-a8be158f63b6",
716
+ "name": "Sticky Note7"
717
+ },
718
+ {
719
+ "parameters": {
720
+ "content": "## Slug\n\n\n \n \n",
721
+ "height": 240,
722
+ "width": 320
723
+ },
724
+ "type": "n8n-nodes-base.stickyNote",
725
+ "position": [
726
+ 2780,
727
+ 480
728
+ ],
729
+ "typeVersion": 1,
730
+ "id": "8c5f22e8-b48c-41c9-a153-69d6a39d6d23",
731
+ "name": "Sticky Note8"
732
+ },
733
+ {
734
+ "parameters": {
735
+ "content": "## Post Title\n\n\n \n \n",
736
+ "height": 240,
737
+ "width": 300
738
+ },
739
+ "type": "n8n-nodes-base.stickyNote",
740
+ "position": [
741
+ 3100,
742
+ 480
743
+ ],
744
+ "typeVersion": 1,
745
+ "id": "01441f93-8e77-42c8-9e29-3d65963bf6e5",
746
+ "name": "Sticky Note9"
747
+ },
748
+ {
749
+ "parameters": {
750
+ "content": "## Meta Description\n\n\n \n \n",
751
+ "height": 240,
752
+ "width": 300
753
+ },
754
+ "type": "n8n-nodes-base.stickyNote",
755
+ "position": [
756
+ 3400,
757
+ 480
758
+ ],
759
+ "typeVersion": 1,
760
+ "id": "1b422deb-65fa-4b14-a23d-aed00321c077",
761
+ "name": "Sticky Note10"
762
+ },
763
+ {
764
+ "parameters": {
765
+ "content": "## Get Image\n\n\n\n \n \n",
766
+ "height": 240,
767
+ "width": 360
768
+ },
769
+ "type": "n8n-nodes-base.stickyNote",
770
+ "position": [
771
+ 3700,
772
+ 480
773
+ ],
774
+ "typeVersion": 1,
775
+ "id": "74113fdc-bd62-4f4e-8b9e-46c7f25d431b",
776
+ "name": "Sticky Note11"
777
+ },
778
+ {
779
+ "parameters": {
780
+ "content": "## Post\n\n\n\n\n \n \n",
781
+ "height": 240,
782
+ "width": 220
783
+ },
784
+ "type": "n8n-nodes-base.stickyNote",
785
+ "position": [
786
+ 4060,
787
+ 480
788
+ ],
789
+ "typeVersion": 1,
790
+ "id": "c9ac7046-4d1c-4731-81cf-7a452b1c0d93",
791
+ "name": "Sticky Note12"
792
+ },
793
+ {
794
+ "parameters": {
795
+ "title": "={{ $('Title').item.json.message.content }}",
796
+ "additionalFields": {
797
+ "authorId": 2,
798
+ "content": "=<img src=\"{{ $json['image-url'] }}\" alt=\"Cover Image\">\n{{ $('HTML version').item.json.message.content }}",
799
+ "slug": "={{ $('Slug').item.json.message.content }}",
800
+ "status": "draft",
801
+ "commentStatus": "closed",
802
+ "sticky": false,
803
+ "categories": [
804
+ 280
805
+ ],
806
+ "tags": [
807
+ 281
808
+ ],
809
+ "postTemplate": {
810
+ "values": {}
811
+ }
812
+ }
813
+ },
814
+ "type": "n8n-nodes-base.wordpress",
815
+ "typeVersion": 1,
816
+ "position": [
817
+ 4140,
818
+ 560
819
+ ],
820
+ "id": "7c623a61-bffa-4a08-b2ed-5b01535c963b",
821
+ "name": "Wordpress",
822
+ "credentials": {
823
+ "wordpressApi": {
824
+ "id": "apQBpnJI8R7EgZZ4",
825
+ "name": "Wordpress account"
826
+ }
827
+ }
828
+ },
829
+ {
830
+ "parameters": {
831
+ "content": "## Completion\n\n\n\n\n\n \n \n",
832
+ "height": 240,
833
+ "width": 480
834
+ },
835
+ "type": "n8n-nodes-base.stickyNote",
836
+ "position": [
837
+ 4300,
838
+ 480
839
+ ],
840
+ "typeVersion": 1,
841
+ "id": "4ae2ad80-3597-4941-80ee-e5e0b2fdc313",
842
+ "name": "Sticky Note13"
843
+ },
844
+ {
845
+ "parameters": {
846
+ "content": "# CLUSTER SEO POST\n\n \n \n",
847
+ "height": 100,
848
+ "width": 500,
849
+ "color": 4
850
+ },
851
+ "type": "n8n-nodes-base.stickyNote",
852
+ "position": [
853
+ 0,
854
+ 0
855
+ ],
856
+ "typeVersion": 1,
857
+ "id": "76007e96-7941-40d5-8760-4464c189f067",
858
+ "name": "Sticky Note14"
859
+ },
860
+ {
861
+ "parameters": {},
862
+ "type": "n8n-nodes-base.manualTrigger",
863
+ "typeVersion": 1,
864
+ "position": [
865
+ 100,
866
+ 500
867
+ ],
868
+ "id": "ba79c1f5-a3e3-4af0-a7c3-6435b5160743",
869
+ "name": "When clicking ‘Test workflow’"
870
+ },
871
+ {
872
+ "parameters": {
873
+ "model": "deepseek/deepseek-r1:free",
874
+ "options": {}
875
+ },
876
+ "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
877
+ "typeVersion": 1,
878
+ "position": [
879
+ 600,
880
+ 720
881
+ ],
882
+ "id": "2f3c3ba8-04f0-4380-a7d3-b8b86026f82f",
883
+ "name": "OpenRouter Chat Model",
884
+ "credentials": {
885
+ "openRouterApi": {
886
+ "id": "vujYaSViYHNMVXm9",
887
+ "name": "OpenRouter account"
888
+ }
889
+ }
890
+ },
891
+ {
892
+ "parameters": {
893
+ "promptType": "define",
894
+ "text": "=You are part of a team that creates world class blog posts. \n\nFor each new blog post project, you are provided with a list of keywords and search intent. \n\n- Keywords: The keywords are to what the blog post is meant to rank for. They are scattered throughout the blog and define the topic of the blog post. \n\n- Search intent: The search intent recognises the intent of the user when searching up the keyword which defines be the theme of the blog post, so they click on our blog to satisfy their search. \n\n- Primary keyword: Out of the keywords, there is one keyword known as the primary keyword. The primary keyword will go in the title and first few sentences. It is important that the topic of the blog post is related to the primary keyword so that you can place it into the title and introduction naturally. \n\nGiven a list of keywords and search intent, your job is to understand the goal of th e blog post, identify the thought process behind the flow of the blog post and come up with a preliminary plan for the post. \n\nYour output must: \n\n- Recognise the discussion points of the blog post.\n\n- Be in dot point format.\n\nYou must ensure that the plan created satisfies the search intent and revolves directly around the given keywords. \n\nWhen making the plan keep in mind that all keywords must be used in the final blog post. \n\nThe final goal of the project is to create a high quality, high value, highly relevant blog post that will satisfy the users search intent and give them everything they need to know about the topic. \n\nA new project just came across your desk with below keywords and search intent:\n\nKeywords: \n{{ $json.Keywords }}\n\nSearch intent: \n{{ $json.Intent }}\n\nPrimary keyword: \n{{ $json['Primary Keyword'] }}\n\nCreate the preliminary plan.",
895
+ "options": {}
896
+ },
897
+ "type": "@n8n/n8n-nodes-langchain.agent",
898
+ "typeVersion": 1.7,
899
+ "position": [
900
+ 560,
901
+ 520
902
+ ],
903
+ "id": "e091badf-8a0d-464a-a4bb-7bce60c270cb",
904
+ "name": "AI Agent"
905
+ },
906
+ {
907
+ "parameters": {
908
+ "model": "deepseek/deepseek-r1:free",
909
+ "options": {}
910
+ },
911
+ "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
912
+ "typeVersion": 1,
913
+ "position": [
914
+ 1300,
915
+ 700
916
+ ],
917
+ "id": "a4f3a398-20d2-425c-9a87-1d2b98e72171",
918
+ "name": "OpenRouter Chat Model1",
919
+ "credentials": {
920
+ "openRouterApi": {
921
+ "id": "vujYaSViYHNMVXm9",
922
+ "name": "OpenRouter account"
923
+ }
924
+ }
925
+ },
926
+ {
927
+ "parameters": {
928
+ "promptType": "define",
929
+ "text": "=You are part of a team that creates world class blog posts. \n\nFor 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: \n\n- Keywords: These are the keywordswhich the blog post is meant to rank for on SEO. They should be scattered throughout the blog post intelligently to help with SEO. \n\n- 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. \n\n- 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. \n\n- Preliminary plan: Very basic plan set out by your colleague to kick off the blog post. \n\n- 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. \n\n\nGiven the said info, you must create a detailed plan for the blog post. \n \nYour output must: \n\n- Include a plan for the blog post.\n\n- Be in dot point format. \n\n- In each part of the blog post, you must mention which keywords should be placed. \n\nHere are the other things you must consider: \n\n- 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. \n\n- 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. \n\n- You must ensure that the plan created satisfies the search intent and revolves directly around the given keywords. \n\n- Your plan must be very detailed. \n\n- 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. So your plan should include all technical detail regarding each point to be in the blog post. For example instead of saying \"define X\", you must have \"define X as ...\". \n\n- The plan you create must have a flow that makes sense. \n\n- You must ensure the bog post will be highly detailed and satisfy the most important concepts regarding the topic. \n\nA new project has just came across your desk with below details:\n\nKeywords: \n{{ $('Grab New Cluster').item.json.Keywords }} \n\nSearch intent: \n{{ $('Grab New Cluster').item.json.Intent }}\n\nPreliminary plan: \n{{ $('AI Agent').item.json.output }}\n\nResearch findings: \n{{ $json.research }}\n\nPrimary keyword: \n{{ $('Grab New Cluster').item.json['Primary Keyword'] }}\n\nCreate the detailed plan. \n\nYour output must only be the plan and nothing else. \n",
930
+ "options": {}
931
+ },
932
+ "type": "@n8n/n8n-nodes-langchain.agent",
933
+ "typeVersion": 1.7,
934
+ "position": [
935
+ 1260,
936
+ 540
937
+ ],
938
+ "id": "e5a5cea2-3426-40f0-8d90-ddf87b7cc92a",
939
+ "name": "create plan"
940
+ },
941
+ {
942
+ "parameters": {
943
+ "model": "google/gemini-2.0-pro-exp-02-05:free",
944
+ "options": {}
945
+ },
946
+ "type": "@n8n/n8n-nodes-langchain.lmChatOpenRouter",
947
+ "typeVersion": 1,
948
+ "position": [
949
+ 1580,
950
+ 700
951
+ ],
952
+ "id": "2371f47b-8cf1-4003-988e-0d057caf9120",
953
+ "name": "OpenRouter Chat Model2",
954
+ "credentials": {
955
+ "openRouterApi": {
956
+ "id": "vujYaSViYHNMVXm9",
957
+ "name": "OpenRouter account"
958
+ }
959
+ }
960
+ },
961
+ {
962
+ "parameters": {
963
+ "promptType": "define",
964
+ "text": "=You are part of a team that creates world class blog posts. \n\nFor 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: \n\n- Keywords: These are the keywordswhich the blog post is meant to rank for on SEO. They should be scattered throughout the blog post intelligently to help with SEO. \n\n- 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. \n\n- 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. \n\n- Preliminary plan: Very basic plan set out by your colleague to kick off the blog post. \n\n- 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. \n\n\nGiven the said info, you must create a detailed plan for the blog post. \n \nYour output must: \n\n- Include a plan for the blog post.\n\n- Be in dot point format. \n\n- In each part of the blog post, you must mention which keywords should be placed. \n\nHere are the other things you must consider: \n\n- 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. \n\n- 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. \n\n- You must ensure that the plan created satisfies the search intent and revolves directly around the given keywords. \n\n- Your plan must be very detailed. \n\n- 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. So your plan should include all technical detail regarding each point to be in the blog post. For example instead of saying \"define X\", you must have \"define X as ...\". \n\n- The plan you create must have a flow that makes sense. \n\n- You must ensure the bog post will be highly detailed and satisfy the most important concepts regarding the topic. \n\nA new project has just came across your desk with below details:\n\nKeywords: \n{{ $('Grab New Cluster').item.json.Keywords }} \n\nSearch intent: \n{{ $('Grab New Cluster').item.json.Intent }}\n\nPreliminary plan: \n{{ $('AI Agent').item.json.output }}\n\nResearch findings: {{ $('Fix Links').item.json.research }}\n\nPrimary keyword: \n{{ $('Grab New Cluster').item.json['Primary Keyword'] }}\n\nCreate the detailed plan. \n\nYour output must only be the plan and nothing else. \n",
965
+ "options": {}
966
+ },
967
+ "type": "@n8n/n8n-nodes-langchain.agent",
968
+ "typeVersion": 1.7,
969
+ "position": [
970
+ 1560,
971
+ 540
972
+ ],
973
+ "id": "eb8811f8-d821-45b3-9807-472c6d7a2b00",
974
+ "name": "write blog"
975
+ }
976
+ ],
977
+ "pinData": {},
978
+ "connections": {
979
+ "Grab New Cluster": {
980
+ "main": [
981
+ [
982
+ {
983
+ "node": "AI Agent",
984
+ "type": "main",
985
+ "index": 0
986
+ }
987
+ ]
988
+ ]
989
+ },
990
+ "Fix Links": {
991
+ "main": [
992
+ [
993
+ {
994
+ "node": "create plan",
995
+ "type": "main",
996
+ "index": 0
997
+ }
998
+ ]
999
+ ]
1000
+ },
1001
+ "Create plan": {
1002
+ "main": [
1003
+ [
1004
+ {
1005
+ "node": "Write Blog",
1006
+ "type": "main",
1007
+ "index": 0
1008
+ }
1009
+ ]
1010
+ ]
1011
+ },
1012
+ "Write Blog": {
1013
+ "main": [
1014
+ []
1015
+ ]
1016
+ },
1017
+ "Previous Posts": {
1018
+ "main": [
1019
+ [
1020
+ {
1021
+ "node": "Aggregate",
1022
+ "type": "main",
1023
+ "index": 0
1024
+ }
1025
+ ]
1026
+ ]
1027
+ },
1028
+ "Aggregate": {
1029
+ "main": [
1030
+ [
1031
+ {
1032
+ "node": "Add internal links",
1033
+ "type": "main",
1034
+ "index": 0
1035
+ }
1036
+ ]
1037
+ ]
1038
+ },
1039
+ "Add internal links": {
1040
+ "main": [
1041
+ [
1042
+ {
1043
+ "node": "HTML version",
1044
+ "type": "main",
1045
+ "index": 0
1046
+ }
1047
+ ]
1048
+ ]
1049
+ },
1050
+ "Slug": {
1051
+ "main": [
1052
+ [
1053
+ {
1054
+ "node": "Title",
1055
+ "type": "main",
1056
+ "index": 0
1057
+ }
1058
+ ]
1059
+ ]
1060
+ },
1061
+ "Title": {
1062
+ "main": [
1063
+ [
1064
+ {
1065
+ "node": "Meta description",
1066
+ "type": "main",
1067
+ "index": 0
1068
+ }
1069
+ ]
1070
+ ]
1071
+ },
1072
+ "HTML version": {
1073
+ "main": [
1074
+ [
1075
+ {
1076
+ "node": "Slug",
1077
+ "type": "main",
1078
+ "index": 0
1079
+ }
1080
+ ]
1081
+ ]
1082
+ },
1083
+ "Meta description": {
1084
+ "main": [
1085
+ [
1086
+ {
1087
+ "node": "Image Covers",
1088
+ "type": "main",
1089
+ "index": 0
1090
+ }
1091
+ ]
1092
+ ]
1093
+ },
1094
+ "Image Covers": {
1095
+ "main": [
1096
+ [
1097
+ {
1098
+ "node": "Edit Fields",
1099
+ "type": "main",
1100
+ "index": 0
1101
+ }
1102
+ ]
1103
+ ]
1104
+ },
1105
+ "Edit Fields": {
1106
+ "main": [
1107
+ [
1108
+ {
1109
+ "node": "Wordpress",
1110
+ "type": "main",
1111
+ "index": 0
1112
+ }
1113
+ ]
1114
+ ]
1115
+ },
1116
+ "Check as completed on Sheets": {
1117
+ "main": [
1118
+ [
1119
+ {
1120
+ "node": "Google Sheets",
1121
+ "type": "main",
1122
+ "index": 0
1123
+ }
1124
+ ]
1125
+ ]
1126
+ },
1127
+ "Research": {
1128
+ "main": [
1129
+ [
1130
+ {
1131
+ "node": "Fix Links",
1132
+ "type": "main",
1133
+ "index": 0
1134
+ }
1135
+ ]
1136
+ ]
1137
+ },
1138
+ "Wordpress": {
1139
+ "main": [
1140
+ [
1141
+ {
1142
+ "node": "Check as completed on Sheets",
1143
+ "type": "main",
1144
+ "index": 0
1145
+ }
1146
+ ]
1147
+ ]
1148
+ },
1149
+ "When clicking ‘Test workflow’": {
1150
+ "main": [
1151
+ [
1152
+ {
1153
+ "node": "Grab New Cluster",
1154
+ "type": "main",
1155
+ "index": 0
1156
+ }
1157
+ ]
1158
+ ]
1159
+ },
1160
+ "OpenRouter Chat Model": {
1161
+ "ai_languageModel": [
1162
+ [
1163
+ {
1164
+ "node": "AI Agent",
1165
+ "type": "ai_languageModel",
1166
+ "index": 0
1167
+ }
1168
+ ]
1169
+ ]
1170
+ },
1171
+ "AI Agent": {
1172
+ "main": [
1173
+ [
1174
+ {
1175
+ "node": "Research",
1176
+ "type": "main",
1177
+ "index": 0
1178
+ }
1179
+ ]
1180
+ ]
1181
+ },
1182
+ "OpenRouter Chat Model1": {
1183
+ "ai_languageModel": [
1184
+ [
1185
+ {
1186
+ "node": "create plan",
1187
+ "type": "ai_languageModel",
1188
+ "index": 0
1189
+ }
1190
+ ]
1191
+ ]
1192
+ },
1193
+ "create plan": {
1194
+ "main": [
1195
+ [
1196
+ {
1197
+ "node": "write blog",
1198
+ "type": "main",
1199
+ "index": 0
1200
+ }
1201
+ ]
1202
+ ]
1203
+ },
1204
+ "OpenRouter Chat Model2": {
1205
+ "ai_languageModel": [
1206
+ [
1207
+ {
1208
+ "node": "write blog",
1209
+ "type": "ai_languageModel",
1210
+ "index": 0
1211
+ }
1212
+ ]
1213
+ ]
1214
+ },
1215
+ "write blog": {
1216
+ "main": [
1217
+ [
1218
+ {
1219
+ "node": "Previous Posts",
1220
+ "type": "main",
1221
+ "index": 0
1222
+ }
1223
+ ]
1224
+ ]
1225
+ }
1226
+ },
1227
+ "active": false,
1228
+ "settings": {
1229
+ "executionOrder": "v1"
1230
+ },
1231
+ "versionId": "9675df72-5405-47aa-aa5f-bd92f445cb59",
1232
+ "meta": {
1233
+ "instanceId": "56bde12b420ee12a1a73bd1e14e7f4194c37cc35508d2c95d81da646ef78f5b4"
1234
+ },
1235
+ "id": "9xOE18PxbY5Yqgo1",
1236
+ "tags": [
1237
+ {
1238
+ "createdAt": "2025-02-11T11:45:04.228Z",
1239
+ "updatedAt": "2025-02-11T11:45:04.228Z",
1240
+ "id": "1v5wgolm1yGcSkAA",
1241
+ "name": "marketing-automation"
1242
+ }
1243
+ ]
1244
+ }
app.py ADDED
@@ -0,0 +1,462 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, request, jsonify
2
+ import os
3
+ from dotenv import load_dotenv
4
+ import requests
5
+ from blog_generator import BlogGenerator
6
+ from csv_handler import CSVHandler
7
+ import ssl
8
+ import logging
9
+ from web_scraper import research_topic
10
+
11
+ # Set up logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ app = Flask(__name__)
16
+ load_dotenv()
17
+
18
+ # Configuration
19
+ OPENAI_API_KEY = os.getenv('OPENAI_API_KEY')
20
+ OPENROUTER_API_KEY = os.getenv('OPENROUTER_API_KEY')
21
+ PERPLEXITY_API_KEY = os.getenv('PERPLEXITY_API_KEY')
22
+ SERPAPI_API_KEY = os.getenv('SERPAPI_API_KEY')
23
+
24
+ ssl._create_default_https_context = ssl._create_unverified_context
25
+
26
+ def generate_preliminary_plan(cluster_data):
27
+ logger.info("Generating preliminary plan...")
28
+ try:
29
+ response = requests.post(
30
+ 'https://openrouter.ai/api/v1/chat/completions',
31
+ headers={
32
+ 'Authorization': f'Bearer {OPENROUTER_API_KEY}',
33
+ 'HTTP-Referer': 'http://127.0.0.1:5001',
34
+ 'X-Title': 'Blog Generator'
35
+ },
36
+ json={
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 and search intent.
43
+
44
+ - Keywords: The keywords are to what the blog post is meant to rank for. They are scattered throughout the blog and define the topic of the blog post.
45
+
46
+ - Search intent: The search intent recognises the intent of the user when searching up the keyword which defines be the theme of the blog post, so they click on our blog to satisfy their search.
47
+
48
+ - Primary keyword: Out of the keywords, there is one keyword known as the primary keyword. The primary keyword will go in the title and first few sentences. It is important that the topic of the blog post is related to the primary keyword so that you can place it into the title and introduction naturally.
49
+
50
+ Given a list of keywords and search intent, your job is to understand the goal of the blog post, identify the thought process behind the flow of the blog post and come up with a preliminary plan for the post.
51
+
52
+ Your output must:
53
+ - Recognise the discussion points of the blog post.
54
+ - Be in dot point format.
55
+
56
+ You must ensure that the plan created satisfies the search intent and revolves directly around the given keywords.
57
+
58
+ When making the plan keep in mind that all keywords must be used in the final blog post.
59
+
60
+ The final goal of the project is to create a high quality, high value, highly relevant blog post that will satisfy the users search intent and give them everything they need to know about the topic.
61
+
62
+ A new project just came across your desk with below keywords and search intent:
63
+
64
+ Keywords:
65
+ {cluster_data['Keywords']}
66
+
67
+ Search intent:
68
+ {cluster_data['Intent']}
69
+
70
+ Primary keyword:
71
+ {cluster_data['Primary Keyword']}
72
+
73
+ Create the preliminary plan."""
74
+ }]
75
+ },
76
+ timeout=60
77
+ )
78
+
79
+ logger.info(f"OpenRouter API Response: {response.text}")
80
+
81
+ if response.status_code != 200:
82
+ raise Exception(f"OpenRouter API error: {response.text}")
83
+
84
+ response_data = response.json()
85
+ if 'choices' not in response_data:
86
+ raise Exception(f"Unexpected API response format: {response_data}")
87
+
88
+ return response_data['choices'][0]['message']['content']
89
+ except Exception as e:
90
+ logger.error(f"Error in generate_preliminary_plan: {e}")
91
+ raise
92
+
93
+ def do_research(plan):
94
+ logger.info("Doing research...")
95
+ try:
96
+ # Extract key points from plan to create search queries
97
+ plan_lines = [line.strip('* -').strip() for line in plan.split('\n') if line.strip()]
98
+
99
+ # Take only the first 3 points
100
+ plan_lines = plan_lines[:3]
101
+ logger.info(f"Researching top 3 points: {plan_lines}")
102
+
103
+ all_research = []
104
+
105
+ # Research each main point in the plan (limited to 3 points)
106
+ for point in plan_lines:
107
+ if not point: # Skip empty lines
108
+ continue
109
+
110
+ # Get research results including both web content and AI analysis
111
+ # Using 5 sites per point for more comprehensive research
112
+ results = research_topic(point, num_sites=5)
113
+
114
+ if results['success']:
115
+ all_research.append({
116
+ 'topic': point,
117
+ 'analysis': results['analysis'],
118
+ 'sources': results['sources']
119
+ })
120
+
121
+ # Format all research into a comprehensive markdown document
122
+ formatted_research = "# Research Results\n\n"
123
+
124
+ for research in all_research:
125
+ formatted_research += f"## {research['topic']}\n\n"
126
+ formatted_research += f"{research['analysis']}\n\n"
127
+ formatted_research += "### Sources Referenced\n\n"
128
+
129
+ for source in research['sources']:
130
+ formatted_research += f"- [{source['title']}]({source['source']})\n"
131
+ if source['meta_info']['description']:
132
+ formatted_research += f" {source['meta_info']['description']}\n"
133
+
134
+ formatted_research += "\n---\n\n"
135
+
136
+ return formatted_research
137
+
138
+ except Exception as e:
139
+ logger.error(f"Error in do_research: {e}")
140
+ raise
141
+
142
+ @app.route('/generate-blog', methods=['POST'])
143
+ def generate_blog():
144
+ try:
145
+ logger.info("Starting blog generation process for multiple clusters...")
146
+
147
+ # Initialize handlers
148
+ blog_gen = BlogGenerator(OPENAI_API_KEY, OPENROUTER_API_KEY)
149
+ csv_handler = CSVHandler()
150
+
151
+ generated_blogs = []
152
+
153
+ # Get all available clusters
154
+ all_clusters = csv_handler.get_all_clusters()
155
+
156
+ for cluster_data in all_clusters:
157
+ try:
158
+ logger.info(f"Processing cluster with primary keyword: {cluster_data['Primary Keyword']}")
159
+
160
+ # 2. Generate preliminary plan
161
+ logger.info("Generating preliminary plan...")
162
+ plan = generate_preliminary_plan(cluster_data)
163
+
164
+ # 3. Do research
165
+ logger.info("Doing research...")
166
+ research = do_research(plan)
167
+
168
+ # 4. Create detailed plan
169
+ logger.info("Creating detailed plan...")
170
+ detailed_plan = blog_gen.create_detailed_plan(cluster_data, plan, research)
171
+
172
+ # 5. Write blog post
173
+ logger.info("Writing blog post...")
174
+ blog_content = blog_gen.write_blog_post(detailed_plan, cluster_data)
175
+
176
+ # 6. Add internal links
177
+ logger.info("Adding internal links...")
178
+ previous_posts = csv_handler.get_previous_posts()
179
+ blog_content = blog_gen.add_internal_links(blog_content, previous_posts)
180
+
181
+ # 7. Convert to HTML
182
+ logger.info("Converting to HTML...")
183
+ cover_image_url = blog_gen.get_cover_image(cluster_data['Primary Keyword'])
184
+ html_content = blog_gen.convert_to_html(blog_content, cover_image_url)
185
+
186
+ # 8. Generate metadata
187
+ logger.info("Generating metadata...")
188
+ metadata = blog_gen.generate_metadata(blog_content, cluster_data['Primary Keyword'], cluster_data)
189
+
190
+ # 9. Get cover image
191
+ logger.info("Getting cover image...")
192
+ cover_image_url = blog_gen.get_cover_image(metadata['title'])
193
+
194
+ # Create blog post data
195
+ blog_post_data = {
196
+ 'title': metadata['title'],
197
+ 'slug': metadata['slug'],
198
+ 'meta_description': metadata['meta_description'],
199
+ 'content': html_content,
200
+ 'cover_image': cover_image_url,
201
+ 'keywords': cluster_data['Keywords'],
202
+ 'primary_keyword': cluster_data['Primary Keyword'],
203
+ 'research': research,
204
+ 'detailed_plan': detailed_plan
205
+ }
206
+
207
+ # 10. Update tracking CSVs
208
+ logger.info("Updating tracking CSVs...")
209
+ csv_handler.mark_cluster_complete(cluster_data['row_number'])
210
+ csv_handler.log_completed_post({**metadata, 'keywords': cluster_data['Keywords']})
211
+
212
+ # Add to generated blogs array
213
+ generated_blogs.append({
214
+ 'status': 'success',
215
+ 'message': f"Blog post generated successfully for {cluster_data['Primary Keyword']}",
216
+ 'data': blog_post_data
217
+ })
218
+
219
+ except Exception as e:
220
+ logger.error(f"Error processing cluster {cluster_data['Primary Keyword']}: {e}")
221
+ generated_blogs.append({
222
+ 'status': 'error',
223
+ 'message': f"Failed to generate blog post for {cluster_data['Primary Keyword']}",
224
+ 'error': str(e)
225
+ })
226
+
227
+ logger.info("All blog generation completed!")
228
+ return jsonify({
229
+ 'status': 'success',
230
+ 'message': f'Generated {len(generated_blogs)} blog posts',
231
+ 'blogs': generated_blogs
232
+ })
233
+
234
+ except Exception as e:
235
+ logger.error(f"Error in generate_blog main process: {e}")
236
+ return jsonify({'error': str(e)}), 500
237
+
238
+ @app.route('/test-api', methods=['GET'])
239
+ def test_api():
240
+ try:
241
+ # Test OpenRouter API
242
+ response = requests.post(
243
+ 'https://openrouter.ai/api/v1/chat/completions',
244
+ headers={
245
+ 'Authorization': f'Bearer {OPENROUTER_API_KEY}',
246
+ 'HTTP-Referer': 'http://localhost:5001',
247
+ 'X-Title': 'Blog Generator'
248
+ },
249
+ json={
250
+ 'model': 'deepseek/deepseek-r1',
251
+ 'messages': [{
252
+ 'role': 'user',
253
+ 'content': 'Say hello!'
254
+ }]
255
+ }
256
+ )
257
+ return jsonify({
258
+ 'status': 'success',
259
+ 'openrouter_response': response.json()
260
+ })
261
+ except Exception as e:
262
+ return jsonify({'error': str(e)}), 500
263
+
264
+ @app.route('/generate-from-csv', methods=['POST'])
265
+ def generate_from_csv():
266
+ try:
267
+ if 'file' not in request.files:
268
+ return jsonify({'error': 'No file uploaded'}), 400
269
+
270
+ file = request.files['file']
271
+ if file.filename == '':
272
+ return jsonify({'error': 'No file selected'}), 400
273
+
274
+ # Read and decode the CSV content
275
+ csv_content = file.read().decode('utf-8')
276
+
277
+ # Initialize handlers
278
+ blog_gen = BlogGenerator(OPENAI_API_KEY, OPENROUTER_API_KEY)
279
+ csv_handler = CSVHandler()
280
+
281
+ # Process the uploaded CSV
282
+ clusters = csv_handler.process_uploaded_csv(csv_content)
283
+
284
+ if not clusters:
285
+ return jsonify({'error': 'No valid clusters found in CSV'}), 400
286
+
287
+ generated_blogs = []
288
+
289
+ # Process each cluster
290
+ for cluster_data in clusters:
291
+ try:
292
+ logger.info(f"Processing cluster with primary keyword: {cluster_data['Primary Keyword']}")
293
+
294
+ # Generate preliminary plan
295
+ plan = generate_preliminary_plan(cluster_data)
296
+
297
+ # Do research
298
+ research = do_research(plan)
299
+
300
+ # Create detailed plan
301
+ detailed_plan = blog_gen.create_detailed_plan(cluster_data, plan, research)
302
+
303
+ # Write blog post
304
+ blog_content = blog_gen.write_blog_post(detailed_plan, cluster_data)
305
+
306
+ # Add internal links
307
+ previous_posts = csv_handler.get_previous_posts()
308
+ blog_content = blog_gen.add_internal_links(blog_content, previous_posts)
309
+
310
+ # Convert to HTML
311
+ cover_image_url = blog_gen.get_cover_image(cluster_data['Primary Keyword'])
312
+ html_content = blog_gen.convert_to_html(blog_content, cover_image_url)
313
+
314
+ # Generate metadata
315
+ metadata = blog_gen.generate_metadata(blog_content, cluster_data['Primary Keyword'], cluster_data)
316
+
317
+ # Get cover image
318
+ cover_image_url = blog_gen.get_cover_image(metadata['title'])
319
+
320
+ # Create blog post data
321
+ blog_post_data = {
322
+ 'title': metadata['title'],
323
+ 'slug': metadata['slug'],
324
+ 'meta_description': metadata['meta_description'],
325
+ 'content': html_content,
326
+ 'cover_image': cover_image_url,
327
+ 'keywords': cluster_data['Keywords'],
328
+ 'primary_keyword': cluster_data['Primary Keyword'],
329
+ 'research': research,
330
+ 'detailed_plan': detailed_plan
331
+ }
332
+
333
+ generated_blogs.append({
334
+ 'status': 'success',
335
+ 'message': f"Blog post generated successfully for {cluster_data['Primary Keyword']}",
336
+ 'data': blog_post_data
337
+ })
338
+
339
+ except Exception as e:
340
+ logger.error(f"Error processing cluster {cluster_data['Primary Keyword']}: {e}")
341
+ generated_blogs.append({
342
+ 'status': 'error',
343
+ 'message': f"Failed to generate blog post for {cluster_data['Primary Keyword']}",
344
+ 'error': str(e)
345
+ })
346
+
347
+ return jsonify({
348
+ 'status': 'success',
349
+ 'message': f'Generated {len(generated_blogs)} blog posts from uploaded CSV',
350
+ 'blogs': generated_blogs
351
+ })
352
+
353
+ except Exception as e:
354
+ logger.error(f"Error in generate_from_csv: {e}")
355
+ return jsonify({'error': str(e)}), 500
356
+
357
+
358
+ @app.route('/generate-from-csv-text', methods=['POST'])
359
+ def generate_from_csv_text():
360
+ try:
361
+ logger.info("Starting blog generation process for multiple clusters...")
362
+
363
+ # Get CSV content from request JSON
364
+ data = request.get_json()
365
+ if not data or 'csv_content' not in data:
366
+ return jsonify({'error': 'No CSV content provided'}), 400
367
+
368
+ csv_content = data['csv_content']
369
+
370
+ # Initialize handlers
371
+ blog_gen = BlogGenerator(OPENAI_API_KEY, OPENROUTER_API_KEY)
372
+ csv_handler = CSVHandler()
373
+
374
+ # Process the CSV text
375
+ clusters = csv_handler.process_csv_text(csv_content)
376
+
377
+ if not clusters:
378
+ return jsonify({'error': 'No valid clusters found in CSV'}), 400
379
+
380
+ generated_blogs = []
381
+
382
+ # Process each cluster
383
+ for cluster_data in clusters:
384
+ try:
385
+ logger.info(f"Processing cluster with primary keyword: {cluster_data['Primary Keyword']}")
386
+
387
+ # Generate preliminary plan
388
+ logger.info("Generating preliminary plan...")
389
+ plan = generate_preliminary_plan(cluster_data)
390
+
391
+ # Do research
392
+ logger.info("Doing research...")
393
+ research = do_research(plan)
394
+
395
+ # Create detailed plan
396
+ logger.info("Creating detailed plan...")
397
+ detailed_plan = blog_gen.create_detailed_plan(cluster_data, plan, research)
398
+
399
+ # Write blog post
400
+ logger.info("Writing blog post...")
401
+ blog_content = blog_gen.write_blog_post(detailed_plan, cluster_data)
402
+
403
+ # Add internal links
404
+ logger.info("Adding internal links...")
405
+ previous_posts = csv_handler.get_previous_posts()
406
+ blog_content = blog_gen.add_internal_links(blog_content, previous_posts)
407
+
408
+ # Convert to HTML
409
+ logger.info("Converting to HTML...")
410
+ cover_image_url = blog_gen.get_cover_image(cluster_data['Primary Keyword'])
411
+ html_content = blog_gen.convert_to_html(blog_content, cover_image_url)
412
+
413
+ # Generate metadata
414
+ logger.info("Generating metadata...")
415
+ metadata = blog_gen.generate_metadata(blog_content, cluster_data['Primary Keyword'], cluster_data)
416
+
417
+ # Get cover image
418
+ logger.info("Getting cover image...")
419
+ cover_image_url = blog_gen.get_cover_image(metadata['title'])
420
+
421
+ blog_post_data = {
422
+ 'title': metadata['title'],
423
+ 'slug': metadata['slug'],
424
+ 'meta_description': metadata['meta_description'],
425
+ 'content': html_content,
426
+ 'cover_image': cover_image_url,
427
+ 'keywords': cluster_data['Keywords'],
428
+ 'primary_keyword': cluster_data['Primary Keyword'],
429
+ 'research': research,
430
+ 'detailed_plan': detailed_plan
431
+ }
432
+
433
+ generated_blogs.append({
434
+ 'status': 'success',
435
+ 'message': f"Blog post generated successfully for {cluster_data['Primary Keyword']}",
436
+ 'data': blog_post_data
437
+ })
438
+
439
+ csv_handler.mark_cluster_complete(cluster_data['row_number'])
440
+ csv_handler.log_completed_post({**metadata, 'keywords': cluster_data['Keywords']})
441
+
442
+ except Exception as e:
443
+ logger.error(f"Error processing cluster {cluster_data['Primary Keyword']}: {e}")
444
+ generated_blogs.append({
445
+ 'status': 'error',
446
+ 'message': f"Failed to generate blog post for {cluster_data['Primary Keyword']}",
447
+ 'error': str(e)
448
+ })
449
+
450
+ logger.info("All blog generation completed!")
451
+ return jsonify({
452
+ 'status': 'success',
453
+ 'message': f'Generated {len(generated_blogs)} blog posts from CSV text',
454
+ 'blogs': generated_blogs
455
+ })
456
+
457
+ except Exception as e:
458
+ logger.error(f"Error in generate_from_csv_text: {e}")
459
+ return jsonify({'error': str(e)}), 500
460
+
461
+ if __name__ == '__main__':
462
+ app.run(host='127.0.0.1', port=5001, debug=True)
blog_generator.py ADDED
@@ -0,0 +1,440 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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):
9
+ self.openai_key = openai_key
10
+ self.openrouter_key = openrouter_key
11
+ # serpapi_key is now optional since we're using our own image search
12
+
13
+ def get_cover_image(self, title: str) -> str:
14
+ """Get a cover image URL for the blog post"""
15
+ try:
16
+ # Use our custom image search function
17
+ image_url = get_cover_image(title)
18
+ if not image_url:
19
+ print("No image found, trying with modified query...")
20
+ # Try again with a more generic query if specific title fails
21
+ image_url = get_cover_image(title + " high quality cover image")
22
+ return image_url
23
+ except Exception as e:
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(
30
+ 'https://openrouter.ai/api/v1/chat/completions',
31
+ headers={
32
+ 'Authorization': f'Bearer {self.openrouter_key}',
33
+ 'HTTP-Referer': 'http://localhost:5001',
34
+ 'X-Title': 'Blog Generator'
35
+ },
36
+ json={
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
+
71
+ Keywords: {cluster_data['Keywords']}
72
+ Primary keyword: {cluster_data['Primary Keyword']}
73
+ Search intent: {cluster_data['Intent']}
74
+ Preliminary plan: {preliminary_plan}
75
+ Research findings: {research}
76
+
77
+ Create the detailed plan."""
78
+ }]
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(
98
+ 'https://openrouter.ai/api/v1/chat/completions',
99
+ headers={
100
+ 'Authorization': f'Bearer {self.openrouter_key}',
101
+ 'HTTP-Referer': 'http://localhost:5001',
102
+ 'X-Title': 'Blog Generator'
103
+ },
104
+ json={
105
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
106
+ 'messages': [{
107
+ 'role': 'user',
108
+ 'content': f"""You are part of a team that creates world-class blog posts.
109
+
110
+ You are the teams best copywriter and are responsible for writing out the actual blog post.
111
+
112
+ For each new blog post project you are provided with a detailed plan and research findings.
113
+
114
+ Your job is to create the blog post by closely following the detailed plan.
115
+
116
+ The blog post you create must:
117
+ - Follow the plan bit by bit
118
+ - Use short paragraphs
119
+ - Use bullet points and subheadings with keywords where appropriate
120
+ - Not have any fluff. The content must be value dense and direct
121
+ - Be very detailed
122
+ - Include the keywords mentioned in each section within that section
123
+ - Place the primary keyword in the blog title, H1 header and early in the introduction
124
+ - Place one keyword for each section in the heading of that section
125
+ - When possible pepper synonyms of the keywords throughout each section
126
+ - When possible use Latent Semantic Indexing (LSI) keywords and related terms
127
+ - Be at minimum 2000 to 2500 words long
128
+ - Be suitable for a year 5 reading level
129
+
130
+ Make sure to create the entire blog post draft in your first output. Don't stop or cut it short.
131
+
132
+ Here are the details for your next blog post:
133
+
134
+ Keywords: {cluster_data['Keywords']}
135
+ Primary keyword: {cluster_data['Primary Keyword']}
136
+ Search intent: {cluster_data['Intent']}
137
+ Detailed plan: {detailed_plan}
138
+
139
+ Write the blog post."""
140
+ }]
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(
160
+ # 'https://api.openai.com/v1/chat/completions',
161
+ # headers={
162
+ # 'Authorization': f'Bearer {self.openai_key}',
163
+ # 'Content-Type': 'application/json'
164
+ # },
165
+ # json={
166
+ # 'model': 'gpt-4',
167
+ 'https://openrouter.ai/api/v1/chat/completions',
168
+ headers={
169
+ 'Authorization': f'Bearer {self.openrouter_key}',
170
+ 'HTTP-Referer': 'http://localhost:5001',
171
+ 'X-Title': 'Blog Generator'
172
+ },
173
+ json={
174
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
175
+ 'messages': [{
176
+ 'role': 'user',
177
+ 'content': f"""You are part of a team that creates world class blog posts.
178
+
179
+ You are in charge of internal linking between blog posts.
180
+
181
+ For each new blog post that comes across your desk, your job is to look through previously posted blogs and make at least 5 internal links.
182
+
183
+ To choose the best internal linking opportunities you must:
184
+ - Read the previous blog post summaries and look through their keywords. If there is a match where the previous blog post is highly relevant, then this is an internal linking opportunity.
185
+ - Do not link if it is not highly relevant. Only make a link if it makes sense and adds value for the reader.
186
+
187
+ Once you've found the best linking opportunities, you must update the blog post with the internal links. To do this you must:
188
+ - Add the link of the previous blog post at the relevant section of the new blog post. Drop the URL at the place which makes most sense. Later we will hyperlink the URL to the word in the blog post which it is placed next to. So your placing is very important.
189
+
190
+ Make sure to:
191
+ - Not delete any existing URLs or change anything about the blog post
192
+ - Only add new internal linking URLs
193
+ - Place URLs next to relevant anchor text
194
+ - Add at least 5 internal links if possible
195
+ - Only link when truly relevant and valuable
196
+ - Preserve all original content and formatting
197
+
198
+ Current blog post:
199
+ {blog_content}
200
+
201
+ Previous blog posts:
202
+ {json.dumps(previous_posts, indent=2)}
203
+
204
+ Your output must be the complete blog post with new internal links added. Don't summarize or modify the content - just add the URLs in appropriate places."""
205
+ }]
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={
236
+ # 'Authorization': f'Bearer {self.openai_key}',
237
+ # 'Content-Type': 'application/json'
238
+ # },
239
+ # json={
240
+ # 'model': 'anthropic/claude-3.5-sonnet:beta',
241
+ 'https://openrouter.ai/api/v1/chat/completions',
242
+ headers={
243
+ 'Authorization': f'Bearer {self.openrouter_key}',
244
+ 'HTTP-Referer': 'http://localhost:5001',
245
+ 'X-Title': 'Blog Generator'
246
+ },
247
+ json={
248
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
249
+ 'messages': [{
250
+ 'role': 'user',
251
+ 'content': f"""DO NOT OUTPUT ANYTHING OTHER THAN THE HTML CODE. Follow this layout template to generate WordPress code for a blog post:
252
+
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
260
+ - FAQ
261
+
262
+ Rules:
263
+ - Make it engaging using italics, dot points, quotes, bold, spaces, and new lines. No emojis.
264
+ - Hyperlink any referenced URLs to their adjacent keyphrases
265
+ - Wrap content in container <div> with inline CSS for white text (#000000), Arial/sans-serif font, 1.6 line height
266
+ - Set non-heading text to 20px and white (#000000) with !important
267
+ - Style links, TOC points, and FAQ questions in blue (#00c2ff)
268
+ - Add blue (#00c2ff) underline border to headings with padding
269
+ - Add double breaks (<br>) between sections
270
+ - Output only the HTML code, no extra text
271
+
272
+ Blog post content:
273
+ {formatted_content}"""
274
+ }]
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}")
285
+ blog_content1 = response_data['choices'][0]['message']['content']
286
+ formatted_content1 = blog_content1.replace('\n', '')
287
+ return formatted_content1
288
+ except Exception as e:
289
+ print(f"Error in convert_to_html: {e}")
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
296
+ slug_response = requests.post(
297
+ # 'https://api.openai.com/v1/chat/completions',
298
+ # headers={
299
+ # 'Authorization': f'Bearer {self.openai_key}',
300
+ # 'Content-Type': 'application/json'
301
+ # },
302
+ # json={
303
+ # 'model': 'gpt-4',
304
+ 'https://openrouter.ai/api/v1/chat/completions',
305
+ headers={
306
+ 'Authorization': f'Bearer {self.openrouter_key}',
307
+ 'HTTP-Referer': 'http://localhost:5001',
308
+ 'X-Title': 'Blog Generator'
309
+ },
310
+ json={
311
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
312
+ 'messages': [{
313
+ 'role': 'user',
314
+ 'content': f"""Create a slug for the following blog post:
315
+
316
+ {blog_content}
317
+
318
+ A slug in a blog post is the part of the URL that comes after the domain name and identifies a specific page. It is typically a short, descriptive phrase that summarizes the content of the post, making it easier for users and search engines to understand what the page is about. For example, in the URL www.example.com/intelligent-agents, the slug is intelligent-agents. A good slug is concise, contains relevant keywords, and avoids unnecessary words to improve readability and SEO.
319
+
320
+ The slug must be 4 or 5 words max and must include the primary keyword of the blog post which is {primary_keyword}.
321
+
322
+ Your output must be the slug and nothing else so that I can copy and paste your output and put it at the end of my blog post URL to post it right away."""
323
+ }]
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
338
+ title_response = requests.post(
339
+ # 'https://api.openai.com/v1/chat/completions',
340
+ # headers={
341
+ # 'Authorization': f'Bearer {self.openai_key}',
342
+ # 'Content-Type': 'application/json'
343
+ # },
344
+ # json={
345
+ # 'model': 'gpt-4',
346
+ 'https://openrouter.ai/api/v1/chat/completions',
347
+ headers={
348
+ 'Authorization': f'Bearer {self.openrouter_key}',
349
+ 'HTTP-Referer': 'http://localhost:5001',
350
+ 'X-Title': 'Blog Generator'
351
+ },
352
+ json={
353
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
354
+ 'messages': [{
355
+ 'role': 'user',
356
+ 'content': f"""Extract the blog post title from the following blog post:
357
+
358
+ {blog_content}
359
+
360
+ The blog post title must include the primary keyword {primary_keyword} and must inform the users right away of what they can expect from reading the blog post.
361
+
362
+ - Don't put the output in "". The output should just text with no markdown or formatting.
363
+
364
+ Your output must only be the blog post title and nothing else."""
365
+ }]
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
380
+ meta_desc_response = requests.post(
381
+ # 'https://api.openai.com/v1/chat/completions',
382
+ # headers={
383
+ # 'Authorization': f'Bearer {self.openai_key}',
384
+ # 'Content-Type': 'application/json'
385
+ # },
386
+ # json={
387
+ # 'model': 'gpt-4',
388
+ 'https://openrouter.ai/api/v1/chat/completions',
389
+ headers={
390
+ 'Authorization': f'Bearer {self.openrouter_key}',
391
+ 'HTTP-Referer': 'http://localhost:5001',
392
+ 'X-Title': 'Blog Generator'
393
+ },
394
+ json={
395
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
396
+ 'messages': [{
397
+ 'role': 'user',
398
+ 'content': f"""Create a proper meta description for the following blog post:
399
+
400
+ {blog_content}
401
+
402
+ A good meta description for a blog post that is SEO-optimized should:
403
+ - Be Concise: Stick to 150-160 characters to ensure the full description displays in search results.
404
+ - Include Keywords: Incorporate primary keywords naturally to improve visibility and relevance to search queries.
405
+
406
+ Primary keyword = {primary_keyword}
407
+
408
+ More keywords to include if possible = [{cluster_data['Keywords']}]
409
+
410
+ - Provide Value: Clearly describe what the reader will learn or gain by clicking the link.
411
+ - Be Engaging: Use persuasive language, such as action verbs or a question, to encourage clicks.
412
+ - Align with Content: Ensure the description accurately reflects the blog post to meet user expectations and reduce bounce rates.
413
+
414
+ Your output must only be the meta description and nothing else."""
415
+ }]
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
430
+ if not title or not meta_desc or not slug:
431
+ raise Exception("Empty title, meta description, or slug")
432
+
433
+ return {
434
+ 'slug': slug,
435
+ 'title': title,
436
+ 'meta_description': meta_desc
437
+ }
438
+ except Exception as e:
439
+ print(f"Error in generate_metadata: {e}")
440
+ raise
clusters.csv ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ Cluster,Keywords,Intent,Primary Keyword,Status
2
+ 1,Maniya Sri Lankan Streamer," Maniya Streams youtube channel""",Manitha Abeysiriwardana,completed,Maniya Streams,yes
completed_posts.csv ADDED
@@ -0,0 +1,4 @@
 
 
 
 
 
1
+ Title,Keywords,Meta Description,URL
2
+ Discovering Nanobotz IT Solutions: Your Detailed Guide,"Nanobotz it solutions company, nanobotz it solutions company details","Discover Nanobotz IT Solutions: Your detailed guide to this Sri Lankan IT company's services, expertise & see if they are the right IT partner for your business.",https://yourblog.com/nanobotz-it-solutions-guide
3
+ Unveiling Nanobotz IT Solutions: Your Comprehensive Guide to the Company and its Services,"Nanobotz it solutions company, nanobotz it solutions company details","Discover Nanobotz IT Solutions, a comprehensive IT solutions company. Learn about their services, expertise & see if they are the right partner for you.",https://yourblog.com/nanobotz-it-solutions-company-guide
4
+ "Discover ""Maniya Streams"": Your Gateway to Engaging Content by Sri Lankan Streamer Manitha Abeysiriwardana",Maniya Sri Lankan Streamer,Discover Maniya Streams! Your gateway to engaging content by Sri Lankan streamer Manitha Abeysiriwardana. Learn about her YouTube channel and videos now!,https://yourblog.com/manitha-abeysiriwardana-maniya-streams
create_csv_files.py ADDED
@@ -0,0 +1,20 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+
3
+ def create_clusters_csv():
4
+ """Create the initial clusters.csv file with proper headers"""
5
+ with open('clusters.csv', 'w', newline='', encoding='utf-8') as file:
6
+ writer = csv.writer(file)
7
+ writer.writerow(['Keywords', 'Intent', 'Primary Keyword', 'Status'])
8
+
9
+ # Create clusters.csv
10
+ with open('clusters.csv', 'w', newline='') as file:
11
+ writer = csv.writer(file)
12
+ writer.writerow(['Cluster', 'Keywords', 'Intent', 'Primary Keyword', 'Status'])
13
+ # Add some sample data
14
+ # writer.writerow(['1', 'ai tools, business productivity, automation', 'Learn about AI tools', 'ai tools for business', 'no'])
15
+ writer.writerow(['2', 'Nanobotz it solutions company, nanobotz it solutions company details', 'nanobotz it solutions', 'nanobotz it solutions', 'no'])
16
+
17
+ # Create completed_posts.csv
18
+ with open('completed_posts.csv', 'w', newline='') as file:
19
+ writer = csv.writer(file)
20
+ writer.writerow(['Title', 'Keywords', 'Meta Description', 'URL'])
credentials.json ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "installed": {
3
+ "client_id": "184835172690-4da6h3la4rovieickngacl4ec9n5g4j9.apps.googleusercontent.com",
4
+ "project_id": "silver-shift-424412-a8",
5
+ "auth_uri": "https://accounts.google.com/o/oauth2/auth",
6
+ "token_uri": "https://oauth2.googleapis.com/token",
7
+ "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
8
+ "client_secret": "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",
9
+ "redirect_uris": [
10
+ "http://localhost:5001",
11
+ "http://127.0.0.1:5001"
12
+ ]
13
+ }
14
+ }
csv_handler.py ADDED
@@ -0,0 +1,196 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import csv
2
+ from typing import Dict, List
3
+ import os
4
+ import logging
5
+
6
+ logger = logging.getLogger(__name__)
7
+
8
+ class CSVHandler:
9
+ def __init__(self):
10
+ self.clusters_file = 'clusters.csv'
11
+ self.completed_posts_file = 'completed_posts.csv'
12
+
13
+ def get_cluster_data(self) -> Dict:
14
+ try:
15
+ with open(self.clusters_file, 'r', newline='') as file:
16
+ reader = csv.DictReader(file)
17
+ for i, row in enumerate(reader, start=2): # start=2 because row 1 is header
18
+ if row['Status'].lower() == 'no':
19
+ return {
20
+ 'Keywords': row['Keywords'],
21
+ 'Intent': row['Intent'],
22
+ 'Primary Keyword': row['Primary Keyword'],
23
+ 'row_number': i
24
+ }
25
+ return None
26
+ except Exception as e:
27
+ print(f"Error reading clusters CSV: {e}")
28
+ return None
29
+
30
+ def get_previous_posts(self) -> List[Dict]:
31
+ try:
32
+ posts = []
33
+ with open(self.completed_posts_file, 'r', newline='') as file:
34
+ reader = csv.DictReader(file)
35
+ for row in reader:
36
+ posts.append({
37
+ 'title': row['Title'],
38
+ 'keywords': row['Keywords'],
39
+ 'summary': row['Meta Description'],
40
+ 'url': row['URL']
41
+ })
42
+ return posts
43
+ except Exception as e:
44
+ print(f"Error reading completed posts CSV: {e}")
45
+ return []
46
+
47
+ def mark_cluster_complete(self, row_number: int):
48
+ try:
49
+ # Read all rows
50
+ rows = []
51
+ with open(self.clusters_file, 'r', newline='', encoding='utf-8') as file:
52
+ reader = csv.reader(file)
53
+ header = next(reader) # Get header row
54
+
55
+ # Find Status column index, default to last column if not found
56
+ status_index = header.index('Status') if 'Status' in header else -1
57
+ if status_index == -1:
58
+ header.append('Status')
59
+ status_index = len(header) - 1
60
+
61
+ rows = [header]
62
+ rows.extend(list(reader))
63
+
64
+ # Update status to 'completed' for the specified row
65
+ if row_number < len(rows):
66
+ # Ensure row has enough columns
67
+ while len(rows[row_number]) <= status_index:
68
+ rows[row_number].append('')
69
+ rows[row_number][status_index] = 'completed'
70
+
71
+ # Write back all rows
72
+ with open(self.clusters_file, 'w', newline='', encoding='utf-8') as file:
73
+ writer = csv.writer(file)
74
+ writer.writerows(rows)
75
+
76
+ except Exception as e:
77
+ logger.error(f"Error updating cluster status: {e}")
78
+ raise
79
+
80
+ def log_completed_post(self, metadata: Dict):
81
+ try:
82
+ with open(self.completed_posts_file, 'a', newline='') as file:
83
+ writer = csv.writer(file)
84
+ writer.writerow([
85
+ metadata['title'],
86
+ metadata['keywords'],
87
+ metadata['meta_description'],
88
+ f"https://yourblog.com/{metadata['slug']}"
89
+ ])
90
+ except Exception as e:
91
+ print(f"Error logging completed post: {e}")
92
+
93
+ def get_all_clusters(self):
94
+ """Get all uncompleted clusters from the CSV file."""
95
+ clusters = []
96
+ try:
97
+ with open(self.clusters_file, 'r', newline='', encoding='utf-8') as file:
98
+ reader = csv.DictReader(file)
99
+ for row_number, row in enumerate(reader, start=1):
100
+ # Check if Status column exists, if not or empty, treat as not completed
101
+ status = row.get('Status', '').lower()
102
+ if status != 'completed':
103
+ cluster_data = {
104
+ 'Keywords': row.get('Keywords', ''),
105
+ 'Intent': row.get('Intent', ''),
106
+ 'Primary Keyword': row.get('Primary Keyword', ''),
107
+ 'row_number': row_number,
108
+ 'Status': status
109
+ }
110
+ # Validate required fields
111
+ if all(cluster_data[field] for field in ['Keywords', 'Intent', 'Primary Keyword']):
112
+ clusters.append(cluster_data)
113
+ else:
114
+ logger.warning(f"Row {row_number}: Missing required fields, skipping")
115
+ return clusters
116
+ except Exception as e:
117
+ logger.error(f"Error reading clusters file: {e}")
118
+ raise
119
+
120
+ def process_uploaded_csv(self, csv_content: str) -> List[Dict]:
121
+ """
122
+ Process an uploaded CSV content string and return cluster data for blog generation.
123
+
124
+ Args:
125
+ csv_content (str): The decoded CSV content as a string
126
+
127
+ Returns:
128
+ List[Dict]: List of cluster data dictionaries
129
+ """
130
+ clusters = []
131
+ try:
132
+ # Split the content into lines and process as CSV
133
+ from io import StringIO
134
+ csv_file = StringIO(csv_content)
135
+ reader = csv.DictReader(csv_file)
136
+
137
+ for row_number, row in enumerate(reader, start=1):
138
+ # Validate required columns
139
+ required_columns = ['Keywords', 'Intent', 'Primary Keyword']
140
+ if not all(col in row for col in required_columns):
141
+ logger.error(f"Row {row_number}: Missing required columns. Required: {required_columns}")
142
+ continue
143
+
144
+ cluster_data = {
145
+ 'Keywords': row['Keywords'],
146
+ 'Intent': row['Intent'],
147
+ 'Primary Keyword': row['Primary Keyword'],
148
+ 'row_number': row_number
149
+ }
150
+ clusters.append(cluster_data)
151
+
152
+ logger.info(f"Successfully processed {len(clusters)} clusters from uploaded CSV")
153
+ return clusters
154
+
155
+ except Exception as e:
156
+ logger.error(f"Error processing uploaded CSV: {e}")
157
+ raise
158
+
159
+ def process_csv_text(self, csv_text: str) -> List[Dict]:
160
+ """
161
+ Process CSV content provided as a text string and return cluster data for blog generation.
162
+
163
+ Args:
164
+ csv_text (str): The CSV content as a string
165
+
166
+ Returns:
167
+ List[Dict]: List of cluster data dictionaries
168
+ """
169
+ clusters = []
170
+ try:
171
+ # Split the text into lines
172
+ from io import StringIO
173
+ csv_file = StringIO(csv_text.strip())
174
+ reader = csv.DictReader(csv_file)
175
+
176
+ for row_number, row in enumerate(reader, start=1):
177
+ # Validate required columns
178
+ required_columns = ['Keywords', 'Intent', 'Primary Keyword']
179
+ if not all(col in row for col in required_columns):
180
+ logger.error(f"Row {row_number}: Missing required columns. Required: {required_columns}")
181
+ continue
182
+
183
+ cluster_data = {
184
+ 'Keywords': row['Keywords'],
185
+ 'Intent': row['Intent'],
186
+ 'Primary Keyword': row['Primary Keyword'],
187
+ 'row_number': row_number
188
+ }
189
+ clusters.append(cluster_data)
190
+
191
+ logger.info(f"Successfully processed {len(clusters)} clusters from CSV text")
192
+ return clusters
193
+
194
+ except Exception as e:
195
+ logger.error(f"Error processing CSV text: {e}")
196
+ raise
output.json ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ [
2
+
3
+ ]
phone_location_tracker.py ADDED
@@ -0,0 +1,58 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import phonenumbers
2
+ from phonenumbers import geocoder, carrier, timezone
3
+
4
+ def track_phone_location(phone_number):
5
+ try:
6
+ # Parse the phone number
7
+ parsed_number = phonenumbers.parse(phone_number)
8
+
9
+ # Get location information
10
+ country = geocoder.description_for_number(parsed_number, "en")
11
+
12
+ # Get carrier information
13
+ service_provider = carrier.name_for_number(parsed_number, "en")
14
+
15
+ # Get timezone information
16
+ time_zones = timezone.time_zones_for_number(parsed_number)
17
+
18
+ # Check if the number is valid
19
+ is_valid = phonenumbers.is_valid_number(parsed_number)
20
+
21
+ return {
22
+ "phone_number": phone_number,
23
+ "country": country,
24
+ "service_provider": service_provider,
25
+ "time_zones": list(time_zones),
26
+ "is_valid": is_valid
27
+ }
28
+
29
+ except Exception as e:
30
+ return {
31
+ "error": str(e),
32
+ "phone_number": phone_number
33
+ }
34
+
35
+ def main():
36
+ while True:
37
+ print("\nPhone Number Location Tracker")
38
+ print("----------------------------")
39
+ phone_number = input("Enter phone number (with country code, e.g., +1234567890) or 'q' to quit: ")
40
+
41
+ if phone_number.lower() == 'q':
42
+ print("Goodbye!")
43
+ break
44
+
45
+ result = track_phone_location(phone_number)
46
+
47
+ if "error" in result:
48
+ print(f"\nError: {result['error']}")
49
+ else:
50
+ print("\nResults:")
51
+ print(f"Phone Number: {result['phone_number']}")
52
+ print(f"Country: {result['country']}")
53
+ print(f"Service Provider: {result['service_provider']}")
54
+ print(f"Time Zones: {', '.join(result['time_zones'])}")
55
+ print(f"Valid Number: {'Yes' if result['is_valid'] else 'No'}")
56
+
57
+ if __name__ == "__main__":
58
+ main()
requirements.txt ADDED
@@ -0,0 +1,7 @@
 
 
 
 
 
 
 
 
1
+ flask==3.0.0
2
+ python-dotenv==1.0.0
3
+ requests==2.31.0
4
+ beautifulsoup4==4.12.2
5
+ python-slugify==8.0.1
6
+ logging==0.4.9.6
7
+ typing==3.7.4.3
sheets_handler.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from google_auth_oauthlib.flow import InstalledAppFlow
2
+ from google.oauth2.credentials import Credentials
3
+ from google.auth.transport.requests import Request
4
+ from googleapiclient.discovery import build
5
+ import os.path
6
+ import pickle
7
+ from typing import Dict
8
+
9
+ class SheetsHandler:
10
+ def __init__(self, credentials_path: str):
11
+ self.SCOPES = ['https://www.googleapis.com/auth/spreadsheets']
12
+ self.creds = None
13
+
14
+ # Load existing token if it exists
15
+ if os.path.exists('token.pickle'):
16
+ with open('token.pickle', 'rb') as token:
17
+ self.creds = pickle.load(token)
18
+
19
+ # If no valid credentials available, let user log in
20
+ if not self.creds or not self.creds.valid:
21
+ if self.creds and self.creds.expired and self.creds.refresh_token:
22
+ self.creds.refresh(Request())
23
+ else:
24
+ flow = InstalledAppFlow.from_client_secrets_file(
25
+ credentials_path, self.SCOPES)
26
+ self.creds = flow.run_local_server(
27
+ port=8085,
28
+ success_message='The authentication flow has completed. You may close this window.',
29
+ open_browser=True
30
+ )
31
+
32
+ # Save the credentials for the next run
33
+ with open('token.pickle', 'wb') as token:
34
+ pickle.dump(self.creds, token)
35
+
36
+ self.service = build('sheets', 'v4', credentials=self.creds)
37
+
38
+ def get_previous_posts(self):
39
+ # Implementation for getting previous posts
40
+ try:
41
+ result = self.service.spreadsheets().values().get(
42
+ spreadsheetId='1CL0L4V288SEygm0BieMRM8t8h7MbcV9bYyzkDc0zInU',
43
+ range='Sheet1!A:D'
44
+ ).execute()
45
+ rows = result.get('values', [])
46
+ if not rows:
47
+ return []
48
+
49
+ posts = []
50
+ for row in rows[1:]: # Skip header
51
+ if len(row) >= 4:
52
+ posts.append({
53
+ 'title': row[0],
54
+ 'keywords': row[1],
55
+ 'summary': row[2],
56
+ 'url': row[3]
57
+ })
58
+ return posts
59
+ except Exception as e:
60
+ print(f"Error getting previous posts: {e}")
61
+ return []
62
+
63
+ def mark_cluster_complete(self, sheet_id: str, sheet_name: str, row_number: int):
64
+ range_name = f"{sheet_name}!E{row_number}"
65
+ body = {
66
+ 'values': [['yes']]
67
+ }
68
+ self.service.spreadsheets().values().update(
69
+ spreadsheetId=sheet_id,
70
+ range=range_name,
71
+ valueInputOption='RAW',
72
+ body=body
73
+ ).execute()
74
+
75
+ def log_completed_post(self, sheet_id: str, metadata: Dict):
76
+ range_name = 'Sheet1!A:D'
77
+ body = {
78
+ 'values': [[
79
+ metadata['title'],
80
+ metadata['keywords'],
81
+ metadata['meta_description'],
82
+ f"https://yourblog.com/{metadata['slug']}"
83
+ ]]
84
+ }
85
+ self.service.spreadsheets().values().append(
86
+ spreadsheetId=sheet_id,
87
+ range=range_name,
88
+ valueInputOption='RAW',
89
+ insertDataOption='INSERT_ROWS',
90
+ body=body
91
+ ).execute()
test_api.py ADDED
@@ -0,0 +1,29 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import json
3
+
4
+ def test_blog_generation():
5
+ try:
6
+ # Add timeout of 300 seconds (5 minutes)
7
+ response = requests.post('http://localhost:5001/generate-blog', timeout=300)
8
+
9
+ if response.status_code == 200:
10
+ data = response.json()
11
+ print("Blog Generation Successful!")
12
+ print("\nTitle:", data['data']['title'])
13
+ print("\nSlug:", data['data']['slug'])
14
+ print("\nMeta Description:", data['data']['meta_description'])
15
+ print("\nKeywords:", data['data']['keywords'])
16
+ print("\nPrimary Keyword:", data['data']['primary_keyword'])
17
+ print("\nCover Image URL:", data['data']['cover_image'])
18
+ print("\nDetailed Plan:", data['data']['detailed_plan'])
19
+ print("\nResearch:", data['data']['research'])
20
+ print("\nContent Preview:", data['data']['content'][:500] + "...")
21
+ else:
22
+ print("Error:", response.json()['error'])
23
+ except requests.exceptions.Timeout:
24
+ print("Request timed out after 5 minutes")
25
+ except Exception as e:
26
+ print(f"Error: {e}")
27
+
28
+ if __name__ == "__main__":
29
+ test_blog_generation()
testt.py ADDED
@@ -0,0 +1,38 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from selenium import webdriver
2
+ from selenium.webdriver.common.by import By
3
+ from selenium.webdriver.chrome.service import Service
4
+ from webdriver_manager.chrome import ChromeDriverManager
5
+
6
+ def start_requests(self):
7
+ query = input("Enter your search query: ")
8
+ google_search_url = f"https://www.google.com/search?q={query}"
9
+
10
+ # Set up Selenium
11
+ options = webdriver.ChromeOptions()
12
+ options.add_argument('--headless') # Run in headless mode
13
+ options.add_argument('--disable-gpu')
14
+ options.add_argument('--no-sandbox')
15
+ driver = webdriver.Chrome(service=Service(ChromeDriverManager().install()), options=options)
16
+
17
+ driver.get(google_search_url)
18
+ soup = BeautifulSoup(driver.page_source, 'html.parser')
19
+ driver.quit()
20
+
21
+ urls = []
22
+ for link in soup.find_all('a', href=True):
23
+ href = link['href']
24
+ if href.startswith('/url?q='):
25
+ url = href.split('/url?q=')[1].split('&')[0]
26
+ if not url.startswith('http'):
27
+ continue
28
+ urls.append(url)
29
+ if len(urls) == self.max_scrapes:
30
+ break
31
+
32
+ if not urls:
33
+ self.logger.error("No URLs extracted from Google search results.")
34
+ return
35
+
36
+ self.logger.info(f"Extracted URLs: {urls}")
37
+ for url in urls:
38
+ yield Request(url, callback=self.parse)
web_scraper.py ADDED
@@ -0,0 +1,377 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from flask import Flask, jsonify, request
2
+ import requests
3
+ from bs4 import BeautifulSoup
4
+ import os
5
+ import re
6
+ import urllib.parse
7
+ import time
8
+ import random
9
+ import base64
10
+ from io import BytesIO
11
+ from googlesearch import search
12
+ import json
13
+
14
+ app = Flask(__name__)
15
+
16
+ def search_images(query, num_images=5):
17
+ # Headers to mimic a browser request
18
+ headers = {
19
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
20
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
21
+ 'Accept-Language': 'en-US,en;q=0.5',
22
+ 'Accept-Encoding': 'gzip, deflate',
23
+ 'DNT': '1',
24
+ 'Connection': 'keep-alive',
25
+ }
26
+
27
+ # Format the query for URL
28
+ formatted_query = urllib.parse.quote(query + " high quality")
29
+
30
+ # Google Images URL
31
+ url = f"https://www.google.com/search?q={formatted_query}&tbm=isch&safe=active"
32
+
33
+ try:
34
+ # Get the HTML content
35
+ response = requests.get(url, headers=headers, timeout=30)
36
+ response.raise_for_status()
37
+
38
+ # Find all image URLs using regex
39
+ image_urls = re.findall(r'https?://[^"\']*?(?:jpg|jpeg|png|gif)', response.text)
40
+
41
+ # Remove duplicates while preserving order
42
+ image_urls = list(dict.fromkeys(image_urls))
43
+
44
+ # Filter and clean results
45
+ results = []
46
+ for img_url in image_urls:
47
+ if len(results) >= num_images:
48
+ break
49
+
50
+ # Skip small thumbnails, icons, and low-quality images
51
+ if ('gstatic.com' in img_url or
52
+ 'google.com' in img_url or
53
+ 'icon' in img_url.lower() or
54
+ 'thumb' in img_url.lower() or
55
+ 'small' in img_url.lower()):
56
+ continue
57
+
58
+ try:
59
+ # Verify the image URL is valid
60
+ img_response = requests.head(img_url, headers=headers, timeout=5)
61
+ if img_response.status_code == 200:
62
+ content_type = img_response.headers.get('Content-Type', '')
63
+ if content_type.startswith('image/'):
64
+ results.append({
65
+ 'url': img_url,
66
+ 'content_type': content_type
67
+ })
68
+
69
+ except Exception as e:
70
+ print(f"Error checking image URL: {str(e)}")
71
+ continue
72
+
73
+ # Add a small delay between checks
74
+ time.sleep(random.uniform(0.2, 0.5))
75
+
76
+ return results
77
+
78
+ except Exception as e:
79
+ print(f"An error occurred: {str(e)}")
80
+ return []
81
+
82
+ def get_cover_image(query):
83
+ """Get a high-quality cover image URL for a given query"""
84
+ try:
85
+ # Search for images
86
+ images = search_images(query, num_images=3) # Get top 3 images to choose from
87
+
88
+ if not images:
89
+ return None
90
+
91
+ # Return the first valid image URL
92
+ return images[0]['url']
93
+
94
+ except Exception as e:
95
+ print(f"Error getting cover image: {str(e)}")
96
+ return None
97
+
98
+ @app.route('/search_images', methods=['GET'])
99
+ def api_search_images():
100
+ try:
101
+ # Get query parameters
102
+ query = request.args.get('query', '')
103
+ num_images = int(request.args.get('num_images', 5))
104
+
105
+ if not query:
106
+ return jsonify({'error': 'Query parameter is required'}), 400
107
+
108
+ if num_images < 1 or num_images > 20:
109
+ return jsonify({'error': 'Number of images must be between 1 and 20'}), 400
110
+
111
+ # Search for images
112
+ results = search_images(query, num_images)
113
+
114
+ return jsonify({
115
+ 'success': True,
116
+ 'query': query,
117
+ 'results': results
118
+ })
119
+
120
+ except Exception as e:
121
+ return jsonify({
122
+ 'success': False,
123
+ 'error': str(e)
124
+ }), 500
125
+
126
+ def scrape_site_content(query, num_sites=5):
127
+ headers = {
128
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36',
129
+ 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8',
130
+ 'Accept-Language': 'en-US,en;q=0.5',
131
+ 'Accept-Encoding': 'gzip, deflate',
132
+ 'DNT': '1',
133
+ 'Connection': 'keep-alive',
134
+ }
135
+
136
+ results = []
137
+ scraped = 0
138
+ retries = 2 # Number of retries per URL
139
+ timeout = 5 # Reduced timeout to 5 seconds
140
+
141
+ try:
142
+ # Get more URLs than needed to account for failures
143
+ search_results = list(search(query, num_results=num_sites * 2))
144
+
145
+ # Process each found URL
146
+ for url in search_results:
147
+ if scraped >= num_sites:
148
+ break
149
+
150
+ success = False
151
+ for attempt in range(retries):
152
+ try:
153
+ # Get the HTML content
154
+ print(f"Trying {url} (attempt {attempt + 1}/{retries})")
155
+ response = requests.get(
156
+ url,
157
+ headers=headers,
158
+ timeout=timeout,
159
+ verify=False # Skip SSL verification
160
+ )
161
+ response.raise_for_status()
162
+
163
+ # Verify it's HTML content
164
+ content_type = response.headers.get('Content-Type', '').lower()
165
+ if 'text/html' not in content_type:
166
+ print(f"Skipping {url} - not HTML content")
167
+ break
168
+
169
+ # Parse the HTML content
170
+ soup = BeautifulSoup(response.text, 'html.parser')
171
+
172
+ # Remove script and style elements
173
+ for script in soup(["script", "style"]):
174
+ script.decompose()
175
+
176
+ # Extract text content (limit to first 10000 characters)
177
+ text_content = soup.get_text(separator='\n', strip=True)[:10000]
178
+
179
+ # Skip if not enough content
180
+ if len(text_content.split()) < 100: # Skip if less than 100 words
181
+ print(f"Skipping {url} - not enough content")
182
+ break
183
+
184
+ # Extract all links (limit to first 10)
185
+ links = []
186
+ for link in soup.find_all('a', href=True)[:10]:
187
+ href = link['href']
188
+ if href.startswith('http'):
189
+ links.append({
190
+ 'text': link.get_text(strip=True),
191
+ 'url': href
192
+ })
193
+
194
+ # Extract meta information
195
+ title = soup.title.string if soup.title else ''
196
+ meta_description = ''
197
+ meta_keywords = ''
198
+
199
+ meta_desc_tag = soup.find('meta', attrs={'name': 'description'})
200
+ if meta_desc_tag:
201
+ meta_description = meta_desc_tag.get('content', '')
202
+
203
+ meta_keywords_tag = soup.find('meta', attrs={'name': 'keywords'})
204
+ if meta_keywords_tag:
205
+ meta_keywords = meta_keywords_tag.get('content', '')
206
+
207
+ results.append({
208
+ 'url': url,
209
+ 'title': title,
210
+ 'meta_description': meta_description,
211
+ 'meta_keywords': meta_keywords,
212
+ 'text_content': text_content,
213
+ 'links': links
214
+ })
215
+
216
+ scraped += 1
217
+ success = True
218
+ # Add a random delay between scrapes
219
+ time.sleep(random.uniform(0.5, 1))
220
+ break # Break retry loop on success
221
+
222
+ except requests.Timeout:
223
+ print(f"Timeout on {url} (attempt {attempt + 1}/{retries})")
224
+ if attempt == retries - 1: # Last attempt
225
+ print(f"Skipping {url} after {retries} timeout attempts")
226
+ except requests.RequestException as e:
227
+ print(f"Error scraping {url} (attempt {attempt + 1}/{retries}): {str(e)}")
228
+ if attempt == retries - 1: # Last attempt
229
+ print(f"Skipping {url} after {retries} failed attempts")
230
+
231
+ # Add a longer delay between retries
232
+ if not success and attempt < retries - 1:
233
+ time.sleep(random.uniform(1, 2))
234
+
235
+ # If we haven't found enough valid content and have more URLs, continue
236
+ if scraped < num_sites and len(results) < len(search_results):
237
+ continue
238
+
239
+ return results
240
+
241
+ except Exception as e:
242
+ print(f"Error in search/scraping process: {str(e)}")
243
+ # Return whatever results we've managed to gather
244
+ return results
245
+
246
+ @app.route('/scrape_sites', methods=['GET'])
247
+ def api_scrape_sites():
248
+ try:
249
+ # Get query parameters
250
+ query = request.args.get('query', '')
251
+ num_sites = int(request.args.get('num_sites', 10))
252
+
253
+ if not query:
254
+ return jsonify({'error': 'Query parameter is required'}), 400
255
+
256
+ if num_sites < 1 or num_sites > 20:
257
+ return jsonify({'error': 'Number of sites must be between 1 and 20'}), 400
258
+
259
+ # Scrape the websites
260
+ results = scrape_site_content(query, num_sites)
261
+
262
+ return jsonify({
263
+ 'success': True,
264
+ 'query': query,
265
+ 'results': results
266
+ })
267
+
268
+ except Exception as e:
269
+ return jsonify({
270
+ 'success': False,
271
+ 'error': str(e)
272
+ }), 500
273
+
274
+ def analyze_with_gpt(scraped_content, research_query):
275
+ """Analyze scraped content using OpenRouter's Gemini model"""
276
+ try:
277
+ headers = {
278
+ 'Authorization': f'Bearer {os.getenv("OPENROUTER_API_KEY")}',
279
+ 'HTTP-Referer': 'http://localhost:5001',
280
+ 'X-Title': 'Research Assistant'
281
+ }
282
+
283
+ # Prepare the prompt
284
+ prompt = f"""You are a research assistant analyzing web content to provide comprehensive research.
285
+
286
+ Research Query: {research_query}
287
+
288
+ Below is content scraped from various web sources. Analyze this content and provide a detailed, well-structured research response.
289
+ Make sure to cite sources when making specific claims.
290
+
291
+ Scraped Content:
292
+ {json.dumps(scraped_content, indent=2)}
293
+
294
+ Please provide:
295
+ 1. A comprehensive analysis of the topic
296
+ 2. Key findings and insights
297
+ 3. Supporting evidence from the sources
298
+ 4. Any additional considerations or caveats
299
+
300
+ Format your response in markdown with proper headings and citations."""
301
+
302
+ response = requests.post(
303
+ 'https://openrouter.ai/api/v1/chat/completions',
304
+ headers=headers,
305
+ json={
306
+ 'model': 'google/gemini-2.0-flash-thinking-exp:free',
307
+ 'messages': [{
308
+ 'role': 'user',
309
+ 'content': prompt
310
+ }]
311
+ },
312
+ timeout=60
313
+ )
314
+
315
+ if response.status_code != 200:
316
+ raise Exception(f"OpenRouter API error: {response.text}")
317
+
318
+ return response.json()['choices'][0]['message']['content']
319
+ except Exception as e:
320
+ print(f"Error in analyze_with_gpt: {str(e)}")
321
+ return f"Error analyzing content: {str(e)}"
322
+
323
+ def research_topic(query, num_sites=5):
324
+ """Research a topic using web scraping and GPT analysis"""
325
+ try:
326
+ # First get web content using existing scrape_site_content function
327
+ scraped_results = scrape_site_content(query, num_sites)
328
+
329
+ # Format scraped content for analysis
330
+ formatted_content = []
331
+ for result in scraped_results:
332
+ formatted_content.append({
333
+ 'source': result['url'],
334
+ 'title': result['title'],
335
+ 'content': result['text_content'][:2000], # Limit content length for GPT
336
+ 'meta_info': {
337
+ 'description': result['meta_description'],
338
+ 'keywords': result['meta_keywords']
339
+ }
340
+ })
341
+
342
+ # Get AI analysis of the scraped content
343
+ analysis = analyze_with_gpt(formatted_content, query)
344
+
345
+ return {
346
+ 'success': True,
347
+ 'query': query,
348
+ 'analysis': analysis,
349
+ 'sources': formatted_content
350
+ }
351
+ except Exception as e:
352
+ return {
353
+ 'success': False,
354
+ 'error': str(e)
355
+ }
356
+
357
+ @app.route('/research', methods=['GET'])
358
+ def api_research():
359
+ try:
360
+ query = request.args.get('query', '')
361
+ # Always use 5 sites for consistency
362
+ num_sites = 5
363
+
364
+ if not query:
365
+ return jsonify({'error': 'Query parameter is required'}), 400
366
+
367
+ results = research_topic(query, num_sites)
368
+ return jsonify(results)
369
+
370
+ except Exception as e:
371
+ return jsonify({
372
+ 'success': False,
373
+ 'error': str(e)
374
+ }), 500
375
+
376
+ if __name__ == '__main__':
377
+ app.run(host='0.0.0.0', port=5000)
wordpress_handler.py ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from wordpress_xmlrpc import Client, WordPressPost
2
+ from wordpress_xmlrpc.methods.posts import NewPost
3
+ from typing import Dict
4
+
5
+ class WordPressHandler:
6
+ def __init__(self, url: str, username: str, password: str):
7
+ self.client = Client(url, username, password)
8
+
9
+ def publish_post(self, content: str, metadata: Dict, image_url: str) -> int:
10
+ post = WordPressPost()
11
+ post.title = metadata['title']
12
+ post.content = f'<img src="{image_url}" alt="Cover Image">\n{content}'
13
+ post.slug = metadata['slug']
14
+ post.post_status = 'draft'
15
+ post.terms_names = {
16
+ 'category': ['Your Category'],
17
+ 'post_tag': ['Your Tags']
18
+ }
19
+ post.custom_fields = []
20
+ post.custom_fields.append({
21
+ 'key': '_yoast_wpseo_metadesc',
22
+ 'value': metadata['meta_description']
23
+ })
24
+
25
+ post_id = self.client.call(NewPost(post))
26
+ return post_id