Seo Audit Checklist
blockmarketingAhrefs and SEMrush style on-page SEO analyzer, meta tag auditor, Google SERP snippet preview simulator, heading hierarchy validator, and Core Web Vitals checklist.
Installation
$pnpm dlx uipkge-ng@latest add seo-audit-checklist$npx uipkge-ng@latest add seo-audit-checklist$yarn dlx uipkge-ng@latest add seo-audit-checklist$bunx uipkge-ng@latest add seo-audit-checklistManifest:
https://uipkge.dev/r/angular/seo-audit-checklist.jsonInstalls to:src/app/components/blocks/seo-audit-checklist/Variants
Loading interactive previews…
Loading interactive previews…
Schema
Type aliases exported from this item's source. Use these to shape the data you pass in.
SeoCheckIteminterface SeoCheckItem {
id: string
title: string
description: string
status: CheckStatus
value?: string
recommendation?: string
tag?: string
}SeoCategoryinterface SeoCategory {
id: string
name: string
icon: 'meta' | 'headings' | 'opengraph' | 'technical'
score: number
checks: SeoCheckItem[]
}CoreVitalMetricinterface 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
export { UiSeoAuditChecklistComponent, type CheckStatus, type SeoCheckItem, type SeoCategory, type CoreVitalMetric, } from './seo-audit-checklist.component'
Raw manifest:https://uipkge.dev/r/angular/seo-audit-checklist.json