Spaces:
Sleeping
Sleeping
Upload 16 files
Browse files- ZZ_auto_blogger_workflow.json +1244 -0
- app.py +462 -0
- blog_generator.py +440 -0
- clusters.csv +2 -0
- completed_posts.csv +4 -0
- create_csv_files.py +20 -0
- credentials.json +14 -0
- csv_handler.py +196 -0
- output.json +3 -0
- phone_location_tracker.py +58 -0
- requirements.txt +7 -0
- sheets_handler.py +91 -0
- test_api.py +29 -0
- testt.py +38 -0
- web_scraper.py +377 -0
- wordpress_handler.py +26 -0
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
|