This commit is contained in:
2026-07-11 01:28:32 +02:00
parent 0a6176cf53
commit 8e01c7c8ec
99 changed files with 34739 additions and 0 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.9 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 63 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 589 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 531 KiB

@@ -0,0 +1,235 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1920 1080">
<defs>
<!-- Background gradient -->
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#7C3AED"/>
<stop offset="100%" style="stop-color:#5B21B6"/>
</linearGradient>
<!-- Document gradient -->
<linearGradient id="docGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#FFFFFF"/>
<stop offset="100%" style="stop-color:#F3F4F6"/>
</linearGradient>
<!-- Glow effect for AI -->
<filter id="glow" x="-50%" y="-50%" width="200%" height="200%">
<feGaussianBlur stdDeviation="8" result="coloredBlur"/>
<feMerge>
<feMergeNode in="coloredBlur"/>
<feMergeNode in="SourceGraphic"/>
</feMerge>
</filter>
<!-- Soft shadow -->
<filter id="shadow" x="-20%" y="-20%" width="140%" height="140%">
<feDropShadow dx="0" dy="8" stdDeviation="12" flood-color="#000000" flood-opacity="0.25"/>
</filter>
<!-- Arrow gradient -->
<linearGradient id="arrowGradient" x1="0%" y1="0%" x2="100%" y2="0%">
<stop offset="0%" style="stop-color:#FBBF24"/>
<stop offset="100%" style="stop-color:#F59E0B"/>
</linearGradient>
</defs>
<!-- Background -->
<rect width="1920" height="1080" fill="url(#bgGradient)"/>
<!-- Subtle pattern overlay -->
<g opacity="0.05">
<circle cx="200" cy="200" r="300" fill="#FFFFFF"/>
<circle cx="1700" cy="900" r="400" fill="#FFFFFF"/>
<circle cx="1000" cy="100" r="200" fill="#FFFFFF"/>
</g>
<!-- ==================== LEFT SIDE: INPUT ==================== -->
<!-- Input document/card -->
<g filter="url(#shadow)">
<rect x="120" y="280" width="380" height="480" rx="20" fill="url(#docGradient)"/>
</g>
<!-- Input header -->
<rect x="120" y="280" width="380" height="70" rx="20" fill="#7C3AED"/>
<rect x="120" y="320" width="380" height="30" fill="#7C3AED"/>
<text x="310" y="325" text-anchor="middle" fill="#FFFFFF" font-family="system-ui, -apple-system, sans-serif" font-size="24" font-weight="600">INPUT</text>
<!-- Keywords label -->
<text x="160" y="400" fill="#6B7280" font-family="system-ui, sans-serif" font-size="18" font-weight="500">Keywords</text>
<!-- Keyword tags -->
<rect x="160" y="415" width="100" height="32" rx="16" fill="#EDE9FE"/>
<text x="210" y="437" text-anchor="middle" fill="#7C3AED" font-family="system-ui, sans-serif" font-size="14" font-weight="500">SEO</text>
<rect x="270" y="415" width="120" height="32" rx="16" fill="#EDE9FE"/>
<text x="330" y="437" text-anchor="middle" fill="#7C3AED" font-family="system-ui, sans-serif" font-size="14" font-weight="500">Content</text>
<rect x="160" y="455" width="140" height="32" rx="16" fill="#EDE9FE"/>
<text x="230" y="477" text-anchor="middle" fill="#7C3AED" font-family="system-ui, sans-serif" font-size="14" font-weight="500">Marketing</text>
<!-- Topic label -->
<text x="160" y="530" fill="#6B7280" font-family="system-ui, sans-serif" font-size="18" font-weight="500">Topic</text>
<!-- Topic input box -->
<rect x="160" y="545" width="300" height="45" rx="8" fill="#F3F4F6" stroke="#E5E7EB" stroke-width="2"/>
<text x="180" y="575" fill="#374151" font-family="system-ui, sans-serif" font-size="16">"Product Launch Blog"</text>
<!-- Tone label -->
<text x="160" y="630" fill="#6B7280" font-family="system-ui, sans-serif" font-size="18" font-weight="500">Tone</text>
<!-- Tone selector -->
<rect x="160" y="645" width="140" height="40" rx="8" fill="#7C3AED"/>
<text x="230" y="672" text-anchor="middle" fill="#FFFFFF" font-family="system-ui, sans-serif" font-size="15" font-weight="500">Professional</text>
<rect x="310" y="645" width="140" height="40" rx="8" fill="#F3F4F6" stroke="#E5E7EB" stroke-width="2"/>
<text x="380" y="672" text-anchor="middle" fill="#6B7280" font-family="system-ui, sans-serif" font-size="15">Casual</text>
<!-- ==================== CENTER: AI TRANSFORMATION ==================== -->
<!-- Flow arrow left -->
<path d="M540 520 L620 520" stroke="url(#arrowGradient)" stroke-width="6" stroke-linecap="round" fill="none"/>
<polygon points="630,520 610,508 610,532" fill="#FBBF24"/>
<!-- AI Brain/Magic circle -->
<g filter="url(#glow)">
<circle cx="810" cy="520" r="140" fill="#FFFFFF" opacity="0.15"/>
<circle cx="810" cy="520" r="110" fill="#FFFFFF" opacity="0.25"/>
<circle cx="810" cy="520" r="80" fill="#FFFFFF"/>
</g>
<!-- AI Icon - Brain with circuit pattern -->
<g transform="translate(810, 520)">
<!-- Brain outline -->
<path d="M-35 -10 C-35 -35 -15 -50 5 -50 C25 -50 40 -35 40 -15 C55 -15 60 5 50 20 C60 35 50 55 30 55 C30 55 20 60 0 60 C-20 60 -30 55 -30 55 C-50 55 -60 35 -50 20 C-60 5 -55 -15 -35 -10 Z"
fill="none" stroke="#7C3AED" stroke-width="4"/>
<!-- Circuit dots -->
<circle cx="-15" cy="-25" r="6" fill="#7C3AED"/>
<circle cx="15" cy="-20" r="6" fill="#A78BFA"/>
<circle cx="-5" cy="5" r="8" fill="#7C3AED"/>
<circle cx="25" cy="10" r="6" fill="#A78BFA"/>
<circle cx="-20" cy="25" r="6" fill="#7C3AED"/>
<circle cx="10" cy="35" r="6" fill="#A78BFA"/>
<!-- Circuit lines -->
<path d="M-15 -25 L-5 5 M15 -20 L-5 5 M-5 5 L25 10 M-5 5 L-20 25 M-5 5 L10 35"
stroke="#7C3AED" stroke-width="2" opacity="0.6"/>
</g>
<!-- AI Sparkles around brain -->
<g filter="url(#glow)">
<!-- Large sparkle top right -->
<g transform="translate(920, 400)">
<path d="M0 -25 L7 -7 L25 0 L7 7 L0 25 L-7 7 L-25 0 L-7 -7 Z" fill="#FBBF24"/>
<circle cx="0" cy="0" r="6" fill="#FFFFFF"/>
</g>
<!-- Medium sparkle top left -->
<g transform="translate(700, 420)">
<path d="M0 -18 L5 -5 L18 0 L5 5 L0 18 L-5 5 L-18 0 L-5 -5 Z" fill="#FBBF24"/>
<circle cx="0" cy="0" r="4" fill="#FFFFFF"/>
</g>
<!-- Small sparkle bottom -->
<g transform="translate(850, 650)">
<path d="M0 -15 L4 -4 L15 0 L4 4 L0 15 L-4 4 L-15 0 L-4 -4 Z" fill="#FBBF24" opacity="0.9"/>
</g>
<!-- Tiny sparkles -->
<g transform="translate(740, 600) scale(0.6)">
<path d="M0 -15 L4 -4 L15 0 L4 4 L0 15 L-4 4 L-15 0 L-4 -4 Z" fill="#FBBF24" opacity="0.7"/>
</g>
<g transform="translate(900, 480) scale(0.5)">
<path d="M0 -15 L4 -4 L15 0 L4 4 L0 15 L-4 4 L-15 0 L-4 -4 Z" fill="#FBBF24" opacity="0.8"/>
</g>
</g>
<!-- "AI" label -->
<text x="810" y="720" text-anchor="middle" fill="#FFFFFF" font-family="system-ui, sans-serif" font-size="28" font-weight="700" opacity="0.9">AI-Powered</text>
<!-- Flow arrow right -->
<path d="M1000 520 L1080 520" stroke="url(#arrowGradient)" stroke-width="6" stroke-linecap="round" fill="none"/>
<polygon points="1090,520 1070,508 1070,532" fill="#FBBF24"/>
<!-- ==================== RIGHT SIDE: OUTPUT ==================== -->
<!-- Output document -->
<g filter="url(#shadow)">
<rect x="1140" y="200" width="420" height="560" rx="20" fill="url(#docGradient)"/>
</g>
<!-- Output header -->
<rect x="1140" y="200" width="420" height="70" rx="20" fill="#10B981"/>
<rect x="1140" y="240" width="420" height="30" fill="#10B981"/>
<text x="1350" y="245" text-anchor="middle" fill="#FFFFFF" font-family="system-ui, -apple-system, sans-serif" font-size="24" font-weight="600">SEO OPTIMIZED</text>
<!-- Generated title -->
<rect x="1180" y="300" width="340" height="20" rx="4" fill="#7C3AED"/>
<rect x="1180" y="330" width="280" height="14" rx="3" fill="#9CA3AF"/>
<!-- Content preview lines -->
<rect x="1180" y="370" width="340" height="10" rx="3" fill="#D1D5DB"/>
<rect x="1180" y="390" width="320" height="10" rx="3" fill="#D1D5DB"/>
<rect x="1180" y="410" width="340" height="10" rx="3" fill="#D1D5DB"/>
<rect x="1180" y="430" width="280" height="10" rx="3" fill="#D1D5DB"/>
<!-- Subheading -->
<rect x="1180" y="465" width="200" height="16" rx="4" fill="#7C3AED" opacity="0.7"/>
<!-- More content lines -->
<rect x="1180" y="495" width="340" height="10" rx="3" fill="#D1D5DB"/>
<rect x="1180" y="515" width="300" height="10" rx="3" fill="#D1D5DB"/>
<rect x="1180" y="535" width="340" height="10" rx="3" fill="#D1D5DB"/>
<!-- SEO Score Circle -->
<g transform="translate(1470, 670)">
<circle cx="0" cy="0" r="70" fill="#FFFFFF" stroke="#10B981" stroke-width="8"/>
<text x="0" y="10" text-anchor="middle" fill="#10B981" font-family="system-ui, sans-serif" font-size="42" font-weight="700">92</text>
<text x="0" y="35" text-anchor="middle" fill="#6B7280" font-family="system-ui, sans-serif" font-size="14">SEO Score</text>
</g>
<!-- Mini chart bars -->
<g transform="translate(1200, 620)">
<rect x="0" y="40" width="25" height="60" rx="4" fill="#7C3AED"/>
<rect x="35" y="25" width="25" height="75" rx="4" fill="#A78BFA"/>
<rect x="70" y="10" width="25" height="90" rx="4" fill="#10B981"/>
<rect x="105" y="30" width="25" height="70" rx="4" fill="#7C3AED"/>
<!-- Trend line -->
<path d="M12 35 L47 20 L82 5 L117 25" stroke="#FBBF24" stroke-width="3" fill="none" stroke-linecap="round"/>
<circle cx="82" cy="5" r="5" fill="#FBBF24"/>
</g>
<!-- Checkmarks for SEO items -->
<g transform="translate(1180, 580)">
<circle cx="10" cy="10" r="10" fill="#10B981"/>
<path d="M5 10 L9 14 L16 6" stroke="#FFFFFF" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round"/>
<text x="30" y="15" fill="#374151" font-family="system-ui, sans-serif" font-size="13">Meta optimized</text>
</g>
<!-- ==================== BOTTOM: TITLE & TAGLINE ==================== -->
<!-- Module name -->
<text x="960" y="920" text-anchor="middle" fill="#FFFFFF" font-family="system-ui, -apple-system, BlinkMacSystemFont, sans-serif" font-size="56" font-weight="700">SEO Content Generator</text>
<!-- Tagline -->
<text x="960" y="980" text-anchor="middle" fill="#FFFFFF" font-family="system-ui, -apple-system, sans-serif" font-size="28" font-weight="400" opacity="0.9">AI-Powered Content Creation &amp; Optimization for Odoo</text>
<!-- Decorative bottom sparkles -->
<g transform="translate(550, 950) scale(0.4)">
<path d="M0 -20 L5 -5 L20 0 L5 5 L0 20 L-5 5 L-20 0 L-5 -5 Z" fill="#FBBF24" opacity="0.6"/>
</g>
<g transform="translate(1370, 940) scale(0.35)">
<path d="M0 -20 L5 -5 L20 0 L5 5 L0 20 L-5 5 L-20 0 L-5 -5 Z" fill="#FBBF24" opacity="0.6"/>
</g>
<!-- Corner accent sparkles -->
<g transform="translate(80, 100)">
<path d="M0 -30 L8 -8 L30 0 L8 8 L0 30 L-8 8 L-30 0 L-8 -8 Z" fill="#FBBF24" opacity="0.5"/>
</g>
<g transform="translate(1840, 980)">
<path d="M0 -25 L7 -7 L25 0 L7 7 L0 25 L-7 7 L-25 0 L-7 -7 Z" fill="#FBBF24" opacity="0.5"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,71 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512">
<defs>
<linearGradient id="bgGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#7C3AED;stop-opacity:1" />
<stop offset="100%" style="stop-color:#5B21B6;stop-opacity:1" />
</linearGradient>
<linearGradient id="docGradient" x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" style="stop-color:#FFFFFF;stop-opacity:1" />
<stop offset="100%" style="stop-color:#F3F4F6;stop-opacity:1" />
</linearGradient>
</defs>
<!-- Background rounded square -->
<rect x="20" y="20" width="472" height="472" rx="80" ry="80" fill="url(#bgGradient)"/>
<!-- Document/Paper -->
<path d="M140 100 L300 100 L360 160 L360 400 C360 415 348 427 333 427 L167 427 C152 427 140 415 140 400 L140 100 Z"
fill="url(#docGradient)"
stroke="#E5E7EB"
stroke-width="2"/>
<!-- Document fold -->
<path d="M300 100 L300 160 L360 160"
fill="#E5E7EB"
stroke="#D1D5DB"
stroke-width="2"/>
<!-- Text lines on document -->
<rect x="170" y="185" width="160" height="12" rx="6" fill="#7C3AED" opacity="0.8"/>
<rect x="170" y="215" width="130" height="10" rx="5" fill="#9CA3AF"/>
<rect x="170" y="240" width="150" height="10" rx="5" fill="#9CA3AF"/>
<rect x="170" y="265" width="120" height="10" rx="5" fill="#9CA3AF"/>
<rect x="170" y="300" width="160" height="12" rx="6" fill="#7C3AED" opacity="0.6"/>
<rect x="170" y="330" width="140" height="10" rx="5" fill="#9CA3AF"/>
<rect x="170" y="355" width="155" height="10" rx="5" fill="#9CA3AF"/>
<!-- SEO Chart/Analytics circle -->
<circle cx="380" cy="340" r="85" fill="#FFFFFF" stroke="#7C3AED" stroke-width="4"/>
<!-- Chart bars inside circle -->
<rect x="330" y="355" width="20" height="45" rx="4" fill="#7C3AED"/>
<rect x="360" y="325" width="20" height="75" rx="4" fill="#A78BFA"/>
<rect x="390" y="305" width="20" height="95" rx="4" fill="#7C3AED"/>
<rect x="420" y="340" width="20" height="60" rx="4" fill="#A78BFA"/>
<!-- Upward trend arrow -->
<path d="M335 320 L405 280 L405 300 L425 300 L425 265 L390 265 L390 285 L340 315"
fill="none"
stroke="#10B981"
stroke-width="6"
stroke-linecap="round"
stroke-linejoin="round"/>
<polygon points="425,265 440,280 425,280" fill="#10B981"/>
<!-- AI Sparkle/Star (top right) -->
<g transform="translate(420, 120)">
<path d="M0 -30 L8 -8 L30 0 L8 8 L0 30 L-8 8 L-30 0 L-8 -8 Z"
fill="#FBBF24"/>
<circle cx="0" cy="0" r="8" fill="#FFFFFF"/>
</g>
<!-- Small sparkles -->
<g transform="translate(100, 150) scale(0.5)">
<path d="M0 -20 L5 -5 L20 0 L5 5 L0 20 L-5 5 L-20 0 L-5 -5 Z"
fill="#FBBF24" opacity="0.8"/>
</g>
<g transform="translate(400, 430) scale(0.4)">
<path d="M0 -20 L5 -5 L20 0 L5 5 L0 20 L-5 5 L-20 0 L-5 -5 Z"
fill="#FBBF24" opacity="0.8"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.9 KiB

@@ -0,0 +1,233 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap@4.4.1/dist/css/bootstrap.min.css" integrity="sha384-Vkoo8x4CGsO3+Hhxv8T/Q5PaXtkKtu6ug5TOeNV6gBiFeWPGFN9MuhOf23Q9Ifjh" crossorigin="anonymous">
</head>
<body>
<div class="row position-relative pt-lg-3 px-lg-5 pb-lg-3 p-md-4 p-3 mx-auto d-flex align-items-center" style="color:#333333; font-weight:500; font-size:16px; width:95%">
<section class="oe_container">
<!-- Header -->
<div class="col-md-12 text-center" style="margin-top: 10px;">
<a target="_blank" href="https://otoolkit.app">
<h2>O'Toolkit</h2>
</a>
<p>
<a href="https://otoolkit.app" target="_blank">Website: otoolkit.app</a><br>
Email: <a href="mailto:contact@otoolkit.app">contact@otoolkit.app</a>
</p>
</div>
<!-- Product Title -->
<div class="col-md-12 text-center" style="margin-top: 30px;">
<a target="_blank" href="https://otoolkit.app/application/seo-content">
<h2>SEO Content Generator</h2>
</a>
<p>The <strong>SEO Content Generator</strong> is an AI-powered content creation suite for Odoo that helps you generate, optimize, and manage SEO-friendly blog posts, product descriptions, and marketing content directly within your ERP. Powered by advanced AI models, it creates high-quality content with built-in SEO analysis, generates matching images, and seamlessly publishes to your Odoo website blog.</p>
<p>Complete documentation available at <a href="https://otoolkit.app/application/seo-content">https://otoolkit.app/application/seo-content</a></p>
</div>
<!-- How it works -->
<div class="col-md-12 text-center" style="margin-top: 30px;text-align: left !important;">
<h2 style="text-align: center !important">
<b>How it works</b>
</h2>
<div class="row text-dark">
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/generate-wizard.webp" class="shadow" style="width: 100%; border-radius: 15px;" alt="SEO Content generation wizard in Odoo">
</div>
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>Generate in one click</b>
</h3>
<p class="mt-4">Enter your keywords or topic, select your content type and tone, and let AI generate a complete SEO-optimized article with title, content, meta tags, and teaser.</p>
</div>
</div>
<hr class="d-lg-none">
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>Real-time SEO analysis</b>
</h3>
<p class="mt-4">Get instant feedback on your content's SEO performance with a detailed score breakdown and actionable improvement suggestions.</p>
</div>
</div>
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/seo-analysis.webp" class="shadow" style="width: 100%; border-radius: 15px;" alt="SEO analysis with score and recommendations">
</div>
</div>
</div>
<!-- AI Image Generation -->
<div class="col-md-12 text-center" style="margin-top: 30px;text-align: left !important;">
<h2 style="text-align: center !important">
<b>AI-Powered Image Generation</b>
</h2>
<div class="row text-dark">
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/image-generation.gif" class="shadow" style="width: 100%; border-radius: 15px;" alt="AI image generation with multiple styles">
</div>
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>Matching visuals</b>
</h3>
<p class="mt-4">Automatically generate images that match your content. Choose from multiple styles: photorealistic, illustration, digital art, watercolor, sketch, or 3D render.</p>
</div>
</div>
<hr class="d-lg-none">
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>Content-aware prompts</b>
</h3>
<p class="mt-4">Images are generated based on your content's title, sections, and keywords, ensuring they perfectly complement your article.</p>
</div>
</div>
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/image-styles.gif" class="shadow" style="width: 100%; border-radius: 15px;" alt="Different image generation styles">
</div>
</div>
</div>
<!-- Features -->
<div class="col-md-12 text-center" style="margin-top: 30px;text-align: left !important;">
<h2 style="text-align: center !important">
<b>Powerful Features</b>
</h2>
<div class="row text-dark">
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/batch-processing.gif" class="shadow" style="width: 100%; border-radius: 15px;" alt="Batch content generation">
</div>
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>Batch generation</b>
</h3>
<p class="mt-4">Generate hundreds of product descriptions or blog posts at once. Import from CSV, product categories, or keyword lists.</p>
</div>
</div>
<hr class="d-lg-none">
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>Ideas Queue & Scheduling</b>
</h3>
<p class="mt-4">Plan your content calendar with the Ideas Queue. Schedule content for future generation and even auto-publish to your blog.</p>
</div>
</div>
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/ideas-queue.webp" class="shadow" style="width: 100%; border-radius: 15px;" alt="Ideas queue and content scheduling">
</div>
<hr class="d-lg-none">
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/blog-integration.gif" class="shadow" style="width: 100%; border-radius: 15px;" alt="Blog post creation from SEO content">
</div>
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>One-click blog publishing</b>
</h3>
<p class="mt-4">Create blog posts directly from your generated content with cover images, meta tags, and SEO-friendly URLs already configured.</p>
</div>
</div>
<hr class="d-lg-none">
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>Templates & Brand Voice</b>
</h3>
<p class="mt-4">Create reusable templates with custom prompts and brand voice guidelines to maintain consistency across all your content.</p>
</div>
</div>
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/templates.gif" class="shadow" style="width: 100%; border-radius: 15px;" alt="Content templates and brand voice">
</div>
<hr class="d-lg-none">
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/references.gif" class="shadow" style="width: 100%; border-radius: 15px;" alt="URL reference fetching">
</div>
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>Reference materials</b>
</h3>
<p class="mt-4">Add URLs as references and the AI will fetch and analyze their content to create more informed, accurate articles.</p>
</div>
</div>
</div>
</div>
<!-- Multi-language -->
<div class="col-md-12 text-center" style="margin-top: 30px;text-align: left !important;">
<h2 style="text-align: center !important">
<b>Multi-Language Support</b>
</h2>
<div class="row text-dark">
<div class="col-lg-4 col-sm-12 mt-5 mb-5" style="display: flex; align-items: center;">
<div>
<h3>
<b>Generate in any language</b>
</h3>
<p class="mt-4">Create content in any language activated in your Odoo instance. Combined with O'Toolkit Auto Translate, automatically translate to all your website languages.</p>
</div>
</div>
<div class="col-lg-8 col-sm-12 mt-5 mb-5">
<img src="assets/multi-language.webp" class="shadow" style="width: 100%; border-radius: 15px;" alt="Multi-language content generation">
</div>
</div>
</div>
<!-- Content Types -->
<div class="col-md-12 text-center" style="margin-top: 30px;">
<h2><b>Content Types</b></h2>
<div class="row mt-4">
<div class="col-md-4 mb-4">
<div class="p-4" style="background-color: #f8f9fa; border-radius: 15px; height: 100%;">
<h4 style="color: rgb(124, 58, 237);">Blog Posts</h4>
<p>Full-length articles optimized for search engines with proper heading structure, meta tags, and engaging content.</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="p-4" style="background-color: #f8f9fa; border-radius: 15px; height: 100%;">
<h4 style="color: rgb(124, 58, 237);">Product Descriptions</h4>
<p>Compelling product descriptions that highlight features, benefits, and SEO keywords. Apply directly to products.</p>
</div>
</div>
<div class="col-md-4 mb-4">
<div class="p-4" style="background-color: #f8f9fa; border-radius: 15px; height: 100%;">
<h4 style="color: rgb(124, 58, 237);">Landing Pages</h4>
<p>Conversion-focused content for landing pages, category descriptions, and marketing campaigns.</p>
</div>
</div>
</div>
</div>
<!-- CTA Footer -->
<div class="col-md-12 text-center mt-5 p-4" style="background-color: #f8f9fa; border-radius: 15px; box-shadow: 0px 0px 10px rgba(0, 0, 0, 0.1);">
<h3 style="color: rgb(124, 58, 237);">Ready to supercharge your content creation?</h3>
<p class="mt-3" style="color: #333;">Start generating SEO-optimized content in minutes. Visit our website or contact us for more information.</p>
<p>
<span href="https://otoolkit.app" class="btn otoolkit-open-site" style="background-color: rgb(124, 58, 237); color: #fff; border-radius: 5px; padding: 10px 20px; text-decoration: none;">Visit Our Website</span>
<a href="mailto:contact@otoolkit.app" class="btn" style="background-color: #fff; color: rgb(124, 58, 237); border: 2px solid rgb(124, 58, 237); border-radius: 5px; padding: 10px 20px; text-decoration: none;">Email Us</a>
</p>
<a href="https://otoolkit.app">https://otoolkit.app</a>
</div>
</section>
</div>
</body>
</html>
@@ -0,0 +1,608 @@
/* O'Toolkit SEO Content Generator - Custom Styles */
/* ============================================
SEO Score Badge Styling
============================================ */
.o_seo_score_badge {
display: inline-flex;
align-items: center;
justify-content: center;
min-width: 50px;
padding: 4px 12px;
border-radius: 12px;
font-weight: 600;
font-size: 0.9em;
}
.o_seo_score_excellent {
background-color: #28a745;
color: white;
}
.o_seo_score_good {
background-color: #5cb85c;
color: white;
}
.o_seo_score_average {
background-color: #ffc107;
color: #212529;
}
.o_seo_score_poor {
background-color: #dc3545;
color: white;
}
/* ============================================
Content Generation States
============================================ */
.o_seo_content_state {
display: inline-flex;
align-items: center;
gap: 6px;
}
.o_seo_content_state .o_state_generating {
color: #17a2b8;
}
.o_seo_content_state .o_state_generated {
color: #28a745;
}
.o_seo_content_state .o_state_failed {
color: #dc3545;
}
/* Generating animation */
.o_seo_generating_spinner {
display: inline-block;
width: 16px;
height: 16px;
border: 2px solid #17a2b8;
border-radius: 50%;
border-top-color: transparent;
animation: seo-spin 1s linear infinite;
}
@keyframes seo-spin {
to {
transform: rotate(360deg);
}
}
/* ============================================
Content Preview Styling
============================================ */
.o_seo_content_preview {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 16px;
max-height: 400px;
overflow-y: auto;
}
.o_seo_content_preview h1,
.o_seo_content_preview h2,
.o_seo_content_preview h3,
.o_seo_content_preview h4 {
color: #343a40;
margin-top: 1em;
margin-bottom: 0.5em;
}
.o_seo_content_preview h1:first-child,
.o_seo_content_preview h2:first-child {
margin-top: 0;
}
.o_seo_content_preview p {
margin-bottom: 1em;
line-height: 1.6;
}
.o_seo_content_preview ul,
.o_seo_content_preview ol {
margin-bottom: 1em;
padding-left: 20px;
}
.o_seo_content_preview li {
margin-bottom: 0.5em;
}
/* ============================================
Image Gallery Styling
============================================ */
.o_seo_image_gallery {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));
gap: 16px;
padding: 16px;
}
.o_seo_image_card {
position: relative;
border: 1px solid #dee2e6;
border-radius: 8px;
overflow: hidden;
background-color: #fff;
transition: box-shadow 0.2s ease;
}
.o_seo_image_card:hover {
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.o_seo_image_card img {
width: 100%;
height: 150px;
object-fit: cover;
}
.o_seo_image_card .o_image_info {
padding: 12px;
}
.o_seo_image_card .o_image_state {
position: absolute;
top: 8px;
right: 8px;
padding: 4px 8px;
border-radius: 4px;
font-size: 0.8em;
font-weight: 500;
}
.o_seo_image_card .o_image_state_pending {
background-color: #ffc107;
color: #212529;
}
.o_seo_image_card .o_image_state_generating {
background-color: #17a2b8;
color: white;
}
.o_seo_image_card .o_image_state_completed {
background-color: #28a745;
color: white;
}
.o_seo_image_card .o_image_state_failed {
background-color: #dc3545;
color: white;
}
/* Image placeholder for pending/generating */
.o_seo_image_placeholder {
width: 100%;
height: 150px;
display: flex;
align-items: center;
justify-content: center;
background-color: #e9ecef;
color: #6c757d;
}
/* ============================================
Kanban Card Styling
============================================ */
.o_kanban_record .o_seo_content_kanban {
padding: 12px;
}
.o_seo_content_kanban .o_kanban_title {
font-weight: 600;
font-size: 1.1em;
margin-bottom: 8px;
color: #212529;
}
.o_seo_content_kanban .o_kanban_meta {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
}
.o_seo_content_kanban .o_kanban_tag {
padding: 2px 8px;
border-radius: 4px;
font-size: 0.85em;
background-color: #e9ecef;
color: #495057;
}
.o_seo_content_kanban .o_kanban_progress {
margin-top: 8px;
}
/* ============================================
Template Selection Styling
============================================ */
.o_seo_template_selector {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(250px, 1fr));
gap: 16px;
padding: 16px;
}
.o_seo_template_card {
border: 2px solid #dee2e6;
border-radius: 8px;
padding: 16px;
cursor: pointer;
transition: all 0.2s ease;
}
.o_seo_template_card:hover {
border-color: #007bff;
background-color: #f8f9fa;
}
.o_seo_template_card.selected {
border-color: #007bff;
background-color: #e7f1ff;
}
.o_seo_template_card .o_template_name {
font-weight: 600;
font-size: 1.1em;
margin-bottom: 8px;
color: #212529;
}
.o_seo_template_card .o_template_desc {
font-size: 0.9em;
color: #6c757d;
line-height: 1.4;
}
.o_seo_template_card .o_template_meta {
display: flex;
gap: 12px;
margin-top: 12px;
font-size: 0.85em;
color: #495057;
}
/* ============================================
Wizard Styling
============================================ */
.o_seo_wizard_step {
padding: 20px;
}
.o_seo_wizard_step .o_step_header {
font-size: 1.2em;
font-weight: 600;
margin-bottom: 16px;
color: #343a40;
}
.o_seo_wizard_step .o_step_description {
color: #6c757d;
margin-bottom: 20px;
}
/* Keyword input styling */
.o_seo_keyword_input {
display: flex;
flex-wrap: wrap;
gap: 8px;
padding: 8px;
border: 1px solid #ced4da;
border-radius: 4px;
background-color: #fff;
min-height: 44px;
}
.o_seo_keyword_tag {
display: inline-flex;
align-items: center;
gap: 4px;
padding: 4px 8px;
background-color: #007bff;
color: white;
border-radius: 4px;
font-size: 0.9em;
}
.o_seo_keyword_tag .o_remove {
cursor: pointer;
opacity: 0.8;
}
.o_seo_keyword_tag .o_remove:hover {
opacity: 1;
}
/* ============================================
Version History Styling
============================================ */
.o_seo_version_list {
max-height: 300px;
overflow-y: auto;
}
.o_seo_version_item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 12px;
border-bottom: 1px solid #dee2e6;
}
.o_seo_version_item:last-child {
border-bottom: none;
}
.o_seo_version_item .o_version_info {
display: flex;
flex-direction: column;
gap: 4px;
}
.o_seo_version_item .o_version_number {
font-weight: 600;
}
.o_seo_version_item .o_version_date {
font-size: 0.85em;
color: #6c757d;
}
.o_seo_version_item .o_version_actions {
display: flex;
gap: 8px;
}
/* ============================================
Cost Estimation Panel
============================================ */
.o_seo_cost_panel {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 16px;
margin-top: 16px;
}
.o_seo_cost_panel .o_cost_header {
font-weight: 600;
margin-bottom: 12px;
color: #343a40;
}
.o_seo_cost_panel .o_cost_row {
display: flex;
justify-content: space-between;
padding: 4px 0;
font-size: 0.9em;
}
.o_seo_cost_panel .o_cost_total {
border-top: 1px solid #dee2e6;
margin-top: 8px;
padding-top: 8px;
font-weight: 600;
}
/* ============================================
SEO Analysis Panel
============================================ */
.o_seo_analysis_panel {
background-color: #fff;
border: 1px solid #dee2e6;
border-radius: 8px;
padding: 16px;
}
.o_seo_analysis_item {
display: flex;
align-items: flex-start;
gap: 12px;
padding: 8px 0;
border-bottom: 1px solid #f1f3f4;
}
.o_seo_analysis_item:last-child {
border-bottom: none;
}
.o_seo_analysis_item .o_check_icon {
width: 20px;
height: 20px;
flex-shrink: 0;
}
.o_seo_analysis_item .o_check_pass {
color: #28a745;
}
.o_seo_analysis_item .o_check_warn {
color: #ffc107;
}
.o_seo_analysis_item .o_check_fail {
color: #dc3545;
}
.o_seo_analysis_item .o_check_text {
flex-grow: 1;
}
.o_seo_analysis_item .o_check_suggestion {
font-size: 0.85em;
color: #6c757d;
margin-top: 4px;
}
/* ============================================
Wizard Step Indicator
============================================ */
.otk_wizard_steps {
gap: 8px;
}
.otk_step_circle {
display: inline-flex;
align-items: center;
justify-content: center;
width: 28px;
height: 28px;
min-width: 28px;
border-radius: 50%;
font-size: 13px;
font-weight: 600;
line-height: 1;
flex-shrink: 0;
}
.otk_step_circle.otk_step_active {
background-color: #007bff;
color: white;
box-shadow: 0 0 0 3px rgba(0, 123, 255, 0.25);
}
.otk_step_circle.otk_step_done {
background-color: #28a745;
color: white;
}
.otk_step_circle.otk_step_upcoming {
background-color: #dee2e6;
color: #6c757d;
}
/* ============================================
Inline SEO Panel
============================================ */
.o_seo_inline_panel {
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 8px;
}
.o_seo_inline_panel .o_meta_indicator {
display: inline-flex;
align-items: center;
}
/* ============================================
Meta Length Indicators
============================================ */
.o_meta_length_optimal {
color: #28a745;
}
.o_meta_length_short {
color: #ffc107;
}
.o_meta_length_long {
color: #dc3545;
}
/* ============================================
Diff View Styling
============================================ */
.o_seo_diff_view {
font-family: 'SFMono-Regular', Consolas, 'Liberation Mono', Menlo, monospace;
font-size: 0.85em;
line-height: 1.5;
background-color: #f8f9fa;
border: 1px solid #dee2e6;
border-radius: 6px;
padding: 12px;
max-height: 500px;
overflow-y: auto;
}
.o_seo_diff_view .o_diff_header {
color: #6c757d;
font-weight: 600;
padding: 2px 4px;
background-color: #e9ecef;
margin-bottom: 2px;
}
.o_seo_diff_view .o_diff_section {
color: #6f42c1;
padding: 2px 4px;
background-color: #f3e8ff;
margin: 4px 0;
}
.o_seo_diff_view .o_diff_add {
color: #155724;
background-color: #d4edda;
padding: 1px 4px;
border-left: 3px solid #28a745;
}
.o_seo_diff_view .o_diff_remove {
color: #721c24;
background-color: #f8d7da;
padding: 1px 4px;
border-left: 3px solid #dc3545;
}
.o_seo_diff_view .o_diff_context {
color: #495057;
padding: 1px 4px;
border-left: 3px solid transparent;
}
/* ============================================
Responsive Adjustments
============================================ */
@media (max-width: 768px) {
.o_seo_image_gallery {
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
}
.o_seo_template_selector {
grid-template-columns: 1fr;
}
.o_seo_content_preview {
max-height: 300px;
}
.o_seo_step_indicator {
flex-wrap: wrap;
gap: 8px;
}
.o_seo_step_chevron {
margin: 0 4px;
}
.o_seo_inline_panel .row {
gap: 8px;
}
.o_seo_diff_view {
max-height: 300px;
font-size: 0.8em;
}
}
@@ -0,0 +1,124 @@
/** @odoo-module **/
import { Component, onWillStart, useState } from "@odoo/owl";
import { registry } from "@web/core/registry";
import { useService } from "@web/core/utils/hooks";
class SeoDashboard extends Component {
static template = "otoolkit_seo_content.SeoDashboard";
static props = ["*"];
setup() {
this.orm = useService("orm");
this.action = useService("action");
this.state = useState({
data: null,
loading: true,
});
onWillStart(async () => {
await this.loadDashboardData();
});
}
async loadDashboardData() {
this.state.loading = true;
try {
this.state.data = await this.orm.call(
"otk.seo.content",
"get_dashboard_data",
[]
);
} catch (e) {
console.error("Failed to load dashboard data:", e);
this.state.data = null;
}
this.state.loading = false;
}
getScoreColor(score) {
if (score >= 70) return "success";
if (score >= 40) return "warning";
return "danger";
}
getStateLabel(state) {
const labels = {
draft: "Draft",
generating: "Generating",
images_pending: "Images Pending",
review: "Review",
approved: "Approved",
published: "Published",
archived: "Archived",
};
return labels[state] || state;
}
getTypeLabel(type) {
const labels = {
blog_post: "Blog Post",
product_desc: "Product Desc",
category_desc: "Category Desc",
landing_page: "Landing Page",
social_post: "Social Post",
};
return labels[type] || type;
}
getStateColor(state) {
const colors = {
draft: "#6c757d",
generating: "#17a2b8",
images_pending: "#ffc107",
review: "#007bff",
approved: "#28a745",
published: "#20c997",
archived: "#adb5bd",
};
return colors[state] || "#6c757d";
}
onClickTotalContent() {
this.action.doAction({
type: "ir.actions.act_window",
name: "All Content",
res_model: "otk.seo.content",
view_mode: "list,form",
views: [[false, "list"], [false, "form"]],
});
}
onClickStaleContent() {
this.action.doAction({
type: "ir.actions.act_window",
name: "Stale Content",
res_model: "otk.seo.content",
view_mode: "list,form",
views: [[false, "list"], [false, "form"]],
domain: [["is_stale", "=", true]],
});
}
onClickState(state) {
this.action.doAction({
type: "ir.actions.act_window",
name: this.getStateLabel(state),
res_model: "otk.seo.content",
view_mode: "list,form",
views: [[false, "list"], [false, "form"]],
domain: [["state", "=", state]],
});
}
onClickRecent(contentId) {
this.action.doAction({
type: "ir.actions.act_window",
res_model: "otk.seo.content",
res_id: contentId,
view_mode: "form",
views: [[false, "form"]],
});
}
}
registry.category("actions").add("seo_content_dashboard", SeoDashboard);
@@ -0,0 +1,196 @@
<?xml version="1.0" encoding="UTF-8"?>
<templates xml:space="preserve">
<t t-name="otoolkit_seo_content.SeoDashboard">
<div class="o_action">
<div class="container-fluid py-3">
<!-- Header -->
<div class="d-flex justify-content-between align-items-center mb-4">
<h2 class="mb-0">
<i class="fa fa-line-chart me-2"/>SEO Content Dashboard
</h2>
<button class="btn btn-sm btn-outline-primary" t-on-click="loadDashboardData">
<i class="fa fa-refresh me-1"/>Refresh
</button>
</div>
<!-- Loading State -->
<div t-if="state.loading" class="text-center py-5">
<i class="fa fa-spinner fa-spin fa-3x text-primary"/>
<p class="mt-3 text-muted">Loading dashboard data...</p>
</div>
<t t-if="!state.loading and state.data">
<!-- Row 1: KPI Cards -->
<div class="row g-3 mb-4">
<!-- Total Content -->
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm" style="cursor:pointer" t-on-click="onClickTotalContent">
<div class="card-body text-center">
<div class="text-muted small text-uppercase mb-1">Total Content</div>
<div class="display-5 fw-bold text-primary" t-out="state.data.totalContent"/>
<div class="text-muted small mt-1">pieces created</div>
</div>
</div>
</div>
<!-- Avg SEO Score -->
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="text-muted small text-uppercase mb-1">Avg SEO Score</div>
<div class="display-5 fw-bold">
<span t-attf-class="text-#{getScoreColor(state.data.avgSeoScore)}" t-out="state.data.avgSeoScore"/>
</div>
<div class="progress mt-2" style="height: 6px;">
<div class="progress-bar"
t-attf-class="bg-#{getScoreColor(state.data.avgSeoScore)}"
role="progressbar"
t-attf-style="width: #{state.data.avgSeoScore}%"/>
</div>
</div>
</div>
</div>
<!-- Tokens Used -->
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm">
<div class="card-body text-center">
<div class="text-muted small text-uppercase mb-1">Tokens Used</div>
<div class="display-5 fw-bold text-info" t-out="state.data.totalTokens"/>
<div class="text-muted small mt-1">total consumed</div>
</div>
</div>
</div>
<!-- Stale Content -->
<div class="col-md-6 col-lg-3">
<div class="card h-100 border-0 shadow-sm" style="cursor:pointer" t-on-click="onClickStaleContent">
<div class="card-body text-center">
<div class="text-muted small text-uppercase mb-1">Needs Refresh</div>
<div t-attf-class="display-5 fw-bold #{state.data.staleCount > 0 ? 'text-warning' : 'text-success'}"
t-out="state.data.staleCount"/>
<div class="text-muted small mt-1">stale items</div>
</div>
</div>
</div>
</div>
<!-- Row 2: Charts (simple bar-like display) -->
<div class="row g-3 mb-4">
<!-- Content by State -->
<div class="col-lg-6">
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent border-0">
<h5 class="card-title mb-0">
<i class="fa fa-bar-chart me-1"/>Content by Status
</h5>
</div>
<div class="card-body">
<t t-foreach="Object.entries(state.data.contentByState)" t-as="entry" t-key="entry[0]">
<div class="d-flex align-items-center mb-2" style="cursor:pointer"
t-on-click="() => this.onClickState(entry[0])">
<span class="text-truncate me-2" style="min-width:100px;" t-out="getStateLabel(entry[0])"/>
<div class="progress flex-grow-1 me-2" style="height: 20px;">
<div class="progress-bar"
t-attf-style="width: #{state.data.totalContent ? (entry[1] / state.data.totalContent * 100) : 0}%; background-color: #{getStateColor(entry[0])};"
t-out="entry[1]"/>
</div>
</div>
</t>
<div t-if="!Object.keys(state.data.contentByState).length"
class="text-muted text-center py-3">
No content yet
</div>
</div>
</div>
</div>
<!-- Content by Type -->
<div class="col-lg-6">
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent border-0">
<h5 class="card-title mb-0">
<i class="fa fa-pie-chart me-1"/>Content by Type
</h5>
</div>
<div class="card-body">
<t t-foreach="Object.entries(state.data.contentByType)" t-as="entry" t-key="entry[0]">
<div class="d-flex justify-content-between align-items-center mb-2">
<span class="badge bg-secondary" t-out="getTypeLabel(entry[0])"/>
<strong t-out="entry[1]"/>
</div>
</t>
<div t-if="!Object.keys(state.data.contentByType).length"
class="text-muted text-center py-3">
No content yet
</div>
</div>
</div>
</div>
</div>
<!-- Row 3: Recent Content -->
<div class="row g-3">
<div class="col-12">
<div class="card border-0 shadow-sm">
<div class="card-header bg-transparent border-0">
<h5 class="card-title mb-0">
<i class="fa fa-clock-o me-1"/>Recent Content
</h5>
</div>
<div class="card-body p-0">
<table class="table table-hover mb-0">
<thead class="table-light">
<tr>
<th>Title</th>
<th>Type</th>
<th>Status</th>
<th>SEO Score</th>
<th>Created</th>
</tr>
</thead>
<tbody>
<t t-foreach="state.data.recentContent" t-as="item" t-key="item.id">
<tr style="cursor:pointer" t-on-click="() => this.onClickRecent(item.id)">
<td class="text-truncate" style="max-width:250px;" t-out="item.name"/>
<td>
<span class="badge bg-light text-dark" t-out="getTypeLabel(item.content_type)"/>
</td>
<td>
<span class="badge"
t-attf-style="background-color: #{getStateColor(item.state)}; color: white;"
t-out="getStateLabel(item.state)"/>
</td>
<td>
<div class="d-flex align-items-center">
<div class="progress flex-grow-1 me-1" style="height:8px;width:60px;">
<div class="progress-bar"
t-attf-class="bg-#{getScoreColor(item.seo_score)}"
t-attf-style="width: #{item.seo_score}%"/>
</div>
<small t-out="item.seo_score"/>
</div>
</td>
<td class="text-muted small" t-out="item.create_date"/>
</tr>
</t>
<tr t-if="!state.data.recentContent.length">
<td colspan="5" class="text-center text-muted py-3">
No content created yet
</td>
</tr>
</tbody>
</table>
</div>
</div>
</div>
</div>
</t>
<!-- Error / No Data -->
<div t-if="!state.loading and !state.data" class="text-center py-5">
<i class="fa fa-exclamation-triangle fa-3x text-warning"/>
<p class="mt-3 text-muted">Failed to load dashboard data. Please try again.</p>
</div>
</div>
</div>
</t>
</templates>