Update Blogger Posts with Python & API: Part 25 — WordsByEkta🌿
How to Update All Blogger Posts Automatically Using Python and Blogger API
If you run a blog series on Blogger, you have probably faced this problem: every time you publish a new part, you need to go back and update every previous article manually. You open each post in HTML mode, find the series link box, add the new link, save, and repeat. For 24 articles, that is 24 manual edits every single time.
This article explains how to automate that entire process using Python and the Blogger API. One script. One run. All posts updated.
What Problem Does This Solve?
Blogger does not have shared components or includes. Every article is a standalone HTML file. If you have a series navigation box that appears in 25 posts and you add Part 26, you need to update all 25 posts manually.
With the Blogger API and a Python script, you can do this in one command.
| Without Script | With Script |
|---|---|
| Open each post manually | Script fetches all posts via API |
| Switch to HTML mode | Script works directly on HTML content |
| Find series link box | Script finds it using class name |
| Paste new link | Script inserts new <li> after last item |
| Save each post | Script pushes update back via API |
| Repeat for every post | Runs on all posts in one command |
What You Need Before Starting
- Python installed — version 3.10 or higher.
- A Google Cloud project — free to create at console.cloud.google.com.
- Blogger API enabled — must be enabled inside your Google Cloud project.
- OAuth2 credentials — downloaded as a JSON file from Google Cloud Console.
- Your Blog ID — visible in your Blogger dashboard URL.
- Post IDs — visible in each post's edit URL in Blogger dashboard.
Step 1: Create a Google Cloud Project
If you have already done this for another API such as the Google Indexing API (covered in Part 19), you can use the same project. If not, create a new one.
- Go to console.cloud.google.com.
- Click New Project.
- Give it any name — for example, BloggerAutomation.
- Click Create.
Step 2: Enable the Blogger API
This is a step many people miss. Even after creating a project and credentials, the Blogger API must be explicitly enabled. If you skip this, you will get a 403 error when running the script.
- Inside your Google Cloud project, go to APIs and Services → Library.
- Search for Blogger API v3.
- Click on it and click Enable.
- Wait 2-3 minutes for it to activate.
Step 3: Create OAuth2 Credentials
The Blogger API uses OAuth2 to confirm that the script is authorized to edit your blog. You need to create credentials and download them as a JSON file.
- Go to APIs and Services → Credentials.
- Click Create Credentials → OAuth client ID.
- If prompted, configure the OAuth consent screen first — choose External, fill in app name and email, save.
- For application type, choose Desktop app.
- Click Create.
- Click Download JSON.
- Save the file somewhere safe on your computer — for example:
D:\Blog\client_secrets.json
Step 4: Find Your Blog ID and Post IDs
The script needs your Blog ID and the Post ID of each article you want to update.
Blog ID: Open your Blogger dashboard. The URL will look like this:
blogger.com/u/1/blog/post/list/YOUR_BLOG_ID
The long number in the URL is your Blog ID.
Post ID: Open any post for editing. The URL will look like this:
blogger.com/u/1/blog/post/edit/YOUR_BLOG_ID/YOUR_POST_ID
The second long number is the Post ID for that specific article.
Step 5: Install Required Python Libraries
pip install google-auth google-auth-oauthlib google-api-python-client
If these are already installed, pip will say "Requirement already satisfied" for each one — that is fine, it means you are ready.
Step 6: The Script
The script works in two modes — Test Mode and Live Mode. Always run Test Mode first. It shows a preview of exactly what will be changed without touching any post.
"""
Blogger Series Link Updater - Safe Version
==========================================
Adds a new <li> link to a Blogger series/index box.
TEST_MODE = True - previews 1 post only, no changes.
TEST_MODE = False - patches content only, preserving title, labels, date and search description.
"""
import json
import os
import re
from datetime import datetime
from google_auth_oauthlib.flow import InstalledAppFlow
from googleapiclient.discovery import build
from google.oauth2.credentials import Credentials
from google.auth.transport.requests import Request
# ============================================================
# CONFIGURATION - Edit these values
# ============================================================
TEST_MODE = True
CREDENTIALS_PATH = r"D:\YOUR_FOLDER\client_secrets.json"
TOKEN_PATH = r"D:\YOUR_FOLDER\token.json"
BACKUP_DIR = r"D:\YOUR_FOLDER\blogger_post_backups"
BLOG_ID = "YOUR_BLOG_ID_HERE"
TARGET_POST_IDS = [
"YOUR_POST_ID_PART_1",
"YOUR_POST_ID_PART_2",
"YOUR_POST_ID_PART_3",
]
NEW_LI = '<li><a href="YOUR_NEW_POST_URL" target="_blank" rel="noopener">Your New Post Title: Part XX</a></li>'
# Put only the class name here, without dot.
# Example: if your HTML is <div class="series-link-box">, use:
# SERIES_BOX_CLASS = "series-link-box"
SERIES_BOX_CLASS = "YOUR_SERIES_BOX_CLASS_HERE"
# ============================================================
# OAUTH2 AUTHENTICATION
# ============================================================
SCOPES = ["https://www.googleapis.com/auth/blogger"]
def authenticate():
creds = None
if os.path.exists(TOKEN_PATH):
creds = Credentials.from_authorized_user_file(TOKEN_PATH, SCOPES)
if not creds or not creds.valid:
if creds and creds.expired and creds.refresh_token:
creds.refresh(Request())
else:
flow = InstalledAppFlow.from_client_secrets_file(CREDENTIALS_PATH, SCOPES)
creds = flow.run_local_server(port=0)
with open(TOKEN_PATH, "w", encoding="utf-8") as token:
token.write(creds.to_json())
return creds
# ============================================================
# HELPER FUNCTIONS
# ============================================================
def safe_filename(text):
text = re.sub(r'[\\/:*?"<>|]', "", text)
return text[:60].strip()
def backup_post(post, post_id):
os.makedirs(BACKUP_DIR, exist_ok=True)
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
title = safe_filename(post.get("title", "untitled"))
backup_path = os.path.join(BACKUP_DIR, f"{timestamp}_{post_id}_{title}.json")
with open(backup_path, "w", encoding="utf-8") as f:
json.dump(post, f, ensure_ascii=False, indent=2)
print(f"Backup saved: {backup_path}")
def find_series_box_start(content):
"""
Finds a div whose class contains SERIES_BOX_CLASS.
Works even if the div has multiple classes.
Example:
<div class="series-link-box">
<div class="series-link-box extra-class">
"""
pattern = re.compile(
r'<div\s+[^>]*class=["\'][^"\']*\b' + re.escape(SERIES_BOX_CLASS) + r'\b[^"\']*["\'][^>]*>',
re.IGNORECASE
)
match = pattern.search(content)
if not match:
return -1
return match.start()
def insert_new_li(content):
start = find_series_box_start(content)
if start == -1:
return None, f"Series box class not found: {SERIES_BOX_CLASS}"
ul_end = content.find("</ul>", start)
if ul_end == -1:
return None, "No closing </ul> found after the series box"
box_content = content[start:ul_end]
last_li_pos = box_content.rfind("</li>")
if last_li_pos == -1:
return None, "No </li> found inside the series box"
if NEW_LI[10:80] in content:
return None, "This link already exists in this post"
insert_at = start + last_li_pos + len("</li>")
new_content = content[:insert_at] + "\n " + NEW_LI + content[insert_at:]
return new_content, None
# ============================================================
# CORE LOGIC
# ============================================================
def process_post(service, post_id):
print(f"\n{'=' * 60}")
print(f"Processing Post ID: {post_id}")
post = service.posts().get(
blogId=BLOG_ID,
postId=post_id,
view="ADMIN"
).execute()
title = post.get("title", "Unknown")
labels = post.get("labels", [])
published = post.get("published", "")
search_description = post.get("customMetaData", "")
content = post.get("content", "")
print(f"Title: {title}")
print(f"Published: {published}")
print(f"Labels: {labels}")
new_content, error = insert_new_li(content)
if error:
print(f"SKIPPED: {error}")
print("No patch/update was sent to Blogger.")
return "skipped"
if TEST_MODE:
print("\n--- TEST MODE PREVIEW ---")
idx = new_content.find(NEW_LI[10:80])
if idx > -1:
preview_start = max(0, idx - 200)
preview_end = min(len(new_content), idx + 300)
print(new_content[preview_start:preview_end])
print("\nTEST MODE: No actual changes made.")
return "previewed"
backup_post(post, post_id)
service.posts().patch(
blogId=BLOG_ID,
postId=post_id,
body={
"content": new_content
}
).execute()
print("Patched content successfully.")
verify = service.posts().get(
blogId=BLOG_ID,
postId=post_id,
view="ADMIN"
).execute()
if verify.get("title") != title:
raise RuntimeError("Title changed unexpectedly.")
if verify.get("labels", []) != labels:
raise RuntimeError("Labels changed unexpectedly.")
if verify.get("published", "") != published:
raise RuntimeError("Published date changed unexpectedly.")
if verify.get("customMetaData", "") != search_description:
raise RuntimeError("Search description changed unexpectedly.")
print("Verified: title, labels, search description and published date preserved.")
return "updated"
# ============================================================
# MAIN
# ============================================================
def main():
print("Blogger Series Link Updater - SAFE VERSION")
print(f"Mode: {'TEST - no changes' if TEST_MODE else 'LIVE - patch content only'}")
print(f"Posts listed: {len(TARGET_POST_IDS)}")
creds = authenticate()
service = build("blogger", "v3", credentials=creds)
posts_to_run = TARGET_POST_IDS[:1] if TEST_MODE else TARGET_POST_IDS
previewed = 0
updated = 0
skipped = 0
for post_id in posts_to_run:
result = process_post(service, post_id)
if result == "previewed":
previewed += 1
elif result == "updated":
updated += 1
else:
skipped += 1
print(f"\n{'=' * 60}")
print("Done.")
print(f"Previewed: {previewed}")
print(f"Updated : {updated}")
print(f"Skipped : {skipped}")
if __name__ == "__main__":
main()
What to Replace in the Python Script
Before running the script, replace these placeholder values with your own Blogger and computer details.
| Python Part | Replace With | Example |
|---|---|---|
TEST_MODE = True |
Keep True for the first test run. Change to False only after the preview looks correct. |
TEST_MODE = False |
CREDENTIALS_PATH |
Your downloaded OAuth client JSON file path. | r"D:\YOUR_FOLDER\client_secrets.json" |
TOKEN_PATH |
Where Python should save the login token after first authorization. | r"D:\YOUR_FOLDER\token.json" |
BACKUP_DIR |
Folder where backup copies of posts should be saved before live patching. | r"D:\YOUR_FOLDER\blogger_post_backups" |
BLOG_ID |
Your Blogger blog ID from the Blogger dashboard URL. | BLOG_ID = "1234567890123456789" |
TARGET_POST_IDS |
The post IDs of the Blogger posts you want to update. | ["1111111111111111111", "2222222222222222222"] |
NEW_LI |
The new list item link you want to add to every post. | <li><a href="YOUR_NEW_POST_URL">Your New Post Title</a></li> |
SERIES_BOX_CLASS |
The class name of the div that contains your series/article list. Write only the class name, without a dot. | SERIES_BOX_CLASS = "series-link-box" |
How the Script Works
- Authentication: On first run, a browser window opens asking you to log in to your Google account and authorize the script. After that, a token file is saved locally so you do not need to log in again.
- Fetch post: The script calls the Blogger API to get the full HTML content of each post.
- Find series box: It searches for your series-link-box div using the CSS class name.
- Find last item: Inside the series box, it finds the last </li> tag.
- Insert new link: It inserts the new <li> immediately after the last existing item.
- Duplicate check: If the link already exists in the post, it skips that post automatically.
- Update post: In Live Mode, it pushes the updated HTML back to Blogger via the API.
Test Mode vs Live Mode
| Setting | What Happens | When to Use |
|---|---|---|
| TEST_MODE = True | Fetches 1 post only. Shows preview of change. Makes no updates. | Always run this first |
| TEST_MODE = False | Processes all posts in TARGET_POST_IDS. Updates each post via API. | Only after trial preview looks correct |
How to Run the Script
python blogger_series_updater.py
On first run, a browser window opens for OAuth authorization. Log in with the Google account that owns your blog. After authorization, a token.json file is saved in the same folder. Future runs will not ask for login again unless the token expires.
Common Errors and Fixes
| Error | Cause | Fix |
|---|---|---|
| 403 — API not enabled | Blogger API is disabled in your Google Cloud project | Go to APIs and Services → Library → Enable Blogger API v3. Wait 2-3 minutes. |
| series-link-box not found | That post uses a different class name or structure | Check the post HTML — update SERIES_BOX_START to match your actual class name. |
| New link already exists — skipping | The link was already added to this post | Not an error — script skipped correctly to avoid duplicates. |
| Token expired | OAuth token needs refresh | Delete token.json and run script again — browser will open for re-authorization. |
What to Change Each Time You Add a New Part
Every time you publish a new article in your series, only two things need updating in the script:
- NEW_LI — update with the new post URL and title.
- TARGET_POST_IDS — add the new post's ID to the list (so it gets updated next time too).
Is This Safe for Your Blog?
- The script only modifies the HTML content of posts you explicitly list in TARGET_POST_IDS.
- It does not touch post titles, labels, publish dates, or any other settings.
- The duplicate check means running the script twice will not insert the link twice.
- Test Mode lets you verify everything before any change is made.
- Your credentials file never leaves your computer — it is used locally only.
Final Thought
Blogger does not have dynamic includes or shared components. But the Blogger API gives you everything you need to work around that limitation. A small Python script running locally on your computer can do in seconds what would otherwise take an hour of manual editing.
Everything I Learned — So You Don't Have To Figure It Out Alone
The technical mistakes I made in year one — the full HTML inside Blogger, the missing meta descriptions, the duplicate H1 tags, the links closing articles — I have written all of it down. Every fix. Every discovery. Every hour of confused trial and error turned into a clear guide.
🌿 The WordsByEkta Blogger Technical Series
- Blogger is Underrated & I'm Rooting for It: Part 1
- How to Set Up Your Blogger About Me Page: Part 2
- Google Search Console for Bloggers: Part 3
- How to Request Indexing in GSC: Part 4
- Internal Linking for Fast Indexing: Part 5
- Why Isn't My Blog Indexing?: Part 6
- Canonical Tag Fix for Blogger: Part 7
- The AdSense Locked Widget Hack: Part 8
- Use Pingomatic for Faster Indexing: Part 9
- Decoding GSC Reports: Part 10
- Get Traffic from Bing and Yahoo: Part 11
- The AdSense Checklist: Part 12
- Auto Submit Blogger Posts to Bing: Part 13
- Custom Contact Form for Blogger: Part 14
- Extract Blog Post URLs from Sitemap: Part 15
- Open Links in New Tab Blogger: Part 16
- Blogger HTML Mode SEO Mistakes: Part 17
- Google Takeout Blogger Not Working: Part 18
- Google Indexing API for Blogger Using Python OAuth2: Part 19
- Is Blogger Worth It Nowadays?: Part 20
- Blogger Mobile HTML Editor Trick for Full Code Copy: Part 21
- Claim Blogger Site on Pinterest (No Custom Domain): Part 22
- Follow.it Email Subscriptions Setup on Blogger: Part 23
- How to Exclude Your Own Visits from GA4 Analytics: Part 24
- Auto Update All Blogger Posts Using Python and Blogger API: Part 25
- My Blog Passed 118/118 AdSense Checks: Part 26
- Ad Networks for Blogger Besides AdSense: Part 27
Comments
Post a Comment