Skip to main content
Glama
snapshot-summary.ts8.36 kB
/** * Snapshot Summary Generator * * Analyzes page snapshots to generate human-readable summaries * of content structure for the "aha moment" UX in CLI. * * @package WP_Navigator_Pro * @since 1.2.0 */ import type { BlockSnapshot, PageSnapshot } from './snapshots/types.js'; // ============================================================================= // Types // ============================================================================= /** * Section type detection result */ export interface SectionInfo { /** Section type (hero, features, cta, grid, etc.) */ type: string; /** Human-readable description */ description: string; /** Component breakdown (e.g., "heading + text + button") */ components?: string; } /** * Page content summary */ export interface PageContentSummary { /** Total block count */ totalBlocks: number; /** Detected sections */ sections: SectionInfo[]; /** Summary lines for display */ summaryLines: string[]; } // ============================================================================= // Section Detection // ============================================================================= /** * Block patterns that suggest section types */ const SECTION_PATTERNS: Record<string, { requiredBlocks: string[]; optionalBlocks?: string[]; minBlocks?: number; description: string; }> = { hero: { requiredBlocks: ['core/heading', 'core/paragraph'], optionalBlocks: ['core/buttons', 'core/image', 'core/cover'], description: 'hero section', }, features: { requiredBlocks: ['core/columns'], optionalBlocks: ['core/heading', 'core/image'], minBlocks: 3, description: 'feature grid', }, cta: { requiredBlocks: ['core/buttons'], optionalBlocks: ['core/heading', 'core/paragraph'], description: 'CTA section', }, testimonial: { requiredBlocks: ['core/quote'], optionalBlocks: ['core/image', 'core/paragraph'], description: 'testimonial', }, gallery: { requiredBlocks: ['core/gallery'], description: 'image gallery', }, media: { requiredBlocks: ['core/media-text'], description: 'media + text section', }, }; /** * Get all block names recursively */ function getAllBlockNames(blocks: BlockSnapshot[]): string[] { const names: string[] = []; for (const block of blocks) { names.push(block.blockName); if (block.innerBlocks.length > 0) { names.push(...getAllBlockNames(block.innerBlocks)); } } return names; } /** * Count blocks by type */ function countBlockTypes(blocks: BlockSnapshot[]): Map<string, number> { const counts = new Map<string, number>(); const allNames = getAllBlockNames(blocks); for (const name of allNames) { counts.set(name, (counts.get(name) || 0) + 1); } return counts; } /** * Get simple block type name (without core/ prefix) */ function getSimpleBlockName(blockName: string): string { return blockName.replace('core/', '').replace(/-/g, ' '); } /** * Analyze a group of blocks to detect section type */ function detectSectionType(blocks: BlockSnapshot[]): SectionInfo | null { const blockNames = getAllBlockNames(blocks); const uniqueNames = new Set(blockNames); // Check each pattern for (const [type, pattern] of Object.entries(SECTION_PATTERNS)) { const hasRequired = pattern.requiredBlocks.every((b) => uniqueNames.has(b)); if (!hasRequired) continue; // Check minimum blocks if specified if (pattern.minBlocks && blockNames.length < pattern.minBlocks) continue; // Build component description const components: string[] = []; for (const block of blocks) { if (block.blockName === 'core/heading') components.push('heading'); else if (block.blockName === 'core/paragraph') components.push('text'); else if (block.blockName === 'core/buttons') components.push('button'); else if (block.blockName === 'core/image') components.push('image'); } return { type, description: pattern.description, components: components.length > 0 ? components.join(' + ') : undefined, }; } return null; } /** * Analyze top-level structure and detect sections */ function detectSections(blocks: BlockSnapshot[]): SectionInfo[] { const sections: SectionInfo[] = []; let currentGroup: BlockSnapshot[] = []; for (const block of blocks) { // Container blocks are potential sections if (['core/group', 'core/cover', 'core/columns'].includes(block.blockName)) { // Flush current group if (currentGroup.length > 0) { const section = detectSectionType(currentGroup); if (section) sections.push(section); currentGroup = []; } // Analyze this container const innerBlocks = block.innerBlocks.length > 0 ? block.innerBlocks : [block]; const section = detectSectionType(innerBlocks); if (section) { sections.push(section); } else if (block.blockName === 'core/columns') { // Columns without clear pattern - describe as grid const colCount = block.innerBlocks.length; sections.push({ type: 'grid', description: `${colCount}-column grid`, }); } else { // Generic section sections.push({ type: 'section', description: 'content section', }); } } else { // Accumulate non-container blocks currentGroup.push(block); } } // Flush remaining group if (currentGroup.length > 0) { const section = detectSectionType(currentGroup); if (section) { sections.push(section); } else if (currentGroup.length > 0) { // Describe based on block types const types = currentGroup.map((b) => getSimpleBlockName(b.blockName)); const uniqueTypes = [...new Set(types)]; sections.push({ type: 'content', description: uniqueTypes.slice(0, 3).join(', ') + (uniqueTypes.length > 3 ? '...' : ''), }); } } return sections; } // ============================================================================= // Summary Generation // ============================================================================= /** * Generate human-readable summary of page content */ export function summarizePageContent(snapshot: PageSnapshot): PageContentSummary { const blocks = snapshot.content.blocks; const totalBlocks = countBlockTypes(blocks); const totalCount = Array.from(totalBlocks.values()).reduce((a, b) => a + b, 0); // Detect sections const sections = detectSections(blocks); // Build summary lines const summaryLines: string[] = []; if (sections.length === 0 && totalCount === 0) { summaryLines.push('Empty page (no blocks)'); } else if (sections.length === 0) { // No clear sections, describe by block types const counts = countBlockTypes(blocks); const topTypes = Array.from(counts.entries()) .sort((a, b) => b[1] - a[1]) .slice(0, 5); for (const [blockName, count] of topTypes) { const name = getSimpleBlockName(blockName); summaryLines.push(`${count} ${name}${count > 1 ? 's' : ''}`); } } else { // Describe sections const sectionCounts = new Map<string, { count: number; info: SectionInfo }>(); for (const section of sections) { const key = section.description; const existing = sectionCounts.get(key); if (existing) { existing.count++; } else { sectionCounts.set(key, { count: 1, info: section }); } } for (const [desc, { count, info }] of sectionCounts) { const countStr = count > 1 ? `${count} ` : ''; const suffix = count > 1 && !desc.endsWith('s') ? 's' : ''; const components = info.components ? ` (${info.components})` : ''; summaryLines.push(`${countStr}${desc}${suffix}${components}`); } } return { totalBlocks: totalCount, sections, summaryLines, }; } /** * Check if this is the first snapshot (no existing snapshots directory) */ export function isFirstSnapshot(snapshotsDir: string): boolean { try { const fs = require('fs'); return !fs.existsSync(snapshotsDir); } catch { return true; } } /** * Get the "aha moment" message for first snapshot */ export function getFirstSnapshotMessage(): string { return 'Your site is now "AI-readable."'; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/littlebearapps/wp-navigator-mcp'

If you have feedback or need assistance with the MCP directory API, please join our Discord server