feat: SEO schema, Word Count Goals, Internal Linking targets, and Admin UI updates

This commit is contained in:
cawcenter
2025-12-12 23:36:22 -05:00
parent f6041af538
commit ad1e1705b7
8 changed files with 365 additions and 26 deletions

View File

@@ -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>

View File

@@ -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');

View File

@@ -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
*/

View File

@@ -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>

View File

@@ -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
})
);

View File

@@ -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;
}