chore: bulk commit of local changes across blog writer, SEO dashboard, scheduler, docs-site, and frontend
This commit is contained in:
@@ -75,12 +75,16 @@ flowchart TD
|
||||
**Request Body:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
|---|---|---|---|---|
|
||||
| `name` | string | Yes | Campaign name. |
|
||||
| `description` | string | No | Campaign description. |
|
||||
| `keywords` | string[] | No | Target keywords for discovery. |
|
||||
|
||||
**Response:** `201 Created` — Campaign object.
|
||||
**Error responses:**
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `422` | Validation error (e.g., empty name). |
|
||||
|
||||
### List Campaigns
|
||||
|
||||
@@ -92,7 +96,7 @@ flowchart TD
|
||||
|---|---|---|---|
|
||||
| `workspace_id` | string | user_id | Workspace to filter by. Defaults to authenticated user. |
|
||||
|
||||
**Response:** `200 OK` — Array of campaign objects.
|
||||
**Response:** `200 OK` — Array of campaign objects scoped to the authenticated user.
|
||||
|
||||
### Get Campaign
|
||||
|
||||
@@ -100,12 +104,24 @@ flowchart TD
|
||||
|
||||
**Response:** `200 OK` — Campaign object with included leads.
|
||||
|
||||
**Error responses:**
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `404` | Campaign not found or does not belong to authenticated user (`BacklinkCampaignNotFoundError`). |
|
||||
|
||||
### Delete Campaign
|
||||
|
||||
`DELETE /api/v1/backlink-outreach/campaigns/{campaign_id}`
|
||||
|
||||
**Response:** `204 No Content`
|
||||
|
||||
**Error responses:**
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `404` | Campaign not found or does not belong to authenticated user. |
|
||||
|
||||
---
|
||||
|
||||
## Leads
|
||||
@@ -117,7 +133,7 @@ flowchart TD
|
||||
**Request Body:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
|---|---|---|---|---|
|
||||
| `website_url` | string | Yes | Target website URL. |
|
||||
| `website_title` | string | No | Website title. |
|
||||
| `contact_email` | string | No | Contact email address. |
|
||||
@@ -126,7 +142,14 @@ flowchart TD
|
||||
| `guest_post_likelihood` | float | No | Guest post likelihood (0-1). |
|
||||
| `source` | string | No | Source of the lead. |
|
||||
|
||||
**Response:** `201 Created` — Lead object.
|
||||
!!! tip "Duplicate handling"
|
||||
If a lead with the same `website_url` already exists in the campaign, the existing lead record is returned (HTTP 200) instead of creating a duplicate.
|
||||
|
||||
**Error responses:**
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `404` | Campaign not found or not owned by user. |
|
||||
|
||||
### Bulk Add Leads
|
||||
|
||||
@@ -138,8 +161,8 @@ flowchart TD
|
||||
|
||||
| Field | Type | Description |
|
||||
|---|---|---|
|
||||
| `added` | int | Number of leads successfully added. |
|
||||
| `skipped` | int | Number of duplicates skipped. |
|
||||
| `added` | int | Number of leads successfully added (duplicates excluded). |
|
||||
| `skipped` | int | Number of existing leads skipped (matched by `(campaign_id, website_url)`). |
|
||||
| `failed` | string[] | List of failed entries with reasons. |
|
||||
|
||||
### Update Lead Status
|
||||
@@ -149,10 +172,15 @@ flowchart TD
|
||||
**Request Body:**
|
||||
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `status` | string | Yes | New status: discovered, contacted, replied, placed, bounced, lost. |
|
||||
|---|---|---|---|---|
|
||||
| `status` | string | Yes | New status: `discovered`, `contacted`, `replied`, `placed`, `bounced`, `unsubscribed`. |
|
||||
|
||||
**Response:** `200 OK` — Updated lead object.
|
||||
**Error responses:**
|
||||
|
||||
| Code | Meaning |
|
||||
|---|---|
|
||||
| `422` | Invalid status value (must be one of the valid statuses). |
|
||||
| `404` | Lead not found. |
|
||||
|
||||
### Bulk Update Status
|
||||
|
||||
@@ -163,7 +191,7 @@ flowchart TD
|
||||
| Field | Type | Required | Description |
|
||||
|---|---|---|---|
|
||||
| `lead_ids` | string[] | Yes | Lead IDs to update. |
|
||||
| `status` | string | Yes | New status for all leads. |
|
||||
| `status` | string | Yes | New status: `discovered`, `contacted`, `replied`, `placed`, `bounced`, `unsubscribed`. |
|
||||
|
||||
**Response:** `200 OK`
|
||||
|
||||
@@ -441,9 +469,10 @@ flowchart TD
|
||||
## Common Error Responses
|
||||
|
||||
| Status | Meaning | Body |
|
||||
|---|---|---|
|
||||
|---|---|---|---|
|
||||
| `401` | Not authenticated | `{"detail": "Not authenticated"}` |
|
||||
| `403` | Policy blocked | `{"detail": "Policy validation failed", "reason": "..."}` |
|
||||
| `404` | Not found | `{"detail": "Resource not found"}` |
|
||||
| `404` | Campaign or lead not found | `{"detail": "BacklinkCampaignNotFoundError: Campaign not found or access denied"}` |
|
||||
| `409` | Duplicate lead (idempotency key collision) | `{"detail": "Duplicate attempt detected"}` |
|
||||
| `422` | Validation error | `{"detail": [...validation errors]}` |
|
||||
| `500` | Server error | `{"detail": "An internal error occurred"}` (generic, no stack trace) |
|
||||
|
||||
@@ -21,6 +21,9 @@ A campaign requires only a name. Add a description and keywords to make discover
|
||||
!!! tip "Naming conventions"
|
||||
Use a consistent naming scheme like `[Vertical] [Content Type] [Period]` — e.g., "Fitness Guest Posts June" or "AI Startups Roundup Q3".
|
||||
|
||||
!!! warning "Ownership validation"
|
||||
Campaigns are scoped to the authenticated user. API calls with a `campaign_id` that does not exist or belongs to another user return `404 BacklinkCampaignNotFoundError`. This applies to all campaign operations (get, delete, add leads, send emails, etc.).
|
||||
|
||||
## Campaign List View
|
||||
|
||||
The campaign list shows:
|
||||
|
||||
@@ -68,6 +68,20 @@ The Backlink Outreach feature uses SQLite with automatic table creation:
|
||||
|
||||
Tables are created automatically on first use via `_ensure_tables()`. No manual migration is required.
|
||||
|
||||
## Feature Flag Configuration
|
||||
|
||||
The Backlink Outreach feature can be enabled in isolation via the `ALWRITY_ENABLED_FEATURES` environment variable:
|
||||
|
||||
| Variable | Value | Description |
|
||||
|---|---|---|
|
||||
| `ALWRITY_ENABLED_FEATURES` | `all` (default) | Enable all platform features. |
|
||||
| `ALWRITY_ENABLED_FEATURES` | `backlinking` | Enable only Backlink Outreach + core services. |
|
||||
|
||||
When set to `backlinking`, only the backlink outreach router and its core dependencies are loaded. Other features (blog writer, podcast, SEO dashboard, etc.) are skipped — reducing startup time and memory usage.
|
||||
|
||||
!!! note "Multiple features"
|
||||
You can also enable a combination: `ALWRITY_ENABLED_FEATURES=core,backlinking` or `ALWRITY_ENABLED_FEATURES=podcast,backlinking`.
|
||||
|
||||
## Deployment Checklist
|
||||
|
||||
### Minimal Setup
|
||||
|
||||
@@ -54,13 +54,15 @@ backend/
|
||||
├── routers/
|
||||
│ └── backlink_outreach.py # 18+ API endpoints
|
||||
├── services/
|
||||
│ ├── backlink_outreach_service.py # Business logic, policy, analytics
|
||||
│ ├── backlink_outreach_storage.py # SQLite CRUD operations
|
||||
│ ├── backlink_outreach_sender.py # SMTP email delivery
|
||||
│ ├── backlink_outreach_reply_monitor.py # IMAP reply polling
|
||||
│ └── backlink_outreach_models.py # Pydantic request/response models
|
||||
│ ├── backlink_outreach_service.py # Business logic, policy, analytics
|
||||
│ ├── backlink_outreach_storage.py # SQLite CRUD operations
|
||||
│ ├── backlink_outreach_sender.py # SMTP email delivery with Message-ID
|
||||
│ ├── backlink_outreach_reply_monitor.py # IMAP reply polling with Message-ID matching
|
||||
│ ├── backlink_outreach_scraper.py # Deep website scraper (Exa + DuckDuckGo)
|
||||
│ ├── backlink_outreach_template_generator.py # LLM-based email copy generation
|
||||
│ └── backlink_outreach_models.py # Pydantic request/response models
|
||||
├── models/
|
||||
│ └── backlink_outreach_models.py # SQLAlchemy models + indexes
|
||||
│ └── backlink_outreach_models.py # SQLAlchemy models + indexes
|
||||
|
||||
frontend/src/
|
||||
├── components/
|
||||
@@ -109,6 +111,7 @@ erDiagram
|
||||
string body
|
||||
string status
|
||||
string legal_basis
|
||||
string message_id
|
||||
datetime sent_at
|
||||
}
|
||||
OutreachReply {
|
||||
@@ -217,10 +220,10 @@ SQLite CRUD operations with 20+ methods:
|
||||
- Campaign CRUD: `create_campaign`, `list_backlink_campaigns`, `get_campaign`, `delete_campaign`.
|
||||
- Lead management: `add_campaign_lead`, `add_campaign_leads_bulk`, `update_lead_status`, `bulk_update_lead_status`.
|
||||
- Outreach: `create_outreach_attempt`, `list_outreach_attempts`, `get_lead_attempts`.
|
||||
- Replies: `store_reply`, `find_attempt_by_from_email`, `reply_exists`, `list_replies`, `count_replies`.
|
||||
- Replies: `store_reply`, `find_attempt_by_from_email`, `find_attempt_by_message_id`, `reply_exists`, `list_replies`, `count_replies`.
|
||||
- Follow-ups: `create_follow_up`, `list_follow_ups`.
|
||||
- Suppression: `add_suppression`, `list_suppression`, `is_suppressed`.
|
||||
- Counters: `increment_user_counter`, `increment_domain_counter` (atomic ON CONFLICT).
|
||||
- Counters: `try_increment_user_send_counter`, `try_increment_domain_send_counter` (atomic ON CONFLICT — reserves cap slot before send).
|
||||
- Idempotency: `check_idempotency`, `mark_idempotency`.
|
||||
- Audit: `log_audit_entry`.
|
||||
- Templates: `create_email_template`, `list_email_templates`, `get_email_template`, `delete_email_template`.
|
||||
@@ -249,7 +252,7 @@ Handles IMAP reply processing:
|
||||
3. Searches for messages matching the outreach sender.
|
||||
4. Fetches up to `IMAP_FETCH_LIMIT` messages.
|
||||
5. Checks for duplicates via `reply_exists()`.
|
||||
6. Matches replies to attempts via `find_attempt_by_from_email()`.
|
||||
6. Matches replies to attempts via `find_attempt_by_message_id()` (primary, using `In-Reply-To`/`References` headers), falls back to `find_attempt_by_from_email()`.
|
||||
7. Classifies replies based on content analysis.
|
||||
8. Stores reply records.
|
||||
|
||||
|
||||
@@ -12,15 +12,16 @@ flowchart TD
|
||||
B --> C[Resolve Lead Email from DB]
|
||||
C --> D[Policy Validation]
|
||||
D -->|Approved| E[Create Outreach Attempt Record]
|
||||
D -->|Blocked| F[Record Audit Log + Return 403]
|
||||
E --> G[Send via SMTP with TLS]
|
||||
G -->|Success| H[Increment Counters]
|
||||
G -->|Success| I[Mark Idempotency Key]
|
||||
G -->|Success| J[Update Lead Status to Contacted]
|
||||
G -->|Failure| K[Return 500 with Generic Error]
|
||||
H --> L[Return 200 with Attempt Details]
|
||||
I --> L
|
||||
J --> L
|
||||
D -->|Blocked| F[Record Audit Log + Return 403]
|
||||
E --> G[Reserve Daily Cap Slots Atomically]
|
||||
G --> H[Send via SMTP with TLS + Message-ID]
|
||||
H -->|Success| I[Store Message-ID on Attempt Record]
|
||||
H -->|Success| J[Mark Idempotency Key]
|
||||
H -->|Success| K[Update Lead Status to Contacted]
|
||||
H -->|Failure| L[Return 500 with Generic Error]
|
||||
I --> M[Return 200 with Attempt Details]
|
||||
J --> M
|
||||
K --> M
|
||||
|
||||
style D fill:#fff3e0
|
||||
style G fill:#e3f2fd
|
||||
@@ -28,7 +29,7 @@ flowchart TD
|
||||
```
|
||||
|
||||
!!! warning "Counter timing"
|
||||
Counters and idempotency keys are marked **only after successful SMTP delivery**, never before. This prevents false cap consumption on failed sends.
|
||||
Daily cap slots are **reserved atomically before sending** via `try_increment_user_send_counter` and `try_increment_domain_send_counter`. If SMTP delivery fails, one slot is consumed (the cap check and increment happen in the same transaction). Idempotency keys are marked only after successful delivery.
|
||||
|
||||
## Policy Validation
|
||||
|
||||
@@ -40,6 +41,7 @@ Before every send, the system validates:
|
||||
| **Daily domain cap** | Max 20 emails/domain/day | Block + audit |
|
||||
| **Suppression list** | Recipient not suppressed | Block + audit |
|
||||
| **Idempotency** | No duplicate `(sender, recipient, subject)` in 24h | Block + audit |
|
||||
| **Sender alias** | `sender_email` must match `SMTP_ALLOWED_FROM_EMAILS` pattern | Block + fallback to `SMTP_FROM_EMAIL` |
|
||||
| **Legal basis** | EU domains → "consent", others → "legitimate_interest" | Auto-assign |
|
||||
|
||||
**API:** `POST /api/v1/backlink-outreach/policy/validate`
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
---
|
||||
description: ALwrity Backlink Outreach - AI-powered backlink discovery, outreach automation, and campaign management.
|
||||
---
|
||||
|
||||
# Backlink Outreach Overview
|
||||
|
||||
Backlink Outreach is an AI-powered guest post outreach platform that takes you from opportunity discovery to published backlink — with smart email composition, policy-safe sending, IMAP reply monitoring, and full campaign analytics.
|
||||
|
||||
@@ -44,15 +44,18 @@ The reply monitor:
|
||||
3. Searches for messages sent to your outreach address.
|
||||
4. Fetches up to `IMAP_FETCH_LIMIT` recent messages.
|
||||
5. For each message, checks if it's already been processed (deduplication).
|
||||
6. Matches the reply to an existing outreach attempt by sender email.
|
||||
6. Matches the reply to an existing outreach attempt (Message-ID first, sender email fallback).
|
||||
7. Classifies the reply and stores it.
|
||||
|
||||
### Reply Matching
|
||||
|
||||
Replies are matched to outreach attempts using the `from_email` field:
|
||||
Replies are matched to outreach attempts using a two-stage strategy:
|
||||
|
||||
- The system looks up `find_attempt_by_from_email(from_email)` to find the most recent outreach attempt sent to that email address.
|
||||
- If no match is found, the reply is still stored but not linked to an attempt.
|
||||
1. **Message-ID matching (primary)**: Each sent email includes a unique `Message-ID` header. When the recipient replies, their email client includes the original `Message-ID` in `In-Reply-To` and `References` headers. The system extracts these and looks up `find_attempt_by_message_id(in_reply_to)` to find the exact outreach attempt.
|
||||
|
||||
2. **Sender email fallback**: If no Message-ID match is found (e.g., the reply client stripped headers), the system falls back to `find_attempt_by_from_email(from_email)` to find the most recent attempt sent to that address.
|
||||
|
||||
3. **Unmatched replies**: If neither strategy produces a match, the reply is still stored but not linked to an attempt.
|
||||
|
||||
### Deduplication
|
||||
|
||||
|
||||
Reference in New Issue
Block a user