feat: SEO schema, Word Count Goals, Internal Linking targets, and Admin UI updates
This commit is contained in:
192
CAMPAIGN_SETUP_GUIDE.md
Normal file
192
CAMPAIGN_SETUP_GUIDE.md
Normal file
@@ -0,0 +1,192 @@
|
||||
# Spark Platform: Master Data & Campaign Manual
|
||||
|
||||
This manual provides comprehensive instructions for managing the "Brains" of your content factory. It covers data import formats, advanced Spintax usage, Geo-Intelligence configuration, and full Content Framework management.
|
||||
|
||||
---
|
||||
|
||||
## 📚 1. Content Intelligence Mastery
|
||||
|
||||
### A. Spintax & Variables Guide
|
||||
Spintax (Spin Syntax) allows you to create dynamic variations of text. The engine processes this *before* it processes variables.
|
||||
|
||||
#### **Basic Syntax**
|
||||
* **Format:** `{Option A|Option B|Option C}`
|
||||
* **Example:** `{Best|Top Rated|Premier}` Solar Installer
|
||||
* **Output:** Randomly selects one option.
|
||||
|
||||
#### **Nested Spintax (Advanced)**
|
||||
You can nest options inside others for exponential variations.
|
||||
* **Example:** `{Get {Started|Going}|Start Your Journey}` today.
|
||||
* **Logic:**
|
||||
* 50% Direct: "Start Your Journey today"
|
||||
* 50% Nested:
|
||||
* 25%: "Get Started today"
|
||||
* 25%: "Get Going today"
|
||||
|
||||
#### **System Variables**
|
||||
These are placeholders that the Generator replaces automatically based on the context (Campaign settings, Location, or Current Time).
|
||||
|
||||
| Variable | Description | Example Output |
|
||||
| :--- | :--- | :--- |
|
||||
| `{City}` | Target City Name | "Austin" |
|
||||
| `{State}` | Full State Name | "Texas" |
|
||||
| `{State_Abbr}` | 2-Letter Code | "TX" |
|
||||
| `{County}` | Target County | "Travis County" |
|
||||
| `{Year}` | Current Year | "2025" |
|
||||
| `{Month}` | Current Month | "December" |
|
||||
| `{Niche}` | Campaign Niche (if set) | "Solar" |
|
||||
| `{Avatar_Name}` | Targeted Avatar | "The Skeptic" |
|
||||
|
||||
#### **Example Usage in a Headline:**
|
||||
`"{Top Rated|Best} {Niche} Services in {City}, {State_Abbr} - {Year} Update"`
|
||||
* *Output 1:* "Top Rated Solar Services in Austin, TX - 2025 Update"
|
||||
* *Output 2:* "Best Solar Services in Miami, FL - 2025 Update"
|
||||
|
||||
---
|
||||
|
||||
## 🛠 2. Data Import Templates (JSON)
|
||||
|
||||
Use the **Import / Export** feature in the Directus Admin sidebar for each collection. Ensure you import as **JSON**.
|
||||
|
||||
### A. Headline Inventory
|
||||
*Collection:* `headline_inventory`
|
||||
*Advanced Feature:* You can include variables in your headlines.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"final_title_text": "The {Ultimate|Definitive} Guide to {Niche} in {City}",
|
||||
"status": "available",
|
||||
"notes": "Good for long-form SEO landers"
|
||||
},
|
||||
{
|
||||
"final_title_text": "How {City} Residents Are Saving Money in {Year}",
|
||||
"status": "available"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### B. Content Fragments (With HTML & Lists)
|
||||
*Collection:* `content_fragments`
|
||||
*Power User Tip:* You can use full HTML, including bullet lists `<ul>`, bolding `<strong>`, and Spintax within the HTML.
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"fragment_type": "pillar_advantages",
|
||||
"content_body": "<h3>Why Choose Us In {City}?</h3><p>We provide:</p><ul><li><strong>Speed:</strong> {Fast|Quick} installation.</li><li><strong>Value:</strong> Best rates in {State}.</li><li><strong>Trust:</strong> 5-Star rated.</li></ul>",
|
||||
"word_count": 50
|
||||
},
|
||||
{
|
||||
"fragment_type": "intro_hook",
|
||||
"content_body": "<p>Are you tired of high energy bills in <strong>{City}</strong>? You aren't alone. {Thousands|Hundreds} of homeowners have made the switch.</p>"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
### C. Geo Intelligence (Counties, Landmarks & Metadata)
|
||||
*Collection:* `geo_intelligence`
|
||||
*Advanced Feature:* You can add arbitrary metadata like `landmarks`, `neighborhoods`, or `weather_zone` to the `data` blob. The engine (if configured) can read these.
|
||||
|
||||
**Example: Bulk Importing a County with City Landmarks**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"cluster_key": "orange_county_ca_attractions",
|
||||
"data": {
|
||||
"cluster_name": "Orange County Metro",
|
||||
"region_type": "county_cluster",
|
||||
"cities": [
|
||||
{
|
||||
"city": "Anaheim",
|
||||
"state": "CA",
|
||||
"zip_focus": "92801",
|
||||
"landmarks": ["Disneyland", "Angel Stadium", "Honda Center"],
|
||||
"population": "346,000"
|
||||
},
|
||||
{
|
||||
"city": "Irvine",
|
||||
"state": "CA",
|
||||
"zip_focus": "92602",
|
||||
"landmarks": ["UC Irvine", "Spectrum Center", "Great Park"],
|
||||
"population": "307,000"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
*Note: This allows you to write Spintax like "Located near {Landmark}" if your engine logic supports picking a random landmark from the current city data.*
|
||||
|
||||
### D. Complete Frameworks (Article Templates)
|
||||
*Collection:* `article_templates`
|
||||
*Concept:* A "Framework" is a blueprint that tells the engine which Fragments to assemble and in what order.
|
||||
|
||||
**Example: "The Authority Framework"**
|
||||
```json
|
||||
[
|
||||
{
|
||||
"name": "Authority SEO Framework",
|
||||
"structure_json": [
|
||||
"intro_hook",
|
||||
"image_hero",
|
||||
"pillar_1_keyword",
|
||||
"ad_block_mid",
|
||||
"pillar_2_uniqueness",
|
||||
"pillar_3_relevance",
|
||||
"faq_section",
|
||||
"cta_footer"
|
||||
],
|
||||
"description": "Standard high-ranking structure for local service pages."
|
||||
},
|
||||
{
|
||||
"name": "Quick Lead Magnet Framework",
|
||||
"structure_json": [
|
||||
"intro_hook_aggressive",
|
||||
"form_embed_top",
|
||||
"social_proof_slider",
|
||||
"pillar_benefits_bullets",
|
||||
"cta_sticky_bottom"
|
||||
],
|
||||
"description": "Short form page designed for PPC traffic."
|
||||
}
|
||||
]
|
||||
```
|
||||
*To use this, you would ensure you have `content_fragments` matching these types (e.g., `intro_hook`, `pillar_1_keyword`) available in the library.*
|
||||
|
||||
---
|
||||
|
||||
## ⚙️ 3. Managing Your Data (CRUD)
|
||||
|
||||
### How to Import
|
||||
1. Navigate to the Collection in Directus (e.g., "Geo Intelligence").
|
||||
2. Look for the **Import / Export** option in the right sidebar (often a box arrow icon).
|
||||
3. Select **Import**.
|
||||
4. Upload your `.json` file.
|
||||
5. Click **Start Import**. The system will notify you of success or errors.
|
||||
|
||||
### How to Edit
|
||||
1. Click on any row in the Collection list.
|
||||
2. Edit the fields directly in the form (e.g., fix a typo in `final_title_text`).
|
||||
3. Click the **Checkmark** (Save) in the top right.
|
||||
|
||||
### How to Delete
|
||||
1. **Single:** Click the item, then click the **Trash Can** (Delete) icon in the toolbar.
|
||||
2. **Bulk:** Select multiple checkboxes on the left side of the list view. A red "Delete" button will appear in the header. Click specific items or "Select All" to wipe a test batch.
|
||||
|
||||
### How to Export (Backup)
|
||||
1. Using the same **Import / Export** menu, select **Export**.
|
||||
2. Choose **JSON**.
|
||||
3. Dowload the file. Use this to backup your "Frameworks" or "Geo Clusters" before making bulk changes.
|
||||
|
||||
---
|
||||
|
||||
## 🚀 4. Workflow: Importing a Complete Campaign Strategy
|
||||
|
||||
If you want to move a strategy from one environment (e.g., Test) to another (e.g., Prod), follow this order:
|
||||
|
||||
1. **Step 1: Import Avatars** (`avatar_intelligence`) - Defines WHO you are targeting.
|
||||
2. **Step 2: Import Geo Data** (`geo_intelligence`) - Defines WHERE you are targeting.
|
||||
3. **Step 3: Import Fragments** (`content_fragments`) - Imports the raw text blocks.
|
||||
4. **Step 4: Import Templates** (`article_templates`) - Defines the structure (HOW to assemble Fragments).
|
||||
5. **Step 5: Create Campaign** (`campaign_masters`) - Link the Template, Geo Cluster, and Avatar together to start generating.
|
||||
62
extra-schema-updates.sql
Normal file
62
extra-schema-updates.sql
Normal file
@@ -0,0 +1,62 @@
|
||||
-- Add Schema JSON fields for SEO
|
||||
ALTER TABLE posts ADD COLUMN IF NOT EXISTS schema_json JSONB;
|
||||
|
||||
ALTER TABLE pages ADD COLUMN IF NOT EXISTS schema_json JSONB;
|
||||
|
||||
ALTER TABLE generated_articles
|
||||
ADD COLUMN IF NOT EXISTS schema_json JSONB;
|
||||
|
||||
-- Add Word Count Goal to Campaign
|
||||
ALTER TABLE campaign_masters
|
||||
ADD COLUMN IF NOT EXISTS target_word_count INTEGER DEFAULT 1500;
|
||||
|
||||
-- Create Link Targets table for Internal Linking Engine
|
||||
CREATE TABLE IF NOT EXISTS link_targets (
|
||||
id UUID PRIMARY KEY DEFAULT gen_random_uuid (),
|
||||
site VARCHAR(255),
|
||||
target_url VARCHAR(255),
|
||||
target_post UUID,
|
||||
anchor_text VARCHAR(255),
|
||||
anchor_variations JSONB,
|
||||
priority INTEGER DEFAULT 5,
|
||||
is_active BOOLEAN DEFAULT TRUE,
|
||||
is_hub BOOLEAN DEFAULT FALSE,
|
||||
max_per_article INTEGER DEFAULT 2,
|
||||
date_created TIMESTAMP DEFAULT CURRENT_TIMESTAMP
|
||||
);
|
||||
|
||||
-- Attempt to improve Admin UI Inputs via Directus Meta Tables
|
||||
-- (This assumes standard Directus 10 schema structure)
|
||||
-- Make 'site' a dropdown
|
||||
UPDATE directus_fields
|
||||
SET
|
||||
interface = 'select-dropdown'
|
||||
WHERE
|
||||
field = 'site'
|
||||
AND collection IN (
|
||||
'posts',
|
||||
'pages',
|
||||
'campaign_masters',
|
||||
'generation_jobs'
|
||||
);
|
||||
|
||||
-- Make 'campaign' a dropdown
|
||||
UPDATE directus_fields
|
||||
SET
|
||||
interface = 'select-dropdown-m2o'
|
||||
WHERE
|
||||
field = 'campaign'
|
||||
AND collection IN (
|
||||
'generated_articles',
|
||||
'headline_inventory',
|
||||
'content_fragments'
|
||||
);
|
||||
|
||||
-- Make 'status' a color badge dropdown
|
||||
UPDATE directus_fields
|
||||
SET
|
||||
interface = 'select-dropdown',
|
||||
options = '{"choices":[{"text":"Published","value":"published","color":"#2ECDA7"},{"text":"Draft","value":"draft","color":"#D3D3D3"}]}'
|
||||
WHERE
|
||||
field = 'status'
|
||||
AND collection IN ('posts', 'pages');
|
||||
@@ -79,15 +79,15 @@ export default function ContentFactoryDashboard() {
|
||||
{/* Header Actions */}
|
||||
<div className="flex justify-between items-center">
|
||||
<div>
|
||||
<h2 className="text-xl font-bold text-white">Production Overview</h2>
|
||||
<p className="text-slate-400">Monitoring Content Velocity & Integrity</p>
|
||||
<h2 className="text-xl font-bold text-white">Tactical Command Center</h2>
|
||||
<p className="text-slate-400">Shields (SEO Defense) & Weapons (Content Offense) Status</p>
|
||||
</div>
|
||||
<div className="flex gap-4">
|
||||
<Button variant="outline" className="text-slate-200 border-slate-700 hover:bg-slate-800" onClick={() => window.open(`${DIRECTUS_ADMIN_URL}/content/posts`, '_blank')}>
|
||||
Manage Articles (Backend)
|
||||
Manage Arsenal (Posts)
|
||||
</Button>
|
||||
<Button className="bg-blue-600 hover:bg-blue-700" onClick={() => window.open(`${DIRECTUS_ADMIN_URL}`, '_blank')}>
|
||||
Open Directus Admin ↗
|
||||
Open HQ (Directus) ↗
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -96,7 +96,7 @@ export default function ContentFactoryDashboard() {
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<Card className="bg-slate-900 border-slate-800">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-400">Total Articles</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-slate-400">Total Units (Articles)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">{stats.total}</div>
|
||||
@@ -104,7 +104,7 @@ export default function ContentFactoryDashboard() {
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800 border-l-4 border-l-purple-500">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-400">Ghost (Staged)</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-slate-400">Stealth (Ghost)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">{stats.ghost}</div>
|
||||
@@ -112,7 +112,7 @@ export default function ContentFactoryDashboard() {
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800 border-l-4 border-l-green-500">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-400">Indexed (Live)</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-slate-400">Deployed (Live)</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">{stats.indexed}</div>
|
||||
@@ -120,7 +120,7 @@ export default function ContentFactoryDashboard() {
|
||||
</Card>
|
||||
<Card className="bg-slate-900 border-slate-800 border-l-4 border-l-blue-500">
|
||||
<CardHeader className="pb-2">
|
||||
<CardTitle className="text-sm font-medium text-slate-400">Active Jobs</CardTitle>
|
||||
<CardTitle className="text-sm font-medium text-slate-400">Active Operations</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<div className="text-2xl font-bold text-white">{queues.filter(q => q.status === 'Processing').length}</div>
|
||||
|
||||
@@ -8,6 +8,7 @@ interface Props {
|
||||
globals?: Globals;
|
||||
navigation?: Navigation[];
|
||||
canonical?: string;
|
||||
schemaJson?: Record<string, any>;
|
||||
}
|
||||
|
||||
const {
|
||||
@@ -16,7 +17,8 @@ const {
|
||||
image,
|
||||
globals,
|
||||
navigation = [],
|
||||
canonical
|
||||
canonical,
|
||||
schemaJson
|
||||
} = Astro.props;
|
||||
|
||||
const siteUrl = Astro.url.origin;
|
||||
@@ -63,6 +65,11 @@ const ogImage = image || globals?.logo || '';
|
||||
<!-- Head Scripts -->
|
||||
{globals?.scripts_head && <Fragment set:html={globals.scripts_head} />}
|
||||
|
||||
<!-- JSON-LD Schema -->
|
||||
{schemaJson && (
|
||||
<script type="application/ld+json" set:html={JSON.stringify(schemaJson)} />
|
||||
)}
|
||||
|
||||
<!-- Styles -->
|
||||
<style is:global>
|
||||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap');
|
||||
|
||||
@@ -218,6 +218,34 @@ export async function fetchGeneratedArticles(
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch a single generated article by slug
|
||||
*/
|
||||
export async function fetchGeneratedArticleBySlug(
|
||||
slug: string,
|
||||
siteId: string
|
||||
): Promise<any | null> {
|
||||
try {
|
||||
const articles = await directus.request(
|
||||
readItems('generated_articles', {
|
||||
filter: {
|
||||
_and: [
|
||||
{ slug: { _eq: slug } },
|
||||
{ site: { _eq: siteId } },
|
||||
{ is_published: { _eq: true } }
|
||||
]
|
||||
},
|
||||
limit: 1,
|
||||
fields: ['*']
|
||||
})
|
||||
);
|
||||
return articles?.[0] || null;
|
||||
} catch (err) {
|
||||
console.error('Error fetching generated article:', err);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch SEO campaigns
|
||||
*/
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
---
|
||||
import BaseLayout from '../layouts/BaseLayout.astro';
|
||||
import { fetchPageByPermalink, fetchSiteGlobals, fetchNavigation } from '../lib/directus/fetchers';
|
||||
import { fetchPageByPermalink, fetchSiteGlobals, fetchNavigation, fetchGeneratedArticleBySlug } from '../lib/directus/fetchers';
|
||||
import BlockHero from '../components/blocks/BlockHero.astro';
|
||||
import BlockRichText from '../components/blocks/BlockRichText.astro';
|
||||
import BlockColumns from '../components/blocks/BlockColumns.astro';
|
||||
@@ -13,16 +13,18 @@ import BlockPosts from '../components/blocks/BlockPosts.astro';
|
||||
import BlockForm from '../components/blocks/BlockForm.astro';
|
||||
|
||||
const siteId = Astro.locals.siteId;
|
||||
const permalink = '/' + (Astro.params.slug || '');
|
||||
const slug = Astro.params.slug || '';
|
||||
const permalink = '/' + slug;
|
||||
|
||||
// Fetch data
|
||||
const [globals, navigation, page] = await Promise.all([
|
||||
const [globals, navigation, page, generatedArticle] = await Promise.all([
|
||||
siteId ? fetchSiteGlobals(siteId) : null,
|
||||
siteId ? fetchNavigation(siteId) : [],
|
||||
siteId ? fetchPageByPermalink(permalink, siteId) : null
|
||||
siteId ? fetchPageByPermalink(permalink, siteId) : null,
|
||||
siteId ? fetchGeneratedArticleBySlug(slug, siteId) : null
|
||||
]);
|
||||
|
||||
if (!page) {
|
||||
if (!page && !generatedArticle) {
|
||||
return Astro.redirect('/404');
|
||||
}
|
||||
|
||||
@@ -39,20 +41,31 @@ const blockComponents: Record<string, any> = {
|
||||
block_posts: BlockPosts,
|
||||
block_form: BlockForm,
|
||||
};
|
||||
|
||||
const activeEntity = page || generatedArticle;
|
||||
const isGenerated = !!generatedArticle && !page;
|
||||
---
|
||||
|
||||
<BaseLayout
|
||||
title={page.seo_title || page.title}
|
||||
description={page.seo_description}
|
||||
image={page.seo_image}
|
||||
title={activeEntity.seo_title || activeEntity.meta_title || activeEntity.title || activeEntity.headline}
|
||||
description={activeEntity.seo_description || activeEntity.meta_description}
|
||||
image={activeEntity.seo_image || (activeEntity.featured_image_filename ? `/assets/content/${activeEntity.featured_image_filename}` : undefined)}
|
||||
globals={globals}
|
||||
navigation={navigation}
|
||||
schemaJson={activeEntity.schema_json}
|
||||
>
|
||||
{page.blocks?.map((block) => {
|
||||
const Component = blockComponents[block.collection];
|
||||
if (Component) {
|
||||
return <Component {...block.item} />;
|
||||
}
|
||||
return null;
|
||||
})}
|
||||
{isGenerated ? (
|
||||
<article class="prose dark:prose-invert max-w-none container mx-auto px-4 py-8">
|
||||
<h1 class="text-4xl font-bold mb-6">{activeEntity.headline}</h1>
|
||||
<div set:html={activeEntity.full_html_body} />
|
||||
</article>
|
||||
) : (
|
||||
page.blocks?.map((block) => {
|
||||
const Component = blockComponents[block.collection];
|
||||
if (Component) {
|
||||
return <Component {...block.item} />;
|
||||
}
|
||||
return null;
|
||||
})
|
||||
)}
|
||||
</BaseLayout>
|
||||
|
||||
@@ -213,7 +213,26 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
: undefined
|
||||
});
|
||||
|
||||
// Create article record with featured image
|
||||
// Generate JSON-LD Schema
|
||||
const schemaJson = {
|
||||
"@context": "https://schema.org",
|
||||
"@type": "Article",
|
||||
"headline": processedHeadline,
|
||||
"description": metaDescription,
|
||||
"wordCount": wordCount,
|
||||
"datePublished": new Date().toISOString(),
|
||||
"author": {
|
||||
"@type": "Organization",
|
||||
"name": locationVars.state ? `${locationVars.state} Services` : "Local Service Provider"
|
||||
},
|
||||
"image": featuredImage.filename ? `/assets/content/${featuredImage.filename}` : undefined
|
||||
};
|
||||
|
||||
// Check Word Count Goal
|
||||
const targetWordCount = campaign.target_word_count || 1500;
|
||||
const wordCountStatus = wordCount >= targetWordCount ? 'optimal' : 'under_target';
|
||||
|
||||
// Create article record with featured image and schema
|
||||
const article = await directus.request(
|
||||
createItem('generated_articles', {
|
||||
site: siteId || campaign.site,
|
||||
@@ -223,13 +242,15 @@ export const POST: APIRoute = async ({ request, locals }) => {
|
||||
meta_description: metaDescription,
|
||||
full_html_body: fullHtmlBody,
|
||||
word_count: wordCount,
|
||||
word_count_status: wordCountStatus,
|
||||
is_published: false,
|
||||
location_city: locationVars.city || null,
|
||||
location_county: locationVars.county || null,
|
||||
location_state: locationVars.state || null,
|
||||
featured_image_svg: featuredImage.svg,
|
||||
featured_image_filename: featuredImage.filename,
|
||||
featured_image_alt: featuredImage.alt
|
||||
featured_image_alt: featuredImage.alt,
|
||||
schema_json: schemaJson
|
||||
})
|
||||
);
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ export interface CampaignMaster {
|
||||
location_target?: string;
|
||||
batch_count?: number;
|
||||
status: 'active' | 'paused' | 'completed';
|
||||
target_word_count?: number;
|
||||
date_created?: string;
|
||||
}
|
||||
|
||||
@@ -222,6 +223,7 @@ export interface GeneratedArticle {
|
||||
meta_desc?: string;
|
||||
is_published?: boolean;
|
||||
sync_status?: string;
|
||||
schema_json?: Record<string, any>;
|
||||
date_created?: string;
|
||||
}
|
||||
|
||||
@@ -262,4 +264,18 @@ export interface SparkSchema {
|
||||
newsletter_subscribers: NewsletterSubscriber[];
|
||||
forms: Form[];
|
||||
form_submissions: FormSubmission[];
|
||||
link_targets: LinkTarget[];
|
||||
}
|
||||
|
||||
export interface LinkTarget {
|
||||
id: string;
|
||||
site: string;
|
||||
target_url?: string;
|
||||
target_post?: string;
|
||||
anchor_text: string;
|
||||
anchor_variations?: string[];
|
||||
priority?: number;
|
||||
is_active?: boolean;
|
||||
is_hub?: boolean;
|
||||
max_per_article?: number;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user