marketing-cli 0.1.0 → 0.2.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/README.md +72 -76
- package/catalogs-manifest.json +24 -0
- package/dist/cli.js +1186 -153
- package/package.json +6 -5
- package/skills/cmo/SKILL.md +73 -7
- package/skills/cmo/rules/brand-file-map.md +193 -0
- package/skills/cmo/rules/command-reference.md +143 -0
- package/skills/cmo/rules/context-switch.md +34 -0
- package/skills/cmo/rules/ecosystem.md +127 -0
- package/skills/cmo/rules/error-recovery.md +94 -0
- package/skills/cmo/rules/learning-loop.md +168 -0
- package/skills/cmo/rules/playbooks.md +215 -0
- package/skills/cmo/rules/progressive-enhancement.md +167 -0
- package/skills/cmo/rules/quality-gate.md +55 -0
- package/skills/cmo/rules/sub-agents.md +135 -0
- package/skills/deepen-plan/SKILL.md +1 -1
- package/skills/direct-response-copy/SKILL.md +1 -1
- package/skills/email-sequences/SKILL.md +1 -1
- package/skills/firecrawl/.skill-meta.json +2 -2
- package/skills/keyword-research/SKILL.md +1 -1
- package/skills/launch-strategy/SKILL.md +2 -2
- package/skills/marketing-psychology/SKILL.md +1 -1
- package/skills/mktg-x/.skill-meta.json +2 -2
- package/skills/positioning-angles/SKILL.md +1 -1
- package/skills/postiz/SKILL.md +248 -0
- package/skills/seo-content/SKILL.md +7 -5
- package/skills/social-campaign/SKILL.md +65 -0
- package/skills-manifest.json +27 -0
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
---
|
|
2
|
+
name: postiz
|
|
3
|
+
description: >-
|
|
4
|
+
Schedule social posts to any of 30+ providers via a running postiz instance
|
|
5
|
+
(hosted at api.postiz.com or self-hosted). Use this skill whenever someone
|
|
6
|
+
wants to post, schedule, or publish a draft to LinkedIn, Reddit, Bluesky,
|
|
7
|
+
Mastodon, Threads, Instagram, TikTok, YouTube, Pinterest, Discord, Slack, or
|
|
8
|
+
any non-Twitter social channel. ALWAYS chain content-atomizer first for
|
|
9
|
+
platform-specific copy — this skill is a distribution layer only, it does
|
|
10
|
+
not rewrite copy. For Twitter/X threads, use typefully instead (better
|
|
11
|
+
thread UX). Triggers: "post to linkedin", "post to reddit", "post to
|
|
12
|
+
bluesky", "post to mastodon", "post to threads", "schedule via postiz".
|
|
13
|
+
category: distribution
|
|
14
|
+
tier: must-have
|
|
15
|
+
layer: distribution
|
|
16
|
+
reads:
|
|
17
|
+
- brand/voice-profile.md
|
|
18
|
+
depends_on:
|
|
19
|
+
- content-atomizer
|
|
20
|
+
env_vars:
|
|
21
|
+
- POSTIZ_API_KEY
|
|
22
|
+
- POSTIZ_API_BASE
|
|
23
|
+
triggers:
|
|
24
|
+
- post to linkedin
|
|
25
|
+
- post to reddit
|
|
26
|
+
- post to bluesky
|
|
27
|
+
- post to mastodon
|
|
28
|
+
- post to threads
|
|
29
|
+
- schedule via postiz
|
|
30
|
+
allowed-tools:
|
|
31
|
+
- Bash(mktg publish *)
|
|
32
|
+
- Bash(mktg catalog *)
|
|
33
|
+
- Bash(mktg doctor *)
|
|
34
|
+
---
|
|
35
|
+
|
|
36
|
+
# Postiz — Social Distribution Layer
|
|
37
|
+
|
|
38
|
+
You schedule pre-written social posts to LinkedIn, Reddit, Bluesky, Mastodon, Threads, Instagram, TikTok, YouTube, Pinterest, Discord, Slack, and any other provider a running postiz instance supports. You do NOT write copy. You do NOT tailor per platform. Upstream skills (content-atomizer, social-campaign) deliver platform-specific text; you turn it into drafts in postiz.
|
|
39
|
+
|
|
40
|
+
For Twitter/X threads, defer to `typefully` — its thread UX is canonical. You handle everything else.
|
|
41
|
+
|
|
42
|
+
## North Star
|
|
43
|
+
|
|
44
|
+
Postiz is a dumb distribution layer in the mktg playbook:
|
|
45
|
+
|
|
46
|
+
1. You never generate or rewrite copy.
|
|
47
|
+
2. You never choose the platform mix — that's the user's ask or social-campaign's plan.
|
|
48
|
+
3. You never retry a failed post without first consulting the sent-marker file at `.mktg/publish/{campaign}-postiz.json` — postiz has no idempotency key, and retries duplicate.
|
|
49
|
+
4. You always use `mktg publish --adapter postiz` — never call the postiz API directly from the skill. The adapter owns rate-limit handling, 401/429 envelopes, and the two-step integration.id resolution.
|
|
50
|
+
|
|
51
|
+
## On Activation
|
|
52
|
+
|
|
53
|
+
Run these 4 steps before anything else. Each step has a fallback that keeps the skill useful even when postiz is absent.
|
|
54
|
+
|
|
55
|
+
### Step 1 — Verify the catalog is registered and configured
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
mktg catalog info postiz --json --fields configured,missing_envs,auth.credential_envs,resolved_base
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
`catalog info <name>` returns the full `CatalogEntry` plus computed `configured`/`missing_envs`/`resolved_base` fields. The command errors with exit code 1 (NOT_FOUND) if the catalog is not in `catalogs-manifest.json` — that's the "not registered" signal.
|
|
62
|
+
|
|
63
|
+
- Exit code 1 → postiz catalog is not registered. Tell the user: `mktg catalog add postiz --confirm`. Stop.
|
|
64
|
+
- `configured: false` → env vars are missing. Build the fix string from `missing_envs` rather than hardcoding. Canonical expectation: `POSTIZ_API_KEY` (required) and `POSTIZ_API_BASE` (defaults to `https://api.postiz.com` if unset; the catalog loader returns `resolved_base: null` in that case and the skill applies the default). Stop.
|
|
65
|
+
- `configured: true` → proceed to Step 2.
|
|
66
|
+
|
|
67
|
+
### Step 2 — Resolve connected provider identifiers (cached)
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
mktg publish --adapter postiz --list-integrations --json
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
Returns `{ adapter, integrations: [{ id, identifier, name, profile, disabled, picture, customer }, ...] }` from `GET /public/v1/integrations`. Cache in `.mktg/cache/postiz-integrations.json` (TTL 15 minutes). Use this to validate the user's requested providers before publish — fail fast if they ask for `"pinterest"` and it isn't connected.
|
|
74
|
+
|
|
75
|
+
**Critical: `identifier` is a postiz-native provider-module key, NOT a platform display name.** Single-variant providers collapse to the expected string (`"reddit"`, `"bluesky"`, `"mastodon"`, `"threads"`), but some platforms ship multiple identifiers:
|
|
76
|
+
|
|
77
|
+
| Platform | Possible postiz `identifier` values |
|
|
78
|
+
|---|---|
|
|
79
|
+
| LinkedIn | `linkedin` (personal profile), `linkedin-page` (company page) — distinct integrations |
|
|
80
|
+
| YouTube | may vary by postiz version (channel vs shorts flavors) |
|
|
81
|
+
| Other | any string postiz's provider module exports |
|
|
82
|
+
|
|
83
|
+
The adapter does **exact-match** on `identifier` (no alias layer) — if the user says *"post to LinkedIn"* and the instance only has `linkedin-page` connected, the adapter will refuse `"linkedin"`. Alias handling is your job in Step 2b.
|
|
84
|
+
|
|
85
|
+
### Step 2b — Handle user friendly-name aliasing (skill-layer only)
|
|
86
|
+
|
|
87
|
+
When the agent hears *"post to LinkedIn"* (a platform name, not a postiz identifier), map the friendly name to the right postiz identifier(s):
|
|
88
|
+
|
|
89
|
+
1. Look up the cached integrations from Step 2.
|
|
90
|
+
2. Find all identifiers whose display name or known-alias matches the user's phrase. For LinkedIn, both `linkedin` and `linkedin-page` match.
|
|
91
|
+
3. If exactly one matches → use it.
|
|
92
|
+
4. If multiple match → present the options via AskUserQuestion. *"You have both `linkedin` (personal) and `linkedin-page` (company) connected. Which?"* Let the user pick; cache their choice in `.mktg/cache/postiz-aliases.json` for future sessions.
|
|
93
|
+
5. If zero match → error: *"'LinkedIn' is not connected in this postiz instance. Connected: {enumeration}. Connect it in the postiz UI first."*
|
|
94
|
+
|
|
95
|
+
**Alias resolution lives here and NEVER in the adapter** — the adapter takes whatever you pass as `metadata.providers` and resolves against the live integrations verbatim.
|
|
96
|
+
|
|
97
|
+
### Step 3 — Chain to content-atomizer FIRST if content is raw
|
|
98
|
+
|
|
99
|
+
If the user's input is a blog post, article, or long-form source (length > 500 chars or file extension `.md` in `marketing/content/`), you MUST chain `/content-atomizer` first to produce platform-native copy. Feed the atomizer's output files (`linkedin-posts.md`, `reddit-posts.md`, etc.) into this skill.
|
|
100
|
+
|
|
101
|
+
If the user's input is already platform-tailored (short-form text targeting one platform), skip atomization and proceed.
|
|
102
|
+
|
|
103
|
+
### Step 4 — Tone-check against voice-profile (sanity guard only)
|
|
104
|
+
|
|
105
|
+
Read `brand/voice-profile.md` if it exists. Before building the publish manifest, verify each post:
|
|
106
|
+
- Does the tone spectrum match (not wildly off-brand)?
|
|
107
|
+
- Is the length within the platform's character limit **and** within the voice profile's stated sentence-rhythm range?
|
|
108
|
+
|
|
109
|
+
If a check fails, surface a warning to the user and ask before proceeding. **Never rewrite.** That's content-atomizer's job — bounce back there.
|
|
110
|
+
|
|
111
|
+
## How to Use
|
|
112
|
+
|
|
113
|
+
This skill runs in one of two modes. Pick by looking at the input.
|
|
114
|
+
|
|
115
|
+
### Mode A — Single post to one or more platforms
|
|
116
|
+
|
|
117
|
+
User says: *"Post this to LinkedIn and Bluesky"* + paste text.
|
|
118
|
+
|
|
119
|
+
1. Run the On Activation steps above. Step 2b resolves any user-said platform names ("LinkedIn") to postiz identifiers (`linkedin` or `linkedin-page` or both).
|
|
120
|
+
2. Build a single-item `publish.json`. The manifest's top-level `name` is the campaign identifier (the adapter threads this into sent-marker keys — do NOT also put `campaign` in `metadata`; the adapter ignores it):
|
|
121
|
+
```json
|
|
122
|
+
{
|
|
123
|
+
"name": "ad-hoc-{timestamp}",
|
|
124
|
+
"items": [{
|
|
125
|
+
"type": "social",
|
|
126
|
+
"adapter": "postiz",
|
|
127
|
+
"content": "<the user's text verbatim>",
|
|
128
|
+
"metadata": { "providers": ["linkedin", "bluesky"] }
|
|
129
|
+
}]
|
|
130
|
+
}
|
|
131
|
+
```
|
|
132
|
+
3. `mktg publish --adapter postiz --dry-run --input '<manifest>' --json` — preview.
|
|
133
|
+
4. On user confirm: `mktg publish --adapter postiz --confirm --input '<manifest>' --json`.
|
|
134
|
+
5. Report per-provider status from the adapter's response envelope.
|
|
135
|
+
|
|
136
|
+
### Mode B — Campaign from content-atomizer output
|
|
137
|
+
|
|
138
|
+
User says: *"Schedule my atomized blog post to LinkedIn, Reddit, and Threads"*.
|
|
139
|
+
|
|
140
|
+
1. Run On Activation.
|
|
141
|
+
2. Read `marketing/social/{source-slug}/linkedin-posts.md`, `reddit-posts.md`, `threads-posts.md`. Each file contains one or more posts with YAML frontmatter matching the atomizer's contract.
|
|
142
|
+
3. Build a multi-item `publish.json` with one item per (file × post). Each item's `metadata.providers` is a single-element array matching the file's platform.
|
|
143
|
+
4. Same dry-run → confirm → report flow as Mode A.
|
|
144
|
+
|
|
145
|
+
### Example 1 — LinkedIn-only draft
|
|
146
|
+
|
|
147
|
+
```bash
|
|
148
|
+
mktg publish --adapter postiz --confirm --input '{
|
|
149
|
+
"name": "linkedin-announcement",
|
|
150
|
+
"items": [{
|
|
151
|
+
"type": "social",
|
|
152
|
+
"adapter": "postiz",
|
|
153
|
+
"content": "We shipped v2.0 today. Here is what changed...",
|
|
154
|
+
"metadata": { "providers": ["linkedin"] }
|
|
155
|
+
}]
|
|
156
|
+
}' --json
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
### Example 2 — Cross-platform (atomizer output + postiz distribution)
|
|
160
|
+
|
|
161
|
+
```bash
|
|
162
|
+
/content-atomizer marketing/content/blog/v2-announcement.md
|
|
163
|
+
# Atomizer writes marketing/social/v2-announcement/{linkedin,reddit,threads}-posts.md
|
|
164
|
+
|
|
165
|
+
# Build a publish.json with one item per atomized post, then:
|
|
166
|
+
mktg publish --adapter postiz --dry-run --json < /tmp/v2-launch.publish.json
|
|
167
|
+
# Review → confirm:
|
|
168
|
+
mktg publish --adapter postiz --confirm --json < /tmp/v2-launch.publish.json
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
### Example 3 — Mixed batch (postiz + typefully)
|
|
172
|
+
|
|
173
|
+
This is the social-campaign Phase 5 path. You do NOT handle this standalone — the orchestrator in social-campaign routes per-post per the Phase 5 routing table.
|
|
174
|
+
|
|
175
|
+
## Inputs
|
|
176
|
+
|
|
177
|
+
| Input | Required | Shape | Notes |
|
|
178
|
+
|---|---|---|---|
|
|
179
|
+
| `content` | yes | string | Platform-tailored post text. Must fit the target platform's character limit (LinkedIn 3000, Reddit no limit, Bluesky 300, Mastodon 500, Threads 500). Failing this → Step 4 tone-check surfaces a warning. |
|
|
180
|
+
| `metadata.providers` | yes | string[] | **Postiz identifiers, not platform display names.** Canonical examples: `"linkedin"` (personal), `"linkedin-page"` (company page), `"reddit"`, `"bluesky"`, `"mastodon"`, `"threads"`, `"instagram"`, `"tiktok"`, `"youtube"`, `"pinterest"`, `"discord"`, `"slack"`. Must match `identifier` exactly from `GET /public/v1/integrations`. Unknown identifiers fail fast per-item with the connected list in the error. Alias resolution (user says "LinkedIn" → you pick `linkedin` vs `linkedin-page`) happens in Step 2b, never in the adapter. |
|
|
181
|
+
| `metadata.mediaPaths` | no | string[] | v2 stub — **ignored in v1** (draft-only, images hardcoded to `[]` in the adapter). Don't populate this field in v1 manifests; you're wasting tokens. |
|
|
182
|
+
|
|
183
|
+
**Campaign identifier does NOT live in `metadata`.** The adapter reads it from the publish manifest's top-level `name` field. If you set `metadata.campaign`, the adapter silently ignores it. Never emit `metadata.campaign`.
|
|
184
|
+
|
|
185
|
+
**Validation performed at publish-time, before the network:**
|
|
186
|
+
- Control chars in `content` are rejected via the base pipeline's `rejectControlChars`.
|
|
187
|
+
- For each item, any identifier in `providers` that isn't in the cached `/public/v1/integrations` result produces a per-item `failed` result with `detail: "Unconnected provider(s): X. Connected: {list}. Connect in the postiz UI first."`. The adapter does NOT short-circuit the whole batch — items with fully-connected providers still publish.
|
|
188
|
+
- Missing `metadata.providers` fails per-item with `detail: "Missing item.metadata.providers[] — add at least one postiz identifier"`.
|
|
189
|
+
|
|
190
|
+
## Outputs
|
|
191
|
+
|
|
192
|
+
The adapter returns the standard `AdapterResult` envelope:
|
|
193
|
+
|
|
194
|
+
```json
|
|
195
|
+
{
|
|
196
|
+
"adapter": "postiz",
|
|
197
|
+
"items": 3,
|
|
198
|
+
"published": 3,
|
|
199
|
+
"failed": 0,
|
|
200
|
+
"errors": [],
|
|
201
|
+
"results": [
|
|
202
|
+
{ "item": 0, "status": "published", "detail": "draft id=abc123 provider=linkedin" },
|
|
203
|
+
{ "item": 0, "status": "published", "detail": "draft id=abc124 provider=reddit" },
|
|
204
|
+
{ "item": 0, "status": "skipped", "detail": "dedupe: sent-marker match" }
|
|
205
|
+
]
|
|
206
|
+
}
|
|
207
|
+
```
|
|
208
|
+
|
|
209
|
+
**Per-provider fan-out:** one input item with N providers becomes N entries in `results[]`, each with its own status. A partial failure (Reddit rate-limited, LinkedIn succeeded) does NOT fail the whole item.
|
|
210
|
+
|
|
211
|
+
**Exit-code aggregation:** the top-level `publish` command exits **0** whenever at least one item succeeds. Top-level non-zero exit only when ZERO items succeed. This matches how `typefully` and `resend` adapters already behave. Inspect `results[]` for per-item outcomes — a successful publish run can still have failed providers inside it.
|
|
212
|
+
|
|
213
|
+
**Sent-marker side-effect:** successful publishes append to `.mktg/publish/<campaign>-postiz.json` with a stable hash key. Re-running the same campaign is a no-op for already-sent items.
|
|
214
|
+
|
|
215
|
+
**Platform coverage table (UI hint):** after a run, render a small confirmation table showing per-provider status, draft ID, and notes.
|
|
216
|
+
|
|
217
|
+
## Anti-Patterns
|
|
218
|
+
|
|
219
|
+
| Anti-Pattern | Why it fails | Instead |
|
|
220
|
+
|---|---|---|
|
|
221
|
+
| Generate copy in this skill | Voice drift — each skill that rewrites copy diverges from `voice-profile.md`. We have one generation path (content-atomizer) for a reason: platform-specific copy rules live there. If you inline paraphrasing here, the voice profile's guardrails don't fire. | Chain `/content-atomizer` first. Feed its file output into this skill. |
|
|
222
|
+
| Retry a failed post without checking the sent-marker | Postiz has no idempotency key and no webhooks. A retry without checking `.mktg/publish/<campaign>-postiz.json` duplicates the draft — if the user accepted the first one before the adapter crashed, you just ship the post twice. | Always call the adapter. The adapter consults the sent-marker *before* POSTing. Never construct your own HTTP request. |
|
|
223
|
+
| Call the postiz API from within this skill | Every HTTP caller reinvents error handling. The adapter owns 401 (bad key), 429 (rate limit with Retry-After), 400 (bad provider), and network timeouts — each with structured `fix` fields. Skipping the adapter loses these. | `mktg publish --adapter postiz ...`. Always. |
|
|
224
|
+
| Pass postiz `integration.id` DB primary keys in `metadata.providers` | `integration.id` values are database rows — they change between environments (hosted vs self-host vs fresh install). A hardcoded `"42"` breaks the moment the user rotates their instance. | Pass postiz **identifiers** (`"linkedin"`, `"linkedin-page"`, `"bluesky"`) verbatim — these are provider-module keys that are stable across instances. The adapter does the identifier → id lookup. |
|
|
225
|
+
| Translate user-said platform names to identifiers in the adapter | Alias drift: the moment postiz ships a new provider module, any in-adapter alias layer becomes wrong. The adapter does exact-match on `identifier`; aliasing is a SKILL-layer concern. | Handle alias resolution in Step 2b of On Activation (this skill). Pass exact postiz identifiers to the adapter. |
|
|
226
|
+
| Populate `metadata.campaign` in publish items | Redundant: the adapter threads `manifest.name` into sent-marker keys. If you also set `metadata.campaign`, the adapter ignores it — confusion for the agent about which wins. | Use the top-level `manifest.name` field as the campaign identifier. Don't duplicate into `metadata`. |
|
|
227
|
+
| Populate `metadata.mediaPaths` in v1 | v1 is draft-only; the adapter hardcodes images to `[]`. Setting `mediaPaths` silently no-ops and wastes tokens. | Leave `mediaPaths` out of v1 manifests. Image support lands in v2 alongside `schedule` / `now` post types. |
|
|
228
|
+
| Skip dry-run and go straight to `--confirm` | 30 drafts/hour is a tight rate limit; burning quota on broken manifests is wasteful. Dry-run validates providers and env vars without touching the network. | Always dry-run first. |
|
|
229
|
+
| Use this skill for X threads | Typefully owns threads — better UX, per-tweet character validation, and thread-unroll view. Postiz can post to X but thread authoring is weak. | Twitter/X single post OR thread → `typefully`. Everything else → this skill. |
|
|
230
|
+
|
|
231
|
+
## Progressive Enhancement
|
|
232
|
+
|
|
233
|
+
| Level | State | Experience |
|
|
234
|
+
|---|---|---|
|
|
235
|
+
| L0 | No brand files, no postiz configured | Activation step 1 fails with `configured: false` and emits a clean fix: set `POSTIZ_API_KEY` and optionally `POSTIZ_API_BASE`. No network calls, no partial state, no crash. Exit code 3 (dependency missing). |
|
|
236
|
+
| L1 | `POSTIZ_API_KEY` set, `POSTIZ_API_BASE` defaulted to `https://api.postiz.com` | The skill works for every platform the hosted instance supports. `voice-profile.md` absent → tone-check is a no-op. User feeds raw copy → warning suggests chaining content-atomizer but doesn't block. |
|
|
237
|
+
| L2 | + `voice-profile.md` | Tone-check fires. Posts that violate length or tone spectrum surface warnings before the publish confirm prompt. Still functional without atomization. |
|
|
238
|
+
| L3 | + `content-atomizer` has run and written `marketing/social/{slug}/` | Mode B is available. Batch-publish N atomized posts across M platforms with one confirm. |
|
|
239
|
+
| L4 | + full brand + postiz configured + content-atomizer chain | End-to-end: user says "launch my blog post on LinkedIn/Reddit/Bluesky", orchestrator routes, atomizer writes, postiz publishes. Voice honored; sent-markers prevent dupes; partial failures roll forward. |
|
|
240
|
+
|
|
241
|
+
This skill never fails silently. At every level, the user either gets structured output (publish succeeded/failed) or a structured error with a `fix` field. Absent postiz, the skill errors cleanly — no half-finished drafts.
|
|
242
|
+
|
|
243
|
+
## Related Skills
|
|
244
|
+
|
|
245
|
+
- **content-atomizer** — generates platform-specific copy. MUST run before this skill for long-form source content.
|
|
246
|
+
- **typefully** — X/Twitter thread handling. Complements this skill (X threads → typefully; everything else → this skill).
|
|
247
|
+
- **social-campaign** — orchestrator that chains strategy/write/review/design/schedule. In Phase 5, it picks the scheduling backend per platform.
|
|
248
|
+
- **brand-voice** — writes `voice-profile.md`, which this skill reads as a sanity guard in Step 4.
|
|
@@ -200,7 +200,7 @@ Run all checklists before saving:
|
|
|
200
200
|
- **SEO quality:** Keywords placed, meta compelling, links included, schema valid
|
|
201
201
|
- **E-E-A-T:** Experience shown, expertise demonstrated, sources cited
|
|
202
202
|
|
|
203
|
-
See `references/eeat-examples.md` for 20
|
|
203
|
+
See `references/eeat-examples.md` for 20 top-performing human content examples.
|
|
204
204
|
|
|
205
205
|
---
|
|
206
206
|
|
|
@@ -364,10 +364,12 @@ After saving, present:
|
|
|
364
364
|
|
|
365
365
|
## Anti-Patterns
|
|
366
366
|
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
367
|
+
| Anti-pattern | Instead | Why |
|
|
368
|
+
|-------------|---------|-----|
|
|
369
|
+
| Skipping Phase 5 (Humanize) to save time | Cut research depth before cutting humanization | Unhumanized content reads like AI slop — readers bounce, Google devalues it, and the brand takes a credibility hit. Content that sounds like a committee wrote it doesn't earn trust, links, or shares. |
|
|
370
|
+
| Skipping schema markup | Always add Article + FAQ JSON-LD — it takes minutes | Schema compounds over time: rich results, AI citations, and structured search features for the life of the content. Skipping it leaves free visibility on the table. |
|
|
371
|
+
| Keyword-stuffing headings | One keyword in H1, natural secondary keywords in H2s | Google detects keyword stuffing and users find it off-putting. If every heading reads like an SEO template, it's over-optimized. |
|
|
372
|
+
| Starting with a generic intro | Answer the search query in sentence one | "In today's digital world..." tells the reader nothing. The search query brought them here — if the value prop isn't instant, they bounce. |
|
|
371
373
|
|
|
372
374
|
---
|
|
373
375
|
|
|
@@ -194,6 +194,38 @@ For the full decision framework (which posts get images), image types, Paper MCP
|
|
|
194
194
|
|
|
195
195
|
### Phase 5: SCHEDULE
|
|
196
196
|
|
|
197
|
+
<!-- BEGIN POSTIZ EXTENSION (mktg v0.2+) -->
|
|
198
|
+
|
|
199
|
+
**Step 0 — Pick the scheduling backend per post.**
|
|
200
|
+
|
|
201
|
+
Before scheduling anything, detect which backends are available. This check runs once per Phase 5 entry and caches for the phase:
|
|
202
|
+
|
|
203
|
+
```bash
|
|
204
|
+
mktg doctor --json --fields checks.typefully.configured,checks.postiz.configured
|
|
205
|
+
```
|
|
206
|
+
|
|
207
|
+
Resolve the routing per post using the table below. `platform` is already tagged per post by Phase 2 and appears verbatim in the post file's YAML frontmatter.
|
|
208
|
+
|
|
209
|
+
| Platform tagged on the post | Both configured | Typefully only | Postiz only | Neither |
|
|
210
|
+
|---|---|---|---|---|
|
|
211
|
+
| `x` / Twitter (single or thread) | **Typefully** (threads are canonical here) | Typefully | Postiz (no thread support, flag it) | Skip with fix: "Set TYPEFULLY_API_KEY or POSTIZ_API_KEY" |
|
|
212
|
+
| `linkedin`, `threads`, `bluesky`, `mastodon` | **Postiz** (richer provider catalog) | Typefully | Postiz | Skip with fix |
|
|
213
|
+
| `reddit`, `instagram`, `tiktok`, `youtube`, `pinterest`, `discord`, `slack` | **Postiz** | Can't publish — write file only, instruct user to copy-paste | Postiz | Skip with fix |
|
|
214
|
+
|
|
215
|
+
**Invariant:** any post with `type: thread` stays on Typefully regardless of postiz availability. Thread UX is Typefully's canonical path; do not degrade it.
|
|
216
|
+
|
|
217
|
+
**Progressive enhancement:** if `checks.postiz.configured === false`, Phase 5 behaves exactly as the Typefully-only path below — zero behavior change for users without postiz. If both are missing, fall back to writing files into `marketing/campaigns/{campaign-name}/posts/` and surface a "copy these to the platform UI" summary.
|
|
218
|
+
|
|
219
|
+
**Fallback behavior under failure (during the phase):**
|
|
220
|
+
- Postiz unreachable / 5xx: retry once with exponential backoff (1s, 2s). On second failure for a Typefully-covered platform (linkedin/threads/bluesky/mastodon), fall back to Typefully. On second failure for a postiz-only platform (reddit/instagram/tiktok/youtube/pinterest/discord/slack), fail the post with `fix: "postiz unreachable — check mktg catalog info postiz --fields configured,resolved_base"`.
|
|
221
|
+
- Postiz 401: halt the entire phase. Bad key means the whole batch will fail; don't waste rate-limit quota.
|
|
222
|
+
- Postiz 429: parse `Retry-After` from the adapter's error envelope; surface to the user. Resume after the cooldown or save the batch for later.
|
|
223
|
+
- Typefully failure: unchanged from pre-extension behavior — fail loudly per the existing flow below.
|
|
224
|
+
|
|
225
|
+
<!-- END POSTIZ EXTENSION -->
|
|
226
|
+
|
|
227
|
+
### Case A — Typefully path (X and threads, plus Typefully-covered platforms when postiz is absent)
|
|
228
|
+
|
|
197
229
|
Upload media and schedule all posts via Typefully.
|
|
198
230
|
|
|
199
231
|
**For posts WITH images:**
|
|
@@ -221,6 +253,39 @@ Upload media and schedule all posts via Typefully.
|
|
|
221
253
|
|
|
222
254
|
**Gate:** User reviews scheduled posts in Typefully UI.
|
|
223
255
|
|
|
256
|
+
### Case B — Postiz path (non-Twitter platforms when postiz is configured)
|
|
257
|
+
|
|
258
|
+
For each post whose routed backend is postiz (per Step 0 table), build a `publish.json` entry and invoke the adapter. You do NOT call the postiz API directly — always go through `mktg publish --adapter postiz`.
|
|
259
|
+
|
|
260
|
+
**For text-only posts (v1 is draft-only; scheduling lands in v2):**
|
|
261
|
+
1. Build `publish.json` with one item per post:
|
|
262
|
+
```json
|
|
263
|
+
{
|
|
264
|
+
"name": "{campaign-name}",
|
|
265
|
+
"items": [{
|
|
266
|
+
"type": "social",
|
|
267
|
+
"adapter": "postiz",
|
|
268
|
+
"content": "<post text>",
|
|
269
|
+
"metadata": { "providers": ["<postiz-identifier>"] }
|
|
270
|
+
}]
|
|
271
|
+
}
|
|
272
|
+
```
|
|
273
|
+
`providers` must be exact postiz `identifier` strings (`linkedin`, `linkedin-page`, `bluesky`, `reddit`, etc.) — resolve friendly names via `/postiz` skill's Step 2b alias logic, not inline here.
|
|
274
|
+
|
|
275
|
+
2. Dry-run: `mktg publish --adapter postiz --dry-run --input '<json>' --json`. Verify the adapter validates the providers against connected integrations.
|
|
276
|
+
3. Confirm: `mktg publish --adapter postiz --confirm --input '<json>' --json`. The adapter writes sent-markers to `.mktg/publish/{campaign-name}-postiz.json` on success.
|
|
277
|
+
4. Update the post file frontmatter: `status: scheduled`, `draft_id: <id>`.
|
|
278
|
+
|
|
279
|
+
**For posts WITH images (v2 — skip in v1):**
|
|
280
|
+
postiz v1 is draft-only; images hardcode to `[]`. If a post has `has_image: true`, either (a) wait for v2, or (b) downgrade to Typefully if the platform allows (X/LinkedIn). Do NOT populate `metadata.mediaPaths` in v1 — the adapter ignores it.
|
|
281
|
+
|
|
282
|
+
**After scheduling all posts (mixed Typefully + postiz):**
|
|
283
|
+
1. Verify Typefully side: `typefully.js drafts:list --status scheduled --sort scheduled_date`.
|
|
284
|
+
2. Verify postiz side: the adapter's response envelope already reports per-provider status in `results[]`. Capture draft IDs to the post file frontmatter.
|
|
285
|
+
3. Present the unified final summary table.
|
|
286
|
+
|
|
287
|
+
**Gate:** User reviews scheduled posts in each platform's UI (Typefully for X, postiz dashboard for everything else).
|
|
288
|
+
|
|
224
289
|
## Human-in-the-Loop Gates
|
|
225
290
|
|
|
226
291
|
```
|
package/skills-manifest.json
CHANGED
|
@@ -1144,6 +1144,33 @@
|
|
|
1144
1144
|
"TYPEFULLY_API_KEY"
|
|
1145
1145
|
]
|
|
1146
1146
|
},
|
|
1147
|
+
"postiz": {
|
|
1148
|
+
"source": "new",
|
|
1149
|
+
"category": "distribution",
|
|
1150
|
+
"layer": "distribution",
|
|
1151
|
+
"tier": "must-have",
|
|
1152
|
+
"reads": [
|
|
1153
|
+
"voice-profile.md"
|
|
1154
|
+
],
|
|
1155
|
+
"writes": [],
|
|
1156
|
+
"depends_on": [
|
|
1157
|
+
"content-atomizer"
|
|
1158
|
+
],
|
|
1159
|
+
"triggers": [
|
|
1160
|
+
"post to linkedin",
|
|
1161
|
+
"post to reddit",
|
|
1162
|
+
"post to bluesky",
|
|
1163
|
+
"post to mastodon",
|
|
1164
|
+
"post to threads",
|
|
1165
|
+
"schedule via postiz"
|
|
1166
|
+
],
|
|
1167
|
+
"review_interval_days": 30,
|
|
1168
|
+
"version": "1.0.0",
|
|
1169
|
+
"env_vars": [
|
|
1170
|
+
"POSTIZ_API_KEY",
|
|
1171
|
+
"POSTIZ_API_BASE"
|
|
1172
|
+
]
|
|
1173
|
+
},
|
|
1147
1174
|
"send-email": {
|
|
1148
1175
|
"source": "third-party",
|
|
1149
1176
|
"category": "distribution",
|