Install
This is the repository archive. It installs, but the folder name will carry the branch suffix and updates will not flow.
Install straight from GitHub with WP-CLI:
wp plugin install https://github.com/dsmacleod/bdn-live-blog/archive/refs/heads/main.zipA live blog plugin for the Bangor Daily News. Built for WordPress 6.x with the block editor. Reporters enable live blogging on a story with one toggle, then post, edit, and delete entries directly from the public story URL — no WP admin required during coverage.
What this is, in plain English
This is a WordPress plugin that turns any normal BDN article into a live blog — the kind of running, time-stamped feed you see on The New York Times or AP during breaking news, elections, storms, or court proceedings.
A reporter in the field can:
- Post short updates directly from the published story page on their phone or laptop — no WordPress admin screen, no separate app
- Attach a photo, embed a tweet or YouTube clip, or add a "BREAKING" / "UPDATE" label
- Mark certain updates as "key moments" so readers can catch up quickly
Readers on bangordailynews.com see those updates appear at the top of the article automatically, refreshing every 15 seconds without a page reload — the same way a Twitter/X feed pushes new posts in.
Each individual update also gets its own permanent, SEO-friendly URL (for example, bangordailynews.com/2026/04/20/liveblog/mills-vetoes-tax-bill/) so it can be shared on social media or indexed by Google News on its own.
Inputs and outputs (how it works for each user)
User A — the reporter in the field
Input: On the published BDN story page (while logged in), a red-topped composer panel appears above the live feed. The reporter types a headline and body text into a textbox, optionally drops in a photo or a tweet URL, picks a label like "BREAKING," and clicks Publish entry.
Output: The new update appears at the top of the live feed on that same page immediately, stamped with the reporter's byline, headshot, and timestamp. No page reload. A permanent shareable URL for that single update is generated automatically in the background (for example, /2026/04/20/liveblog/mills-vetoes-tax-bill/).
User B — the reader on bangordailynews.com
Input: The reader visits a normal BDN article URL that the reporter has flagged as a live blog.
Output: They see a running feed of time-stamped updates from newest to oldest, with photos, tweets, and videos embedded inline. The feed refreshes automatically every 15 seconds — new updates slide in without a page reload. A pulsing red "LIVE" dot shows at the top of the feed while the blog is active. Readers can toggle between an "All updates" view and a "Key moments" view to catch up on just the highlights. Once three or more updates are posted, a plain-English "Story so far" summary appears at the top, generated by the newsroom's NOTA AI tool.
User C — the editor or desk
Input: One toggle in the block editor sidebar to turn live blogging on for a story, and a status dropdown (Live / Scheduled / Ended).
Output: Everything else — status banners, the composer panel, SEO metadata, individual entry URLs, the "Story so far" summary — is handled automatically. The editor can end the live blog from the composer on the public page without going back into WP admin.
Visual walk-through
No screenshots are committed to this repo yet. The dev site with the plugin installed is the fastest way to see it in action: open any story on the dev site, toggle live blog on in the block editor sidebar, then view the published story URL while logged in to see the composer. The reader view is what the same URL shows to a logged-out user.
What's new in 1.2.7
Bug-fix bundle focused on the composer. These were found and verified end-to-end against Newspack staging with a Playwright harness (tests/browser/).
- Inline photo upload works under Newspack hardening —
/wp/v2/mediais blocked for Editors on hardened Newspack installs with a 403 "not allowed to create posts as this user," even when the same user can upload through the Media Library modal. The plugin now owns aPOST /wp-json/bdn-liveblog/v1/upload-inline-imageendpoint that gates on the sameedit_postscheck as every other write. JPEG/PNG/GIF/WebP accepted. - Composer entries render their photo — the reader widget has always shown the featured image on an entry; the editor-side entry list was silently dropping it. The composer list now renders the figure (and caption/credit) so you can confirm what was attached.
- Timestamp + byline moved below the body — the top meta row kept only the label/pin badge. Timestamp and byline now appear in a footer row under the entry body for a scan-friendlier feed.
- "Use this photo" button visible on Newspack — the media modal was rendering with an extra
.search-formclass leaking onto.media-toolbar-primary, which triggered the theme's absolute-positioned search-submit styling and pushed the primary action button out of the modal. Scoped CSS override neutralizes those rules inside.media-modalonly. - Cache poisoning fixed — the
/entriesREST endpoint previously keyed its 30s transient onpost_id+pageonly. A reader hitting the "Key moments" tab and a reader loading the full feed would race for the same cache key, so within the TTL window different readers could see different subsets. Cache key now includeshighlights_only; every variant is tracked in an index so writes invalidate all of them. AddedCache-Control: no-storeon the response so upstream caches can't replay one reader's view to another. - Link button inserts into the editor, not next to the input — when the toolbar button was clicked without a saved selection, the generated
<a>was being inserted into the link bar wrapper instead of the editor. Switched tomousedown+preventDefaultso the editor keeps focus, and fall back to placing the caret at the end of the editor if nothing is saved. - Media modal diagnostics — when the WP media frame opens, a
[BDN Liveblog] media modal diagnosticsgroup is logged to DevTools console with modal/toolbar/button rects, computed styles, and the topmost element at the button center. Makes future theme-CSS conflicts quick to diagnose. - Fixed a syntax error shipped in 1.2.4 — an earlier diagnostics patch left the
mediaFrame.on('select', ...)handler dangling, producingUnexpected token ')'at parse time. When the script fails to parse, the entire IIFE fails to run, which means no composer, no photo button, no link button, no REST polling. If you ever see multiple unrelated composer features break at once, suspect a parse error first (check DevTools console).
What's new in 1.2.1
- Typography refresh — entries now render in Libre Franklin (loaded from Google Fonts) across the whole widget for a cleaner, more consistent look. Previous releases mixed Publico Headline, Charter, and Helvetica Neue; the widget now uses a single family.
- AP-style timestamps — each entry now shows a single inline timestamp in AP style (for example,
April 20, 2026, 3:15 p.m.) instead of the previous two-column clock layout. The left time gutter is gone; the timestamp now sits flush with the headline. - Mobile-tighter layout — entry padding reduced slightly and the headline now uses the same UI font at a smaller weight for a denser, scannable feed.
Features
- Front-end composer — logged-in reporters write and publish entries from the live story page, not the WP admin
- Photos — attach images via the WP Media Library or a direct upload from any device; add caption and photo credit per entry
- Video and embeds — paste a YouTube, Twitter/X, Vimeo, Instagram, TikTok, or Facebook URL in the composer and see a live preview; renders as an embedded player for readers
- Key moments — reporters mark entries as highlights; readers toggle between "All updates" and "Key moments" tabs to catch up quickly on long-running blogs
- AI-generated SEO slugs — each entry gets its own canonical URL at
bangordailynews.com/YYYY/MM/DD/liveblog/descriptive-slug/generated by Claude (configurable model), with a keyword-extraction fallback if no API key is set - Full SEO metadata — every entry page includes canonical tags, Open Graph, Twitter Card, and Schema.org
NewsArticle/LiveBlogPostingmarkup - Byline photos — reporters upload their headshots once in their WP profile; no Gravatar account needed
- Live polling — readers receive new entries automatically every 15 seconds while the blog is live; exponential backoff on connection failures with a visible "Connection lost" notice
- Status control — flip between Live, Scheduled, and Ended from the composer header; no admin visit needed to end a blog
- Mobile-responsive — entries stack on small screens, time column hides, composer fields adapt at 640px
- NOTA AI enrichment — auto-generates SEO meta descriptions, keywords, and entities for each entry page on publish
- Story so far — NOTA generates a narrative summary from all entries, shown to readers who arrive late; appears automatically once 3+ entries are published
- Smart slug fallback — slug generation tries Claude first, then NOTA SUM, then local keyword extraction
- BDN design — sharp corners, Publico Headline / Georgia type, BDN green author names, BDN red accents; matches the existing article page exactly
Requirements
- WordPress 6.0 or later
- Block editor (Gutenberg) enabled
- PHP 7.4 or later
- An Anthropic API key (optional — enables AI slug generation; falls back gracefully without one)
Installation
- In WP Admin go to Plugins → Add New → Upload Plugin
- Upload
bdn-liveblog.zipand click Install Now - Click Activate Plugin
WordPress registers the custom post type and flushes rewrite rules automatically on activation.
If entry URLs return 404 after activation: go to Settings → Live Blog and click Flush Rewrite Rules. This is occasionally needed depending on the server environment and only needs to be done once.
Setup (one time)
1. Add your Anthropic API key
Go to Settings → Live Blog and paste in an Anthropic API key from console.anthropic.com. This enables AI-generated URL slugs. The plugin works without it — slugs fall back to keyword extraction from the entry content.
You can also change the Claude Model on this page. The default is claude-haiku-4-5-20251001. Change it to use a different model for slug generation without editing code.
2. Set up reporter profiles
Each reporter should do this once before their first live blog:
- Go to Users → Edit Profile (or Users → Your Profile for their own account)
- Scroll to the BDN Live Blog Byline section at the bottom
- Set their Byline name — this overrides their WordPress display name on entries (leave blank to use their WP display name)
- Click Upload photo to select or upload their headshot from the Media Library
Reporters who skip this step will appear under their WordPress display name with no photo. The headshot also replaces the default Gravatar avatar site-wide.
Using the plugin
Enabling live blogging on a story
- Open the story in the block editor
- In the Document sidebar (right panel), find the Live Blog panel
- Toggle Enable live blog on — status auto-sets to Live
- Set the status if needed: Live, Scheduled, or Ended
- Publish or update the story as normal
That's the only time anyone needs to be in the WP admin for a live blog.
Posting entries from the story page
Navigate to the published story URL while logged in with edit_posts capability. A red-topped composer panel appears above the live feed, invisible to readers.
To publish an entry:
- Type a headline (optional — entries without headlines are common for quick updates)
- Write the entry text in the body field
- Optionally click Add photo to open the Media Library — use the Upload Files tab to upload from a device, or select an existing image; add a caption and photo credit if needed
- To embed a video or tweet, paste a URL (YouTube, Twitter/X, Vimeo, etc.) directly into the body field — a live preview appears in the editor
- Set a byline override if the entry is credited to someone other than your profile name (leave blank to use your profile)
- Set a label if appropriate — e.g.
BREAKING,UPDATE,DEVELOPING - The URL preview shows the AI-generated slug path as you type
- Click Publish entry — it appears in the reader feed immediately, no page reload
To edit an entry:
Click Edit on any entry in the published entries list below the composer. The form pre-fills with the existing content, photo, and metadata. Make changes and click Save changes, or click Cancel to discard.
To delete an entry:
Click Delete on any entry. A confirmation prompt appears before deletion. Deleted entries are removed from the reader feed immediately.
To regenerate a slug:
Click ↻ Slug on any entry to force a fresh AI-generated URL slug — useful after significantly editing an entry's content. Rate-limited to one regeneration per 30 seconds per entry.
To mark a key moment:
Click ☆ Key moment on any entry in the composer's published list. The star fills in and readers see the entry highlighted with a gold border. Readers can switch to the "Key moments" tab to see only highlighted entries — useful for catching up on long-running blogs.
Ending a live blog
Change the status dropdown in the composer header from Live to Ended. The pulsing red dot stops for readers, the status line updates, and automatic polling stops. No WP admin visit required.
How entry URLs work
Each entry gets a permanent canonical URL:
bangordailynews.com/2025/03/24/liveblog/shah-maine-cdc-coronavirus-briefing/
The slug is generated by Claude Haiku (4–8 keyword-rich words, under 60 characters) immediately when the entry is published, then cached. The entry also retains a #entry-{ID} anchor on the parent article URL for backward-compatible fragment linking.
Each entry page includes:
<link rel="canonical">pointing to its own URL- Full Open Graph and Twitter Card meta tags
NewsArticleSchema.org JSON-LD withisPartOf: LiveBlogPostinglinking back to the parent story
File structure
bdn-liveblog/
├── bdn-liveblog.php Main plugin file
├── README.md This file
├── admin/
│ ├── class-bdn-liveblog-admin.php Settings page + Classic Editor meta box
│ ├── editor-panel.js Gutenberg sidebar panel (toggle + status)
│ ├── admin.js Classic Editor composer fallback
│ └── admin.css Admin styles
├── includes/
│ ├── class-bdn-liveblog-post-type.php Custom post type + meta registration
│ ├── class-bdn-liveblog-api.php REST API endpoints
│ ├── class-bdn-liveblog-slug.php AI slug generation (Anthropic API)
│ ├── class-bdn-liveblog-rewrite.php URL routing + SEO meta for entry pages
│ ├── class-bdn-liveblog-shortcode.php [bdn_liveblog] shortcode + composer root
│ └── class-bdn-liveblog-profiles.php Reporter byline name + photo management
└── public/
├── css/liveblog.css Front-end styles (reader feed + composer)
└── js/liveblog.js Reader polling + front-end composer
REST API
The plugin exposes endpoints under /wp-json/bdn-liveblog/v1/:
| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /entries?post_id=X |
public | List entries for a post (paginated, 20/page, cached 30s) |
| GET | /entries?post_id=X&after=TIMESTAMP |
public | Poll for new entries since a Unix timestamp |
| GET | /entries?post_id=X&highlights_only=1 |
public | List only highlighted (key moment) entries |
| POST | /entries |
editor | Create an entry |
| GET | /entries/{id} |
public | Get a single entry |
| POST | /entries/{id} |
editor | Update an entry |
| DELETE | /entries/{id} |
editor | Delete an entry |
| POST | /entries/{id}/regenerate-slug |
editor | Force a new AI slug |
| GET | /status?post_id=X |
public | Get live status for a post |
| POST | /status |
editor | Set live status |
| GET | /me |
editor | Get current reporter's byline name and photo |
| GET | /summary?post_id=X |
public | NOTA-generated story-so-far summary |
"Editor" means any WordPress user with the edit_posts capability.
Troubleshooting
Entry URLs return 404 Go to Settings → Live Blog → Flush Rewrite Rules.
Composer doesn't appear on the story page
Confirm you are logged in to WordPress with edit_posts capability and that live blogging is enabled on the post (block editor sidebar toggle).
Photos don't appear in the composer
The WP Media Library requires wp_enqueue_media() to be loaded. The plugin loads this automatically for logged-in editors; if it's missing, check that no other plugin or theme is dequeuing WP media scripts on the front end.
AI slugs aren't being generated
Check that your Anthropic API key is saved correctly at Settings → Live Blog. The plugin logs API errors to the PHP error log under the prefix BDN LiveBlog slug API error:.
Byline photo not showing
The reporter needs to upload their photo at Users → Edit Profile → BDN Live Blog Byline. The photo is stored as a WP media attachment — confirm the attachment ID is saved correctly in user meta _bdn_byline_photo.
Notes for developers
- Entry content is stored in the
bdn_lb_entrycustom post type, not post meta, so it benefits from WordPress's native revision and caching systems. - The plugin hooks into
the_contentfilter to auto-inject the live blog below post content when_bdn_liveblog_enabledis true. Remove this hook if you prefer to place the[bdn_liveblog]shortcode manually. - Entry content is rendered via a lightweight
render_entry_content()method instead ofapply_filters('the_content', ...)to avoid oEmbed timeouts and filter recursion. It runs$wp_embed->autoembed(),wptexturize,wpautop,shortcode_unautop,wp_filter_content_tags,do_shortcode, andconvert_smilies. - GET
/entriesresponses are cached in WordPress transients (30-second TTL). Cache is busted automatically on create, update, or delete. - Poll interval defaults to 15 seconds with exponential backoff on failure (15s → 30s → 60s → 120s max). Override the base interval via the
BDN_LB.poll_intervalJS variable (in milliseconds) by filteringbdn_liveblog_js_configor re-localizing the script. - Slug regeneration is rate-limited to one API call per 30 seconds per entry via a WordPress transient.
- All REST responses include
entry_url,anchor_url,seo_slug, andhighlightfields so external consumers (apps, aggregators) can link to individual entries and filter key moments. - NOTA SUM API integration reads credentials from the existing Nota WordPress plugin options (
nota_api_url,nota_api_key). No additional configuration needed if the Nota plugin is already installed. - On entry publish, the plugin calls NOTA for meta descriptions, keywords, and entities. Results are cached in post meta and only generated once per entry.
- The
/summaryendpoint aggregates all entries and generates a narrative via NOTA, cached for 5 minutes.