backend-manager 5.8.3 → 5.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +26 -0
- package/CLAUDE.md +1 -1
- package/docs/admin-post-route.md +1 -1
- package/docs/ghostii.md +85 -60
- package/docs/marketing-campaigns.md +2 -2
- package/package.json +1 -1
- package/src/manager/events/cron/daily/blog-auto-publisher.js +680 -0
- package/src/manager/libraries/content/feed-parser.js +1 -1
- package/src/manager/libraries/content/ghostii.js +3 -3
- package/src/manager/libraries/email/generators/lib/structure.js +5 -1
- package/src/manager/libraries/email/generators/newsletter.js +47 -46
- package/src/manager/routes/brand/get.js +1 -1
- package/templates/backend-manager-config.json +66 -70
- package/test/helpers/content/blog-auto-publisher.js +484 -0
- package/test/helpers/content/ghostii-feed-integration.js +22 -10
- package/test/helpers/content/ghostii-write-article.js +1 -1
- package/test/routes/test/usage.js +1 -1
- package/src/manager/events/cron/daily/ghostii-auto-publisher.js +0 -513
- package/test/helpers/content/ghostii-auto-publisher.js +0 -349
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,32 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/).
|
|
|
14
14
|
- `Fixed` for any bug fixes.
|
|
15
15
|
- `Security` in case of vulnerabilities.
|
|
16
16
|
|
|
17
|
+
# [5.9.0] - 2026-06-19
|
|
18
|
+
|
|
19
|
+
### BREAKING
|
|
20
|
+
- **Config key `ghostii` renamed to `blog`.** Consumer projects must migrate their `backend-manager-config.json`: `ghostii[]` → `blog.content[]` with `blog.enabled` and `blog.platform: 'ghostii'`.
|
|
21
|
+
- **Field renames in blog content entries.** `articles` → `quantity`, `prompt` → `instructions`, `$app` → `$brand`.
|
|
22
|
+
- **Newsletter `claimSources()` removed.** Parent server now serves sources read-only (no claiming). Children track usage locally in `content-sources`.
|
|
23
|
+
|
|
24
|
+
### Added
|
|
25
|
+
- **Provider-based blog architecture.** `blog.platform` selects the article-generation provider (dispatched via `require(../content/${platform}.js)`). Only `ghostii` for now.
|
|
26
|
+
- **`$parent` source type.** Blog entries can fetch sources from the parent server's pool, filtered by categories, with local dedup tracking.
|
|
27
|
+
- **`$brand` source type.** Renamed from `$app` — generic brand-topic generation.
|
|
28
|
+
- **New content entry fields.** `tone`, `categories`, `keywords` added to both blog and newsletter content entries. Injected into AI prompts for better content targeting.
|
|
29
|
+
- **`links` and `keywords` support in newsletter.** Newsletter AI prompt now weaves in brand links and SEO keywords from content config.
|
|
30
|
+
- **`sources` field on newsletter content.** Newsletter entries now explicitly declare source types (default: `['$parent']`), matching blog config shape.
|
|
31
|
+
- **Newsletter `content` supports array format.** Matches blog config shape for structural parity.
|
|
32
|
+
- **Unified `content-sources` Firestore collection.** Both blog and newsletter track used sources in the same collection with `usedBy` field (`'blog'` or `'newsletter'`).
|
|
33
|
+
|
|
34
|
+
### Changed
|
|
35
|
+
- **Cron renamed.** `ghostii-auto-publisher.js` → `blog-auto-publisher.js`.
|
|
36
|
+
- **`postPath` defaults to platform name.** Instead of hardcoded `'blog'`, defaults to the platform value (e.g. `'ghostii'`).
|
|
37
|
+
- **Consistent field ordering.** `sources → categories → links → instructions → tone → keywords` across blog and newsletter config templates.
|
|
38
|
+
|
|
39
|
+
### Removed
|
|
40
|
+
- **`ghostii-auto-publisher.js`** — replaced by `blog-auto-publisher.js`.
|
|
41
|
+
- **`claimSources()` from newsletter generator** — parent no longer tracks claim state.
|
|
42
|
+
|
|
17
43
|
# [5.8.3] - 2026-06-19
|
|
18
44
|
|
|
19
45
|
### Fixed
|
package/CLAUDE.md
CHANGED
|
@@ -162,7 +162,7 @@ Deep references live in `docs/`. **Whenever you make a behavioral change, update
|
|
|
162
162
|
|
|
163
163
|
### Subsystems & Libraries
|
|
164
164
|
|
|
165
|
-
- [docs/ghostii.md](docs/ghostii.md) — Ghostii
|
|
165
|
+
- [docs/ghostii.md](docs/ghostii.md) — Blog auto-publisher (Ghostii provider): source types (`$brand` / `$feed:` / `$parent` / URL / text), provider-based architecture, per-entry API overrides, RSS/Atom feed parser, unified `content-sources` Firestore tracking, `sourceContent` pass-through to Ghostii API
|
|
166
166
|
- [docs/email-system.md](docs/email-system.md) — unified MJML email rendering pipeline: shared preparation (`prepare.js`), composable template system (`base.js` blocks), 4 email templates (card, plain, order, feedback), no SendGrid dynamic templates — everything rendered server-side
|
|
167
167
|
- [docs/usage-rate-limiting.md](docs/usage-rate-limiting.md) — usage tracking, monthly/daily caps, `setUser()` + mirrors for proxy usage, reset schedule
|
|
168
168
|
- [docs/ai-library.md](docs/ai-library.md) — `Manager.AI()` unified entry for OpenAI + Anthropic (text via `.request()`, images via `.image()` → `gpt-image-2`)
|
package/docs/admin-post-route.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
The `POST /admin/post` route creates blog posts via GitHub's API. It handles image extraction, resize, upload, and body rewriting.
|
|
4
4
|
|
|
5
|
-
**Consumers.** Besides direct admin/blogger HTTP calls, this route is the publish target for the **Ghostii article engine** (`src/manager/libraries/content/ghostii.js` → `publishArticle()`), which is used by two paths: the standalone daily `
|
|
5
|
+
**Consumers.** Besides direct admin/blogger HTTP calls, this route is the publish target for the **Ghostii article engine** (`src/manager/libraries/content/ghostii.js` → `publishArticle()`), which is used by two paths: the standalone daily `blog-auto-publisher.js` cron (off by default), and the newsletter generator's linked-article flow (`marketing.newsletter.content.article.enabled`). Both POST `title`/`url`/`description`/`headerImageURL`/`body`/`author`/`categories`/`tags`/`postPath`/`githubUser`/`githubRepo` with a `backendManagerKey`. Note: `headerImageURL` MUST resolve to a `.jpg` (the downloader rejects non-`.jpg` headers); Ghostii's `unsplash` hero satisfies this. See [docs/marketing-campaigns.md](marketing-campaigns.md).
|
|
6
6
|
|
|
7
7
|
**Ghostii is unopinionated about BEM.** Its `/write/article` response is a generic article — a `json` block array (`[{ name, content }]` where name ∈ `heading-1..6`/`image`/`paragraph`/`blockquote`/`list`) plus top-level `title`/`description`/`headerImageUrl`/`images`/`categories`/`keywords`. BEM owns the transform into this route's shape: `blocksToPost(article.json)` extracts the `heading-1` as the title, the first `image` block as `headerImageURL`, and joins every remaining block as the `body` (content only — NO title, NO header image embedded, since this route adds those itself; section images stay in the body and are extracted normally). Older Ghostii responses without `json` fall back to the flat `article.{title,body,headerImageUrl}` fields.
|
|
8
8
|
|
package/docs/ghostii.md
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
# Ghostii
|
|
1
|
+
# Blog Auto-Publisher (Ghostii Provider)
|
|
2
2
|
|
|
3
|
-
The
|
|
3
|
+
The blog system generates and publishes blog posts via the daily `bm_cronDaily` cron job. Two independent paths:
|
|
4
4
|
|
|
5
|
-
1. **Standalone publisher** — `config.
|
|
5
|
+
1. **Standalone publisher** — `config.blog` with `enabled: true`: daily cron picks sources, calls the configured platform provider (default: Ghostii AI API), publishes via `admin/post`.
|
|
6
6
|
2. **Newsletter-linked articles** — `marketing.newsletter.content.article.enabled`: the newsletter generator expands its lead section into a full blog post via Ghostii.
|
|
7
7
|
|
|
8
8
|
This doc covers the standalone publisher. For newsletter integration, see [marketing-campaigns.md](marketing-campaigns.md).
|
|
@@ -10,59 +10,71 @@ This doc covers the standalone publisher. For newsletter integration, see [marke
|
|
|
10
10
|
## Architecture
|
|
11
11
|
|
|
12
12
|
```
|
|
13
|
-
config.
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
resolveSource()
|
|
18
|
-
|
|
19
|
-
writeArticle()
|
|
20
|
-
|
|
21
|
-
publishArticle()
|
|
22
|
-
|
|
23
|
-
|
|
13
|
+
config.blog.content[] entry
|
|
14
|
+
|
|
|
15
|
+
blog-auto-publisher cron (daily)
|
|
16
|
+
| (per entry, per article)
|
|
17
|
+
resolveSource() -- detect source type, fetch/parse if needed
|
|
18
|
+
|
|
|
19
|
+
provider.writeArticle() -- call provider API (e.g. Ghostii: api.ghostii.ai/write/article)
|
|
20
|
+
|
|
|
21
|
+
provider.publishArticle() -- POST to admin/post -> GitHub commit
|
|
22
|
+
|
|
|
23
|
+
trackContentSource() -- Firestore (for $feed:, $parent sources)
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
### Key files
|
|
27
27
|
|
|
28
28
|
| File | Purpose |
|
|
29
29
|
|---|---|
|
|
30
|
-
| `src/manager/libraries/content/ghostii.js` | `writeArticle()`, `publishArticle()`, `blocksToPost()`
|
|
31
|
-
| `src/manager/libraries/content/feed-parser.js` | `parseFeed()`, `extractArticleContent()`
|
|
32
|
-
| `src/manager/events/cron/daily/
|
|
30
|
+
| `src/manager/libraries/content/ghostii.js` | `writeArticle()`, `publishArticle()`, `blocksToPost()` -- Ghostii API client + post transform |
|
|
31
|
+
| `src/manager/libraries/content/feed-parser.js` | `parseFeed()`, `extractArticleContent()` -- RSS/Atom/JSON parser + article extractor |
|
|
32
|
+
| `src/manager/events/cron/daily/blog-auto-publisher.js` | Daily cron: source resolution, feed processing, Firestore tracking, provider dispatch |
|
|
33
33
|
| `src/manager/routes/admin/post/post.js` | Publishing endpoint: image download + resize, GitHub commit |
|
|
34
34
|
|
|
35
35
|
## Source Types
|
|
36
36
|
|
|
37
|
-
Each `
|
|
37
|
+
Each `blog.content[]` entry has a `sources` array. The cron picks one at random per article.
|
|
38
38
|
|
|
39
39
|
| Source | Format | Behavior |
|
|
40
40
|
|---|---|---|
|
|
41
|
-
| Generic topic | `'$
|
|
42
|
-
| RSS/Atom feed | `'$feed:https://example.com/feed/'` | Parses feed, picks one unprocessed article, extracts content, passes as `sourceContent` to
|
|
41
|
+
| Generic topic | `'$brand'` | Prompts the provider to write about any topic relevant to the brand |
|
|
42
|
+
| RSS/Atom feed | `'$feed:https://example.com/feed/'` | Parses feed, picks one unprocessed article, extracts content, passes as `sourceContent` to the provider |
|
|
43
|
+
| Parent server | `'$parent'` | Fetches sources from parent server's source pool (without claiming), filters out already-used sources locally |
|
|
43
44
|
| URL | `'https://example.com/page'` | Fetches page HTML, extracts body text, uses as prompt seed |
|
|
44
45
|
| Text | `'Write about blockchain'` | Uses directly as prompt seed |
|
|
45
46
|
|
|
46
47
|
### Feed source flow
|
|
47
48
|
|
|
48
49
|
1. Fetch and parse the RSS/Atom/JSON feed via `feed-parser.parseFeed()`
|
|
49
|
-
2. Query `
|
|
50
|
+
2. Query `content-sources` in Firestore for already-processed items
|
|
50
51
|
3. Filter to unprocessed items, pick the newest
|
|
51
52
|
4. Extract full article content from the item URL via `feed-parser.extractArticleContent()`
|
|
52
|
-
5. Pass extracted text as `sourceContent` to the
|
|
53
|
-
6. After publish, write a tracking doc to `
|
|
54
|
-
7. On failure (feed unreachable, unparseable, exhausted): fall back to `$
|
|
53
|
+
5. Pass extracted text as `sourceContent` to the provider API (separate from the `description` prompt)
|
|
54
|
+
6. After publish, write a tracking doc to `content-sources/{hash}`
|
|
55
|
+
7. On failure (feed unreachable, unparseable, exhausted): fall back to `$brand` behavior
|
|
55
56
|
|
|
56
|
-
###
|
|
57
|
+
### Parent source flow
|
|
57
58
|
|
|
58
|
-
|
|
59
|
+
1. Fetch available sources from parent server via `GET {parentUrl}/newsletter-sources` (no `claimFor` -- read-only)
|
|
60
|
+
2. Query `content-sources` in Firestore for already-used sources with `origin: '$parent'`
|
|
61
|
+
3. Filter out already-used sources, pick the first available
|
|
62
|
+
4. On failure (no parent URL, no sources, all exhausted): fall back to `$brand` behavior
|
|
63
|
+
|
|
64
|
+
### Content source tracking
|
|
65
|
+
|
|
66
|
+
Collection: `content-sources` (consumer project Firestore)
|
|
67
|
+
|
|
68
|
+
Unified tracker for all source types -- shared between blog-auto-publisher and newsletter generator.
|
|
59
69
|
|
|
60
70
|
```js
|
|
61
|
-
|
|
62
|
-
|
|
71
|
+
content-sources/{sha256(origin + '::' + url).slice(0,20)}: {
|
|
72
|
+
url: 'https://...', // unique identifier (article URL or source ID)
|
|
73
|
+
origin: '$feed:https://...', // or '$parent', '$brand'
|
|
74
|
+
feedUrl: 'https://...', // for feed sources
|
|
63
75
|
itemId: 'guid-or-url',
|
|
64
|
-
itemUrl: 'https://...',
|
|
65
76
|
itemTitle: '...',
|
|
77
|
+
usedBy: 'blog', // or 'newsletter'
|
|
66
78
|
brandId: '...',
|
|
67
79
|
postUrl: '...',
|
|
68
80
|
postSlug: '...',
|
|
@@ -70,7 +82,6 @@ ghostii-sources/{sha256(feedUrl + '::' + itemId).slice(0,20)}: {
|
|
|
70
82
|
created: { timestamp: '...ISO...', timestampUNIX: 1234567890 },
|
|
71
83
|
updated: { timestamp: '...ISO...', timestampUNIX: 1234567890 },
|
|
72
84
|
},
|
|
73
|
-
metadata: { created: Timestamp },
|
|
74
85
|
}
|
|
75
86
|
```
|
|
76
87
|
|
|
@@ -78,53 +89,67 @@ ghostii-sources/{sha256(feedUrl + '::' + itemId).slice(0,20)}: {
|
|
|
78
89
|
|
|
79
90
|
```js
|
|
80
91
|
// backend-manager-config.json
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
//
|
|
95
|
-
//
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
92
|
+
blog: {
|
|
93
|
+
enabled: false,
|
|
94
|
+
platform: 'ghostii', // provider: only 'ghostii' for now
|
|
95
|
+
content: [
|
|
96
|
+
{
|
|
97
|
+
quantity: 1, // articles per cron run (0 = disabled)
|
|
98
|
+
sources: ['$brand'], // source pool (mixed types OK)
|
|
99
|
+
links: [], // URLs to inject into article body
|
|
100
|
+
instructions: '', // custom instructions for the AI
|
|
101
|
+
tone: 'professional', // 'professional', 'casual', 'actionable', etc.
|
|
102
|
+
categories: [], // topic constraints + output tags
|
|
103
|
+
keywords: [], // SEO keywords to weave in
|
|
104
|
+
chance: 1.0, // probability of running (0-1)
|
|
105
|
+
author: null, // author slug (null = admin/post default)
|
|
106
|
+
postPath: 'blog', // sub-folder under _posts/{year}/
|
|
107
|
+
overrides: { // per-entry Ghostii API param overrides
|
|
108
|
+
// keywords: ['AI'],
|
|
109
|
+
// length: 'long', // short | medium | long | comprehensive
|
|
110
|
+
// research: true, // web search for real links
|
|
111
|
+
// insertImages: true,
|
|
112
|
+
// insertLinks: true,
|
|
113
|
+
// headerImageUrl: 'unsplash', // disabled | unsplash | generate
|
|
114
|
+
// maxLinks: 6,
|
|
115
|
+
// sectionQuantity: 5,
|
|
116
|
+
// feedUrl: '', // brand blog feed (title dedup)
|
|
117
|
+
},
|
|
118
|
+
// brand: 'other-brand-id', // cross-brand publishing
|
|
119
|
+
// brandUrl: 'https://api.other.com',
|
|
120
|
+
}
|
|
121
|
+
],
|
|
122
|
+
}
|
|
105
123
|
```
|
|
106
124
|
|
|
107
125
|
### Per-entry overrides
|
|
108
126
|
|
|
109
127
|
All Ghostii API params were previously hardcoded in `writeArticle()`. They now accept per-entry overrides via the `overrides` object. Unspecified values fall back to framework defaults (length: 'long', research: true, etc.).
|
|
110
128
|
|
|
111
|
-
###
|
|
129
|
+
### Keywords resolution
|
|
130
|
+
|
|
131
|
+
Keywords are resolved in this order:
|
|
132
|
+
1. `overrides.keywords` -- per-entry Ghostii API override (highest priority)
|
|
133
|
+
2. `entry.keywords` -- top-level entry keywords (used if overrides.keywords is not set)
|
|
134
|
+
3. Empty array -- framework default
|
|
135
|
+
|
|
136
|
+
### `sourceContent` -- Ghostii API field
|
|
112
137
|
|
|
113
|
-
When a `$feed:` source provides extracted article text, it's passed to the Ghostii API as `sourceContent` (max 16KB). This is separate from `description` (the editorial brief, max 2KB). Ghostii's title generation, outline, and section writing prompts reference the source content as context for writing original articles
|
|
138
|
+
When a `$feed:` or `$parent` source provides extracted article text, it's passed to the Ghostii API as `sourceContent` (max 16KB). This is separate from `description` (the editorial brief, max 2KB). Ghostii's title generation, outline, and section writing prompts reference the source content as context for writing original articles -- not rewrites.
|
|
114
139
|
|
|
115
140
|
## Feed Parser
|
|
116
141
|
|
|
117
142
|
`src/manager/libraries/content/feed-parser.js` exports:
|
|
118
143
|
|
|
119
|
-
- **`parseFeed(text)`**
|
|
144
|
+
- **`parseFeed(text)`** -- Parse RSS 2.0, Atom 1.0, or JSON Feed into `{ items: [{ id, title, url, summary, content, publishedAt }] }`. Handles CDATA, namespaced elements (`content:encoded`), BOM, attribute-based links. Returns `{ items: [] }` on invalid input.
|
|
120
145
|
|
|
121
|
-
- **`extractArticleContent(url)`**
|
|
146
|
+
- **`extractArticleContent(url)`** -- Fetch URL, extract readable text from `<article>` -> `<main>` -> `<body>`, strip scripts/styles/nav/footer/header/aside, normalize whitespace, truncate to 14KB.
|
|
122
147
|
|
|
123
148
|
## Tests
|
|
124
149
|
|
|
125
150
|
```bash
|
|
126
|
-
npx mgr test bem:helpers/content/feed-parser
|
|
151
|
+
npx mgr test bem:helpers/content/feed-parser # 32 tests: RSS/Atom/JSON parsing, edge cases, HTML extraction
|
|
127
152
|
npx mgr test bem:helpers/content/ghostii-write-article # 15 tests: override pass-through, sourceContent, backwards compat
|
|
128
|
-
npx mgr test bem:helpers/content/
|
|
153
|
+
npx mgr test bem:helpers/content/blog-auto-publisher # 25+ tests: source detection, feed processing, Firestore tracking
|
|
129
154
|
npx mgr test bem:helpers/content/ghostii-blocks # 8 tests: blocksToPost() (unchanged)
|
|
130
155
|
```
|
|
@@ -174,7 +174,7 @@ Flow (runs in parallel with SVG generation, between structure and render):
|
|
|
174
174
|
|
|
175
175
|
**Failure is isolated** — if the article build throws, it resolves to `null`, no CTA is injected, and the newsletter ships normally. The newsletter never depends on the article succeeding.
|
|
176
176
|
|
|
177
|
-
The published URL is surfaced on the return as `assets.articleUrl` and `meta.article`. Config block: `marketing.newsletter.content.article = { enabled, author }` (`enabled` default `false`; `author` is the post author slug). Standalone article publishing (independent of the newsletter)
|
|
177
|
+
The published URL is surfaced on the return as `assets.articleUrl` and `meta.article`. Config block: `marketing.newsletter.content.article = { enabled, author }` (`enabled` default `false`; `author` is the post author slug). Standalone article publishing (independent of the newsletter) lives in the daily `blog-auto-publisher.js` cron, which uses the same `libraries/content/ghostii.js` engine — see [docs/admin-post-route.md](admin-post-route.md). Standalone blog publishing is **disabled by default** (`blog.enabled: false`).
|
|
178
178
|
|
|
179
179
|
## Template-owned schemas
|
|
180
180
|
|
|
@@ -360,7 +360,7 @@ marketing: {
|
|
|
360
360
|
| Newsletter asset host (GitHub upload — PNGs + newsletter.html + newsletter.md + summary.md) | `src/manager/libraries/email/generators/lib/image-host.js` |
|
|
361
361
|
| Newsletter markdown renderer (programmatic, no AI) | `src/manager/libraries/email/generators/lib/markdown-renderer.js` |
|
|
362
362
|
| Ghostii article engine (writeArticle + publishArticle) | `src/manager/libraries/content/ghostii.js` |
|
|
363
|
-
| Standalone
|
|
363
|
+
| Standalone blog article cron (off by default) | `src/manager/events/cron/daily/blog-auto-publisher.js` |
|
|
364
364
|
| Unified AI library | `src/manager/libraries/ai/index.js` (OpenAI + Anthropic via `Manager.AI(assistant).request({ provider, ... })`) |
|
|
365
365
|
| Notification library | `src/manager/libraries/notification.js` |
|
|
366
366
|
| SendGrid provider | `src/manager/libraries/email/providers/sendgrid.js` |
|