Update Blogger Posts with Python & API: Part 25 — WordsByEkta🌿

Python Blogger API Automation

How to Update All Blogger Posts Automatically Using Python and Blogger API

WordsByEkta🌿 · Blogger Technical Series · Part 25

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.

Cinematic illustration of a glowing Python terminal with teal code lines and a robotic arm of light touching a row of blog post cards all updating simultaneously with green checkmarks, representing automated Blogger post updates using Python and the Blogger API — WordsByEkta
One script. One run. All 25 posts updated — no manual editing required.

This article explains how to automate that entire process using Python and the Blogger API. One script. One run. All posts updated.

This is a real automation I built and tested for this very series. The script fetches each post via the Blogger API, finds the series link box in the HTML, inserts the new link, and pushes the updated content back — exactly like a manual edit, but without touching a single post yourself.

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.

  1. Go to console.cloud.google.com.
  2. Click New Project.
  3. Give it any name — for example, BloggerAutomation.
  4. 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.

  1. Inside your Google Cloud project, go to APIs and Services → Library.
  2. Search for Blogger API v3.
  3. Click on it and click Enable.
  4. Wait 2-3 minutes for it to activate.
If you enable the API and run the script immediately, you may still get a 403 error. Wait a few minutes before running. Google needs time to propagate the change across its systems.

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.

  1. Go to APIs and Services → Credentials.
  2. Click Create Credentials → OAuth client ID.
  3. If prompted, configure the OAuth consent screen first — choose External, fill in app name and email, save.
  4. For application type, choose Desktop app.
  5. Click Create.
  6. Click Download JSON.
  7. Save the file somewhere safe on your computer — for example: D:\Blog\client_secrets.json
Never share this JSON file with anyone. It contains your OAuth client ID and secret. Keep it on your local machine only.

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 Dashboard URL Format
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 Post Edit URL Format
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

Install Command
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_updater.py
""" 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

  1. 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.
  2. Fetch post: The script calls the Blogger API to get the full HTML content of each post.
  3. Find series box: It searches for your series-link-box div using the CSS class name.
  4. Find last item: Inside the series box, it finds the last </li> tag.
  5. Insert new link: It inserts the new <li> immediately after the last existing item.
  6. Duplicate check: If the link already exists in the post, it skips that post automatically.
  7. 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
Do not skip the trial run. The Blogger API update is permanent and immediate. There is no undo button. Always check the preview first.

How to Run the Script

Run Command
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).
Once the script is set up, adding a new part to all existing articles takes less than 2 minutes — run trial, confirm, run live. Done.

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.

The best automation is the one that solves a real problem you already have. If you run a series on Blogger, this script solves exactly that.

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.

📊 SEO Health Checker
Analyze your website's SEO basics instantly. Check titles, meta descriptions, headings, indexing signals and overall SEO health using our free browser-based SEO tool.
Open Free SEO Health Checker
Share on WhatsApp

Comments

Popular Posts

Stop Uploading PDFs Online — Unlock Them Yourself — WordsByEkta🌿

Publish Your Android App on Google Play Store — WordsByEkta🌿

How to Set Up Your Blogger About Me Page: Part 02 — WordsByEkta🌿