import { decodeEntities } from '@wordpress/html-entities'; import { __ } from '@wordpress/i18n'; const ROW_LIMIT = 10; // Subset of the /accelerate/v1/top post shape this tile needs. `url` is the // post permalink (absolute, or null for non-public types) — used for the // click-to-drill path filter (#669). export type TopPost = { id: number; title: string; views: number; url?: string | null; }; // Matches the BreakdownPanel HostList row contract (key + value), with an // optional label override so the row can show a decoded title while keeping // the post id as its stable rank/React identity. `path` is the relative URL // used to drill the view by this post (#669); absent for non-public posts. export type ContentItem = { key: string; value: number; label: string; path?: string; }; /** * Map view-ranked /top posts to HostList rows. Pure (no fetch, no React) so * the decode + fallback + slice logic is unit-testable in isolation, like * prettyLabel. `/top` already returns posts ordered by views, so we just cap * the list. Post id is the row key (stable identity); the decoded title is the * display label, falling back to a localized "(no title)" for untitled posts. */ /** * Reduce an absolute permalink to the relative path the `filter.path` facet * expects (the server reconstructs it via `home_url( $filter->path )`). Returns * undefined for a missing/unparseable url so the row falls back to inert. */ function toRelativePath( url?: string | null ): string | undefined { if ( ! url ) { return undefined; } try { const parsed = new URL( url ); const home = window.AltisAccelerateDashboardData?.home_url; const base = home ? new URL( home ) : null; // Guard against external permalinks using the home origin when known, // falling back to window.location.origin. const expectedOrigin = base ? base.origin : window.location.origin; if ( parsed.origin !== expectedOrigin ) { return undefined; } // Strip the home base path so filter.path is relative to home_url, // matching how the server reconstructs it via home_url( $filter->path ). // Handles subdirectory installs (e.g. home_url = https://site.com/wp). let path = parsed.pathname; const basePath = base ? base.pathname.replace( /\/+$/, '' ) : ''; if ( basePath ) { if ( path === basePath || path.startsWith( basePath + '/' ) ) { path = path.slice( basePath.length ) || '/'; } else { // Same-origin but outside the WP install's base path — not a // drillable content path, so leave the row inert. return undefined; } } return path + parsed.search; } catch { return undefined; } } export function toContentItems( posts: TopPost[], limit: number = ROW_LIMIT ): ContentItem[] { return posts .slice( 0, limit ) .map( post => ( { key: String( post.id ), value: post.views, label: decodeEntities( post.title || '' ) || __( '(no title)', 'altis' ), path: toRelativePath( post.url ), } ) ); }