Menu

Change language

GitHub repo

Seo Audit Checklist

blockmarketing

Ahrefs and SEMrush style on-page SEO analyzer, meta tag auditor, Google SERP snippet preview simulator, heading hierarchy validator, and Core Web Vitals checklist.

Installation

$npx uipkge-ng@latest add seo-audit-checklist
Manifest:https://uipkge.dev/r/angular/seo-audit-checklist.jsonInstalls to:src/app/components/blocks/seo-audit-checklist/

Variants

Loading interactive previews…

Schema

Type aliases exported from this item's source. Use these to shape the data you pass in.

SeoCheckItem
interface SeoCheckItem {
  id: string
  title: string
  description: string
  status: CheckStatus
  value?: string
  recommendation?: string
  tag?: string
}
SeoCategory
interface SeoCategory {
  id: string
  name: string
  icon: 'meta' | 'headings' | 'opengraph' | 'technical'
  score: number
  checks: SeoCheckItem[]
}
CoreVitalMetric
interface CoreVitalMetric {
  name: string
  label: string
  value: string
  status: 'good' | 'needs-improvement' | 'poor'
  target: string
}

Files installed (2)

  • src/app/components/blocks/seo-audit-checklist/seo-audit-checklist.component.ts40.7 kB
    // Ahrefs and SEMrush style on-page SEO analyzer: meta tag auditor, Google SERP
    // snippet preview simulator, heading hierarchy validator, and Core Web Vitals
    // checklist. Port of the React block 1:1.
    import { ChangeDetectionStrategy, Component, Input, computed, signal } from '@angular/core'
    import {
      AlertCircle,
      AlertTriangle,
      Check,
      CheckCircle2,
      Code2,
      Copy,
      Eye,
      FileDown,
      FileText,
      Globe,
      Heading1,
      Info,
      Lock,
      LucideAngularModule,
      Monitor,
      RefreshCw,
      Search,
      Share2,
      ShieldCheck,
      Smartphone,
      Zap,
    } from 'lucide-angular'
    import { cn } from '@/lib/utils'
    import {
      UiAccordionComponent,
      UiAccordionContentComponent,
      UiAccordionItemComponent,
      UiAccordionTriggerComponent,
    } from '@/ui/accordion/accordion.component'
    import { UiBadgeComponent } from '@/ui/badge/badge.component'
    import { UiButtonComponent } from '@/ui/button/button.component'
    import {
      UiCardComponent,
      UiCardContentComponent,
      UiCardDescriptionComponent,
      UiCardHeaderComponent,
      UiCardTitleComponent,
    } from '@/ui/card/card.component'
    import { UiInputComponent } from '@/ui/input/input.component'
    import { UiProgressComponent } from '@/ui/progress/progress.component'
    import { UiSeparatorComponent } from '@/ui/separator/separator.component'
    
    export type CheckStatus = 'pass' | 'warning' | 'fail' | 'info'
    
    export interface SeoCheckItem {
      id: string
      title: string
      description: string
      status: CheckStatus
      value?: string
      recommendation?: string
      tag?: string
    }
    
    export interface SeoCategory {
      id: string
      name: string
      icon: 'meta' | 'headings' | 'opengraph' | 'technical'
      score: number
      checks: SeoCheckItem[]
    }
    
    export interface CoreVitalMetric {
      name: string
      label: string
      value: string
      status: 'good' | 'needs-improvement' | 'poor'
      target: string
    }
    
    const defaultVitals: CoreVitalMetric[] = [
      { name: 'LCP', label: 'Largest Contentful Paint', value: '1.2s', status: 'good', target: '≤ 2.5s' },
      { name: 'FID', label: 'First Input Delay', value: '12ms', status: 'good', target: '≤ 100ms' },
      { name: 'CLS', label: 'Cumulative Layout Shift', value: '0.01', status: 'good', target: '≤ 0.1' },
    ]
    
    const defaultCategories: SeoCategory[] = [
      {
        id: 'meta-tags',
        name: 'Title & Meta Description',
        icon: 'meta',
        score: 100,
        checks: [
          {
            id: 'title-length',
            title: 'Title Tag Length & Structure',
            description: 'Page title is 58 characters (optimal range: 50–60 characters). Fully visible in 98% of SERPs.',
            status: 'pass',
            value: 'UIPKGE — Dual-Framework UI Registry for Vue & React',
            tag: '58 / 60 chars',
          },
          {
            id: 'meta-desc',
            title: 'Meta Description Length',
            description:
              'Meta description is 152 characters (optimal range: 140–160 characters). Rich keyword density without truncation.',
            status: 'pass',
            value:
              'Explore high-quality accessible Vue, React, and Angular UI components and blocks built with Tailwind CSS v4 and Lucide icons.',
            tag: '152 / 160 chars',
          },
          {
            id: 'canonical-url',
            title: 'Canonical URL Tag',
            description: 'Self-referencing canonical URL tag prevents duplicate content indexing across query variants.',
            status: 'pass',
            value: '<link rel="canonical" href="https://uipkge.dev/vue/components" />',
            tag: 'Present',
          },
          {
            id: 'meta-robots',
            title: 'Meta Robots Directives',
            description: 'Directives instruct crawlers to index the document and follow embedded hyperlinks.',
            status: 'pass',
            value: 'index, follow, max-image-preview:large, max-snippet:-1',
            tag: 'Index, Follow',
          },
        ],
      },
      {
        id: 'headings',
        name: 'Heading Hierarchy & Structure',
        icon: 'headings',
        score: 100,
        checks: [
          {
            id: 'h1-presence',
            title: 'Single Primary H1 Tag',
            description: 'Exactly 1 H1 tag detected containing target search terms and establishing clear topic authority.',
            status: 'pass',
            value: '<h1>UIPKGE Component & Block Registry</h1>',
            tag: '1 H1 Tag',
          },
          {
            id: 'h2-distribution',
            title: 'H2 Section Organization',
            description: '6 H2 tags logically divide major categories (Primitives, Blocks, Forms, Marketing, Feedback).',
            status: 'pass',
            value: '6 H2 tags · 0 skipped levels',
            tag: '6 H2 Tags',
          },
          {
            id: 'h3-nesting',
            title: 'H3 Subsection Hierarchy',
            description: '12 H3 tags cleanly nested under parent H2 sections without skipping hierarchical levels.',
            status: 'pass',
            value: '12 H3 tags · Valid tree hierarchy',
            tag: '12 H3 Tags',
          },
          {
            id: 'content-depth',
            title: 'Content Word Count & Substance',
            description: '1,840 words detected with strong semantic depth for component discovery.',
            status: 'pass',
            value: '1,840 words · 4.2 min read',
            tag: 'Substantial',
          },
        ],
      },
      {
        id: 'opengraph',
        name: 'OpenGraph & Social Share Cards',
        icon: 'opengraph',
        score: 100,
        checks: [
          {
            id: 'og-title-desc',
            title: 'OpenGraph Title & Description',
            description: 'Social graph tags are present and optimized for display across Twitter/X, LinkedIn, and Discord.',
            status: 'pass',
            value: 'og:title, og:description, og:type, og:url configured',
            tag: 'Configured',
          },
          {
            id: 'og-image',
            title: 'OpenGraph Social Banner Image',
            description:
              '1200 × 630px high-resolution banner image detected with crisp aspect ratio (1.91:1) and fast CDN delivery.',
            status: 'pass',
            value: 'https://uipkge.dev/og/components.png (1200x630 · 84 KB)',
            tag: '1200x630 px',
          },
          {
            id: 'twitter-card',
            title: 'Twitter / X Card Format',
            description: 'Card format explicitly set to summary_large_image for maximal feed visibility.',
            status: 'pass',
            value: 'twitter:card = summary_large_image',
            tag: 'Large Image',
          },
        ],
      },
      {
        id: 'technical',
        name: 'Performance, Images & Technical SEO',
        icon: 'technical',
        score: 85,
        checks: [
          {
            id: 'image-alt-tags',
            title: 'Image Alt Attributes',
            description:
              'All 24 embedded images contain meaningful, non-empty alt text attributes for accessibility and image search.',
            status: 'pass',
            value: '24 / 24 images with valid alt text (100%)',
            tag: '24 / 24 Pass',
          },
          {
            id: 'broken-links',
            title: 'Crawlable Hyperlinks & 404 Errors',
            description: 'Crawled 142 internal and external links. 0 broken links or redirect loops detected.',
            status: 'pass',
            value: '142 links verified · 0 errors',
            tag: '0 Broken',
          },
          {
            id: 'ssl-security',
            title: 'SSL / HTTPS & Security Headers',
            description: 'Valid TLS 1.3 certificate with HSTS header enabled and automatic HTTP-to-HTTPS redirect.',
            status: 'pass',
            value: 'TLS 1.3 · HSTS Active · HTTP/2 Supported',
            tag: 'Secure',
          },
          {
            id: 'schema-markup',
            title: 'Schema.org Structured Data',
            description:
              'No SoftwareApplication or Product JSON-LD schema detected. Structured data enables rich Google SERP snippet features.',
            status: 'warning',
            value: 'Missing schema.org/SoftwareApplication microdata',
            recommendation:
              'Add JSON-LD script tag with SoftwareApplication schema to qualify for rich snippet search results.',
            tag: 'Missing Schema',
          },
        ],
      },
    ]
    
    const defaultOpenCategories = ['meta-tags', 'headings', 'opengraph', 'technical']
    
    @Component({
      changeDetection: ChangeDetectionStrategy.Eager,
      selector: 'ui-seo-audit-checklist, [ui-seo-audit-checklist]',
      standalone: true,
      // React renders the root <div> itself: the host stays out of layout and `class` goes to the root.
      host: { '[attr.class]': '"contents"' },
      imports: [
        LucideAngularModule,
        UiAccordionComponent,
        UiAccordionContentComponent,
        UiAccordionItemComponent,
        UiAccordionTriggerComponent,
        UiBadgeComponent,
        UiButtonComponent,
        UiCardComponent,
        UiCardContentComponent,
        UiCardDescriptionComponent,
        UiCardHeaderComponent,
        UiCardTitleComponent,
        UiInputComponent,
        UiProgressComponent,
        UiSeparatorComponent,
      ],
      template: `
        <div [class]="rootClass" data-slot="seo-audit-checklist">
          <!-- Header & Audit Control Center -->
          <div ui-card class="border-border bg-card shadow-xs">
            <div ui-card-content class="p-5 sm:p-6">
              <div class="flex flex-col gap-5 lg:flex-row lg:items-center lg:justify-between">
                <!-- Left: Target URL & Score -->
                <div class="space-y-3">
                  <div class="flex flex-wrap items-center gap-3">
                    <div
                      class="border-success/30 bg-success/10 text-success flex items-center gap-1.5 rounded-full border px-3 py-1 text-xs font-semibold"
                    >
                      <span class="relative flex size-2">
                        <span class="bg-success absolute inline-flex h-full w-full rounded-full opacity-75"></span>
                        <span class="bg-success relative inline-flex size-2 rounded-full"></span>
                      </span>
                      <span class="font-mono tabular-nums">{{ score }} / 100</span>
                      <span>·</span>
                      <span>Healthy Score</span>
                    </div>
                    <span ui-badge wrap variant="outline" class="gap-1 text-xs font-normal">
                      <lucide-icon [img]="Globe" class="text-muted-foreground size-3" />
                      HTTP 200 OK
                    </span>
                    <span ui-badge wrap variant="outline" class="gap-1 text-xs font-normal">
                      <lucide-icon [img]="ShieldCheck" class="text-success size-3" />
                      Indexable
                    </span>
                  </div>
    
                  <div>
                    <h1 class="text-foreground text-lg font-bold tracking-tight sm:text-xl">
                      On-Page SEO Analyzer & Snippet Simulator
                    </h1>
                    <p class="text-muted-foreground text-xs">
                      {{ lastAudited }}
                    </p>
                  </div>
                </div>
    
                <!-- Right: Action Buttons -->
                <div class="flex flex-wrap items-center gap-2.5 sm:self-start lg:self-center">
                  <button
                    ui-button
                    variant="outline"
                    size="sm"
                    class="h-9 gap-1.5 text-xs shadow-xs"
                    [disabled]="isAuditing()"
                    (click)="handleReaudit()"
                  >
                    <lucide-icon [img]="RefreshCw" [class]="reauditIconClass" />
                    <span>Re-audit URL</span>
                  </button>
    
                  <button
                    ui-button
                    aria-label="Download attachment"
                    size="sm"
                    class="h-9 gap-1.5 text-xs font-medium shadow-xs"
                    (click)="handleDownloadReport()"
                  >
                    @if (!reportDownloaded()) {
                      <lucide-icon [img]="FileDown" class="size-3.5" />
                    } @else {
                      <lucide-icon [img]="Check" class="text-success size-3.5" />
                    }
                    <span>{{ reportDownloaded() ? 'Report Exported!' : 'Download SEO Report' }}</span>
                  </button>
                </div>
              </div>
    
              <ui-separator class="my-4" />
    
              <!-- URL Input Strip -->
              <div class="flex flex-col gap-2 sm:flex-row sm:items-center">
                <div class="relative flex-1">
                  <div class="text-muted-foreground pointer-events-none absolute inset-y-0 left-0 flex items-center pl-3">
                    <lucide-icon [img]="Search" class="size-4" />
                  </div>
                  <ui-input
                    [value]="currentUrl()"
                    (valueChange)="currentUrl.set($event)"
                    type="url"
                    placeholder="https://yourdomain.com/path"
                    class="h-9 w-full rounded-md pr-24 pl-9 font-mono text-xs shadow-xs"
                    (keydown.enter)="handleReaudit()"
                  />
                  <button
                    type="button"
                    class="text-muted-foreground hover:text-foreground absolute inset-y-0 right-0 flex min-h-6 items-center pr-3 text-xs transition-colors"
                    (click)="handleCopyUrl()"
                  >
                    @if (copiedUrl()) {
                      <lucide-icon [img]="Check" class="text-success size-3.5" />
                    } @else {
                      <lucide-icon [img]="Copy" class="size-3.5" />
                    }
                    <span class="ml-1 text-xs">{{ copiedUrl() ? 'Copied' : 'Copy' }}</span>
                  </button>
                </div>
                <button
                  ui-button
                  size="sm"
                  variant="secondary"
                  class="h-9 shrink-0 px-4 text-xs font-medium"
                  [disabled]="isAuditing()"
                  (click)="handleReaudit()"
                >
                  <lucide-icon [img]="Search" class="text-primary mr-1.5 size-3.5" />
                  Analyze Page
                </button>
              </div>
            </div>
          </div>
    
          <!-- 4 Core SEO Pillar Metric Cards -->
          <div class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
            <!-- Pillar 1: Meta Tags & Snippet -->
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <div ui-card-title class="text-muted-foreground text-xs font-medium">Meta Tags & Snippet</div>
                <div
                  class="border-border/60 bg-muted/60 text-muted-foreground flex size-7 items-center justify-center rounded-md border"
                >
                  <lucide-icon [img]="FileText" class="text-success size-3.5" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                  <div class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">100% Pass</div>
                  <span ui-badge wrap variant="success" class="text-xs font-medium">Optimal</span>
                </div>
                <div class="space-y-1.5">
                  <ui-progress
                    [value]="100"
                    class="bg-success/20 [&_[data-slot=progress-indicator]]:bg-success h-1.5 w-full"
                  />
                  <p class="text-muted-foreground text-xs">Title & description optimized</p>
                </div>
                <div
                  class="border-border/50 text-muted-foreground flex items-center justify-between border-t pt-2 font-mono text-xs"
                >
                  <span>58 chars title</span>
                  <span>·</span>
                  <span>152 chars desc</span>
                </div>
              </div>
            </div>
    
            <!-- Pillar 2: Core Web Vitals -->
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <div ui-card-title class="text-muted-foreground text-xs font-medium">Core Web Vitals</div>
                <div
                  class="border-border/60 bg-muted/60 text-muted-foreground flex size-7 items-center justify-center rounded-md border"
                >
                  <lucide-icon [img]="Zap" class="text-success size-3.5" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                  <div class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">Good</div>
                  <span ui-badge wrap variant="success" class="text-xs font-medium">98/100</span>
                </div>
                <div class="space-y-1.5">
                  <ui-progress
                    [value]="98"
                    class="bg-success/20 [&_[data-slot=progress-indicator]]:bg-success h-1.5 w-full"
                  />
                  <p class="text-muted-foreground text-xs">LCP 1.2s · FID 12ms · CLS 0.01</p>
                </div>
                <div
                  class="border-border/50 text-muted-foreground flex items-center justify-between border-t pt-2 font-mono text-xs"
                >
                  <span>Chrome UX Report</span>
                  <span>·</span>
                  <span class="text-success">Passed</span>
                </div>
              </div>
            </div>
    
            <!-- Pillar 3: Mobile Usability -->
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <div ui-card-title class="text-muted-foreground text-xs font-medium">Mobile Usability</div>
                <div
                  class="border-border/60 bg-muted/60 text-muted-foreground flex size-7 items-center justify-center rounded-md border"
                >
                  <lucide-icon [img]="Smartphone" class="text-success size-3.5" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                  <div class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">100% Pass</div>
                  <span ui-badge wrap variant="success" class="text-xs font-medium">Valid</span>
                </div>
                <div class="space-y-1.5">
                  <ui-progress
                    [value]="100"
                    class="bg-success/20 [&_[data-slot=progress-indicator]]:bg-success h-1.5 w-full"
                  />
                  <p class="text-muted-foreground text-xs">Touch targets & viewport valid</p>
                </div>
                <div
                  class="border-border/50 text-muted-foreground flex items-center justify-between border-t pt-2 font-mono text-xs"
                >
                  <span>48px tap targets</span>
                  <span>·</span>
                  <span>0 horizontal scroll</span>
                </div>
              </div>
            </div>
    
            <!-- Pillar 4: Indexability & Crawl -->
            <div ui-card class="border-border bg-card shadow-xs">
              <div ui-card-header class="flex flex-row items-center justify-between space-y-0 pb-2">
                <div ui-card-title class="text-muted-foreground text-xs font-medium">Indexability & Crawl</div>
                <div
                  class="border-border/60 bg-muted/60 text-muted-foreground flex size-7 items-center justify-center rounded-md border"
                >
                  <lucide-icon [img]="ShieldCheck" class="text-success size-3.5" />
                </div>
              </div>
              <div ui-card-content class="space-y-2">
                <div class="flex flex-wrap items-baseline justify-between gap-x-2 gap-y-0.5">
                  <div class="text-foreground text-xl font-bold tracking-tight sm:text-2xl">Valid</div>
                  <span ui-badge wrap variant="success" class="text-xs font-medium">Indexed</span>
                </div>
                <div class="space-y-1.5">
                  <ui-progress
                    [value]="95"
                    class="bg-success/20 [&_[data-slot=progress-indicator]]:bg-success h-1.5 w-full"
                  />
                  <p class="text-muted-foreground text-xs">Canonical URL present & sitemap verified</p>
                </div>
                <div
                  class="border-border/50 text-muted-foreground flex items-center justify-between border-t pt-2 font-mono text-xs"
                >
                  <span>robots.txt allowed</span>
                  <span>·</span>
                  <span>sitemap.xml</span>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Google Search Result Snippet Preview Simulator -->
          <div ui-card class="border-border bg-card shadow-xs">
            <div ui-card-header class="flex flex-col gap-3 pb-4 sm:flex-row sm:items-center sm:justify-between">
              <div>
                <div class="flex items-center gap-2">
                  <div ui-card-title class="text-base font-semibold">Google Search Result Snippet Preview</div>
                  <span ui-badge wrap variant="outline" class="gap-1 text-xs font-normal">
                    <lucide-icon [img]="Eye" class="text-primary size-3" />
                    Live SERP Simulator
                  </span>
                </div>
                <div ui-card-description class="text-xs">
                  Simulate how your meta tags, title, and URL render in Google SERPs on desktop vs. mobile.
                </div>
              </div>
    
              <!-- Desktop / Mobile View Switcher -->
              <div class="border-border bg-muted/50 flex items-center rounded-lg border p-0.5">
                <button type="button" [class]="deviceButtonClass('desktop')" (click)="previewDevice.set('desktop')">
                  <lucide-icon [img]="Monitor" class="size-3.5" />
                  <span>Desktop</span>
                </button>
                <button type="button" [class]="deviceButtonClass('mobile')" (click)="previewDevice.set('mobile')">
                  <lucide-icon [img]="Smartphone" class="size-3.5" />
                  <span>Mobile</span>
                </button>
              </div>
            </div>
    
            <div ui-card-content class="space-y-4">
              <!-- Mock Google SERP Result Box -->
              <div [class]="serpBoxClass">
                <!-- Google Result Header / Breadcrumb -->
                <div class="flex items-center gap-2.5 pb-1.5">
                  <div
                    class="border-border bg-muted text-foreground flex size-7 shrink-0 items-center justify-center rounded-full border text-xs font-bold"
                  >
                    ⚡
                  </div>
                  <div class="min-w-0 flex-1">
                    <div class="text-foreground flex items-center gap-1.5 text-xs">
                      <span class="font-medium">uipkge.dev</span>
                      <span class="text-muted-foreground">›</span>
                      <span class="text-muted-foreground truncate">vue › components</span>
                    </div>
                    <p class="text-muted-foreground truncate font-mono text-xs">{{ urlValue() }}</p>
                  </div>
                </div>
    
                <!-- Google SERP Title -->
                <div class="space-y-1 pt-1">
                  <a
                    href="#"
                    class="text-info block text-base leading-snug font-medium hover:underline sm:text-lg"
                    (click)="$event.preventDefault()"
                  >
                    {{ titleTag }}
                  </a>
    
                  <!-- Google Snippet Description with highlighted keywords -->
                  <p class="text-muted-foreground text-xs leading-relaxed sm:text-sm">
                    <span class="text-muted-foreground/90 font-medium">Aug 20, 2026 — </span>
                    Explore high-quality accessible
                    <strong class="text-foreground font-semibold">Vue</strong> and
                    <strong class="text-foreground font-semibold">React</strong>
                    <strong class="text-foreground font-semibold">UI components</strong> and blocks built with
                    <strong class="text-foreground font-semibold">Tailwind CSS v4</strong>, Reka UI, and Lucide icons.
                    Unbundled registry with full source code ownership.
                  </p>
                </div>
    
                <!-- Google Rich Snippet Badges -->
                <div class="border-border/40 mt-3 flex flex-wrap items-center gap-2 border-t pt-2.5">
                  <span ui-badge wrap variant="secondary" class="text-xs font-normal"> ★ 4.9 · 1.4k GitHub Stars </span>
                  <span ui-badge wrap variant="outline" class="text-xs font-normal"> Free & Open Source </span>
                  <span ui-badge wrap variant="outline" class="text-xs font-normal"> Vue 3.5 + React 19 </span>
                </div>
              </div>
    
              <!-- Snippet Metrics Character / Pixel Budget Strip -->
              <div class="border-border/80 bg-muted/30 grid grid-cols-1 gap-3 rounded-lg border p-3 sm:grid-cols-3">
                <div class="min-w-0 space-y-1">
                  <div class="flex items-center justify-between text-xs">
                    <span class="text-muted-foreground">Title Tag Budget:</span>
                    <span class="text-foreground font-mono font-semibold tabular-nums">58 / 60 chars</span>
                  </div>
                  <ui-progress
                    [value]="(58 / 60) * 100"
                    class="bg-success/20 [&_[data-slot=progress-indicator]]:bg-success h-1.5 w-full"
                  />
                  <p class="text-success text-xs">Optimal (485px / 600px max SERP width)</p>
                </div>
    
                <div class="min-w-0 space-y-1">
                  <div class="flex items-center justify-between text-xs">
                    <span class="text-muted-foreground">Meta Description:</span>
                    <span class="text-foreground font-mono font-semibold tabular-nums">152 / 160 chars</span>
                  </div>
                  <ui-progress
                    [value]="(152 / 160) * 100"
                    class="bg-success/20 [&_[data-slot=progress-indicator]]:bg-success h-1.5 w-full"
                  />
                  <p class="text-success text-xs">Optimal (890px / 960px max SERP width)</p>
                </div>
    
                <div class="flex flex-col justify-between space-y-1">
                  <div class="flex items-center justify-between text-xs">
                    <span class="text-muted-foreground">Truncation Risk:</span>
                    <span ui-badge wrap variant="success" class="px-1.5 py-0 text-xs">0% Truncation</span>
                  </div>
                  <p class="text-muted-foreground text-xs">Fully visible on both desktop & mobile Google viewports.</p>
                </div>
              </div>
            </div>
          </div>
    
          <!-- Actionable Recommendation Callout Banner -->
          <div ui-card class="border-warning/30 bg-warning/10 shadow-xs">
            <div ui-card-content class="p-5">
              <div class="flex flex-col gap-4 lg:flex-row lg:items-start lg:justify-between">
                <div class="flex items-start gap-3">
                  <div
                    class="border-warning/30 bg-warning/20 text-warning mt-0.5 flex size-8 shrink-0 items-center justify-center rounded-lg border"
                  >
                    <lucide-icon [img]="AlertTriangle" class="size-4" />
                  </div>
                  <div class="space-y-1.5">
                    <div class="flex flex-wrap items-center gap-2">
                      <h3 class="text-foreground text-sm font-semibold">
                        Actionable Recommendation: Add Schema.org JSON-LD Structured Data
                      </h3>
                      <span ui-badge wrap variant="warning" class="text-xs">High Impact</span>
                    </div>
                    <p class="text-muted-foreground text-xs leading-relaxed">
                      Adding
                      <code
                        class="bg-background/80 text-foreground border-border rounded border px-1 py-0.5 font-mono text-xs"
                        >SoftwareApplication</code
                      >
                      or
                      <code
                        class="bg-background/80 text-foreground border-border rounded border px-1 py-0.5 font-mono text-xs"
                        >TechArticle</code
                      >
                      structured data enables Google Rich Snippets, rating stars, and knowledge graph carousels.
                    </p>
    
                    <!-- Inline Code Preview -->
                    <div
                      class="border-border/80 bg-background/90 text-foreground mt-2 rounded-md border p-3 font-mono text-xs"
                    >
                      <div class="text-muted-foreground flex items-center justify-between pb-1.5">
                        <span class="text-xs">Recommended JSON-LD Block</span>
                        <button
                          type="button"
                          class="text-muted-foreground hover:text-foreground flex min-h-6 items-center gap-1 text-xs transition-colors"
                          (click)="handleCopySchema()"
                        >
                          @if (copiedSchema()) {
                            <lucide-icon [img]="Check" class="text-success size-3" />
                          } @else {
                            <lucide-icon [img]="Copy" class="size-3" />
                          }
                          <span>{{ copiedSchema() ? 'Copied' : 'Copy JSON-LD' }}</span>
                        </button>
                      </div>
                      <pre class="text-muted-foreground overflow-x-auto text-xs leading-snug"><code>{{
                        jsonLdSnippet
                      }}</code></pre>
                    </div>
                  </div>
                </div>
    
                <button
                  ui-button
                  variant="outline"
                  size="sm"
                  class="shrink-0 gap-1.5 text-xs shadow-xs"
                  (click)="handleCopySchema()"
                >
                  <lucide-icon [img]="Code2" class="size-3.5" />
                  <span>Copy Schema Snippet</span>
                </button>
              </div>
            </div>
          </div>
    
          <!-- Audit Checklist Grouped Accordion Section -->
          <div class="space-y-4">
            <div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
              <div>
                <h2 class="text-foreground text-sm font-semibold tracking-tight">Detailed On-Page Audit Checklist</h2>
                <p class="text-muted-foreground text-xs">
                  Complete verification of on-page meta tags, heading semantics, social cards, and technical crawlability.
                </p>
              </div>
    
              <!-- Filter Tabs -->
              <div class="flex items-center gap-1.5">
                <button
                  ui-button
                  variant="ghost"
                  size="sm"
                  [class]="filterClass('all')"
                  (click)="activeStatusFilter.set('all')"
                >
                  All Checks ({{ totalCount() }})
                </button>
                <button
                  ui-button
                  variant="ghost"
                  size="sm"
                  [class]="filterClass('pass')"
                  (click)="activeStatusFilter.set('pass')"
                >
                  Passed ({{ passCount() }})
                </button>
                <button
                  ui-button
                  variant="ghost"
                  size="sm"
                  [class]="filterClass('warning')"
                  (click)="activeStatusFilter.set('warning')"
                >
                  Warnings ({{ warningCount() }})
                </button>
              </div>
            </div>
    
            <!-- Accordion List -->
            <ui-accordion type="multiple" [defaultValue]="openCategories" class="space-y-3">
              @for (category of filteredCategories(); track category.id) {
                <ui-accordion-item
                  [value]="category.id"
                  class="border-border bg-card overflow-hidden rounded-lg border shadow-xs"
                >
                  <button ui-accordion-trigger class="px-5 py-4 hover:no-underline">
                    <div class="flex w-full flex-wrap items-center justify-between gap-3 pr-2 text-left">
                      <div class="flex items-center gap-3">
                        <div
                          class="border-border/60 bg-muted/60 text-muted-foreground flex size-8 shrink-0 items-center justify-center rounded-md border"
                        >
                          <lucide-icon [img]="categoryIcon(category.icon)" class="size-4" />
                        </div>
                        <div>
                          <span class="text-foreground text-sm font-semibold">{{ category.name }}</span>
                          <p class="text-muted-foreground text-xs">
                            {{ passingCount(category) }} of {{ category.checks.length }} tests passing
                          </p>
                        </div>
                      </div>
    
                      <div class="flex items-center gap-2.5">
                        <span
                          ui-badge
                          [variant]="category.score === 100 ? 'success' : 'warning'"
                          class="text-xs font-semibold whitespace-normal tabular-nums"
                        >
                          {{ category.score }}% Score
                        </span>
                      </div>
                    </div>
                  </button>
    
                  <ui-accordion-content class="px-5 pt-0 pb-5">
                    <div class="divide-border/60 border-border/60 divide-y border-t">
                      @for (check of category.checks; track check.id) {
                        <div class="space-y-2 py-3.5 first:pt-4 last:pb-1">
                          <div class="flex flex-col gap-1.5 sm:flex-row sm:items-start sm:justify-between sm:gap-3">
                            <div class="min-w-0 space-y-1">
                              <div class="flex items-center gap-2">
                                @if (check.status === 'pass') {
                                  <lucide-icon [img]="CheckCircle2" class="text-success size-4 shrink-0" />
                                } @else if (check.status === 'warning') {
                                  <lucide-icon [img]="AlertTriangle" class="text-warning size-4 shrink-0" />
                                } @else {
                                  <lucide-icon [img]="AlertCircle" class="text-destructive size-4 shrink-0" />
                                }
                                <h4 class="text-foreground text-xs font-semibold sm:text-sm">{{ check.title }}</h4>
                              </div>
                              <p class="text-muted-foreground pl-6 text-xs leading-relaxed">{{ check.description }}</p>
                            </div>
    
                            <div class="flex flex-wrap items-center gap-2 pl-6 sm:pl-0">
                              @if (check.tag) {
                                <span ui-badge wrap variant="outline" class="font-mono text-xs font-normal">
                                  {{ check.tag }}
                                </span>
                              }
                              <span
                                ui-badge
                                wrap
                                [variant]="statusBadgeVariant(check.status)"
                                class="text-xs font-medium whitespace-normal capitalize"
                              >
                                {{ check.status }}
                              </span>
                            </div>
                          </div>
    
                          <!-- Code / Value Inspector Box -->
                          @if (check.value) {
                            <div
                              class="border-border/70 bg-muted/40 text-foreground ml-6 rounded-md border px-3 py-2 font-mono text-xs"
                            >
                              <span class="text-xs break-all">{{ check.value }}</span>
                            </div>
                          }
    
                          <!-- Recommendation Callout -->
                          @if (check.recommendation) {
                            <div
                              class="border-warning/30 bg-warning/10 text-warning ml-6 flex items-start gap-2 rounded-md border p-2.5 text-xs"
                            >
                              <lucide-icon [img]="Info" class="text-warning size-4 shrink-0" />
                              <p class="text-xs leading-relaxed">{{ check.recommendation }}</p>
                            </div>
                          }
                        </div>
                      }
                    </div>
                  </ui-accordion-content>
                </ui-accordion-item>
              }
            </ui-accordion>
          </div>
        </div>
      `,
    })
    export class UiSeoAuditChecklistComponent {
      protected readonly AlertCircle = AlertCircle
      protected readonly AlertTriangle = AlertTriangle
      protected readonly Check = Check
      protected readonly CheckCircle2 = CheckCircle2
      protected readonly Code2 = Code2
      protected readonly Copy = Copy
      protected readonly Eye = Eye
      protected readonly FileDown = FileDown
      protected readonly FileText = FileText
      protected readonly Globe = Globe
      protected readonly Heading1 = Heading1
      protected readonly Info = Info
      protected readonly Lock = Lock
      protected readonly Monitor = Monitor
      protected readonly RefreshCw = RefreshCw
      protected readonly Search = Search
      protected readonly Share2 = Share2
      protected readonly ShieldCheck = ShieldCheck
      protected readonly Smartphone = Smartphone
      protected readonly Zap = Zap
    
      protected readonly openCategories = defaultOpenCategories
      protected readonly jsonLdSnippet = `{
      "@context": "https://schema.org",
      "@type": "SoftwareApplication",
      "name": "UIPKGE",
      "applicationCategory": "DeveloperApplication",
      "operatingSystem": "Web",
      "offers": { "@type": "Offer", "price": "0", "priceCurrency": "USD" }
    }`
    
      @Input() set url(v: string | undefined) {
        const next = v ?? 'https://uipkge.dev/vue/components'
        this.urlValue.set(next)
        this.currentUrl.set(next)
      }
      @Input() score = 94
      @Input() lastAudited = 'Audit completed in 640ms · Cached 2m ago'
      @Input() titleTag = 'UIPKGE — Dual-Framework UI Registry for Vue & React'
      @Input() metaDescription =
        'Explore high-quality accessible Vue, React, and Angular UI components and blocks built with Tailwind CSS v4 and Lucide icons. Unbundled registry with full source code ownership.'
      @Input() h1Tag = 'UIPKGE Component & Block Registry'
      @Input() ogImageUrl = 'https://images.unsplash.com/photo-1618005182384-a83a8bd57fbe?w=1200&auto=format&fit=crop&q=80'
      @Input() set categories(v: SeoCategory[] | undefined) {
        this.categoriesSig.set(v ?? defaultCategories)
      }
      @Input() vitals: CoreVitalMetric[] = defaultVitals
      @Input('class') className?: string
    
      readonly urlValue = signal('https://uipkge.dev/vue/components')
      readonly categoriesSig = signal<SeoCategory[]>(defaultCategories)
    
      readonly currentUrl = signal('https://uipkge.dev/vue/components')
      readonly isAuditing = signal(false)
      readonly copiedSchema = signal(false)
      readonly copiedUrl = signal(false)
      readonly reportDownloaded = signal(false)
      readonly previewDevice = signal<'desktop' | 'mobile'>('desktop')
      readonly activeStatusFilter = signal<'all' | 'pass' | 'warning'>('all')
    
      readonly allChecks = computed(() => this.categoriesSig().flatMap((cat) => cat.checks))
      readonly passCount = computed(() => this.allChecks().filter((c) => c.status === 'pass').length)
      readonly warningCount = computed(() => this.allChecks().filter((c) => c.status === 'warning').length)
      readonly totalCount = computed(() => this.allChecks().length)
    
      readonly filteredCategories = computed(() => {
        const filter = this.activeStatusFilter()
        const cats = this.categoriesSig()
        if (filter === 'all') return cats
        return cats
          .map((cat) => ({ ...cat, checks: cat.checks.filter((check) => check.status === filter) }))
          .filter((cat) => cat.checks.length > 0)
      })
    
      get rootClass(): string {
        return cn('w-full space-y-6', this.className)
      }
    
      get reauditIconClass(): string {
        return cn('size-3.5', this.isAuditing() && 'animate-spin')
      }
    
      get serpBoxClass(): string {
        return cn(
          'border-border/80 bg-background mx-auto rounded-xl border p-4 shadow-xs transition-colors duration-200 sm:p-5',
          this.previewDevice() === 'mobile' ? 'border-border max-w-md' : 'w-full',
        )
      }
    
      deviceButtonClass(device: 'desktop' | 'mobile'): string {
        return cn(
          'flex min-h-6 items-center gap-1.5 rounded-md px-3 py-1 text-xs font-medium transition-colors',
          this.previewDevice() === device
            ? 'bg-background text-foreground shadow-xs'
            : 'text-muted-foreground hover:text-foreground',
        )
      }
    
      filterClass(filter: 'all' | 'pass' | 'warning'): string {
        const active = this.activeStatusFilter() === filter
        const activeColor =
          filter === 'pass'
            ? 'bg-muted text-success font-medium'
            : filter === 'warning'
              ? 'bg-muted text-warning font-medium'
              : 'bg-muted text-foreground font-medium'
        return cn('h-8 px-2.5 text-xs', active ? activeColor : 'text-muted-foreground')
      }
    
      categoryIcon(icon: SeoCategory['icon']): typeof FileText {
        if (icon === 'headings') return Heading1
        if (icon === 'opengraph') return Share2
        if (icon === 'technical') return Lock
        return FileText
      }
    
      statusBadgeVariant(status: CheckStatus): 'success' | 'warning' | 'destructive' | 'info' {
        if (status === 'warning') return 'warning'
        if (status === 'fail') return 'destructive'
        if (status === 'info') return 'info'
        return 'success'
      }
    
      passingCount(category: SeoCategory): number {
        return category.checks.filter((c) => c.status === 'pass').length
      }
    
      handleReaudit(): void {
        this.isAuditing.set(true)
        setTimeout(() => this.isAuditing.set(false), 500)
      }
    
      handleCopySchema(): void {
        try {
          void navigator.clipboard.writeText(this.jsonLdSnippet)
        } catch {
          // Clipboard unavailable: still flip the label.
        }
        this.copiedSchema.set(true)
        setTimeout(() => this.copiedSchema.set(false), 1800)
      }
    
      handleCopyUrl(): void {
        try {
          void navigator.clipboard.writeText(this.currentUrl())
        } catch {
          // Clipboard unavailable: still flip the label.
        }
        this.copiedUrl.set(true)
        setTimeout(() => this.copiedUrl.set(false), 1800)
      }
    
      handleDownloadReport(): void {
        this.reportDownloaded.set(true)
        setTimeout(() => this.reportDownloaded.set(false), 2000)
      }
    }
    
  • src/app/components/blocks/seo-audit-checklist/index.ts0.2 kB

Raw manifest:https://uipkge.dev/r/angular/seo-audit-checklist.json