diff --git a/includes/class-peptide-news-rest-api.php b/includes/class-peptide-news-rest-api.php index d3a657e..db4b737 100644 --- a/includes/class-peptide-news-rest-api.php +++ b/includes/class-peptide-news-rest-api.php @@ -146,6 +146,17 @@ class Peptide_News_Rest_API { $offset ) ); + // Strip source name from titles and summaries (e.g. "Article Title - Toronto Star"). + foreach ( $articles as $article ) { + $article->title = $this->strip_source_suffix( $article->title, $article->source, $article->source_url ); + if ( ! empty( $article->ai_summary ) ) { + $article->ai_summary = $this->strip_source_suffix( $article->ai_summary, $article->source, $article->source_url ); + } + if ( ! empty( $article->excerpt ) ) { + $article->excerpt = $this->strip_source_suffix( $article->excerpt, $article->source, $article->source_url ); + } + } + $total = $wpdb->get_var( "SELECT COUNT(*) FROM {$table} WHERE is_active = 1" ); $response = rest_ensure_response( array( @@ -163,6 +174,69 @@ class Peptide_News_Rest_API { return $response; } + /** + * Strip the source/publisher name from the end of a string. + * + * Handles common separator patterns like " - Source", " | Source", + * " — Source", and " – Source". Uses case-insensitive matching against + * the source field. Also extracts publisher names from source URLs + * and has a regex fallback for common RSS title patterns. + * + * @param string $text The text to clean. + * @param string $source The source name to strip. + * @param string $source_url The source URL (used to extract domain-based publisher names). + * @return string Cleaned text. + */ + private function strip_source_suffix( $text, $source, $source_url = '' ) { + if ( empty( $text ) ) { + return $text; + } + + $separators = array( ' - ', ' | ', ' — ', ' – ', ' // ' ); + + // Build list of source names to try matching against. + $source_names = array(); + if ( ! empty( $source ) ) { + $source_names[] = $source; + } + + // Extract domain-based publisher name (e.g., "News-Medical" from "news-medical.net"). + if ( ! empty( $source_url ) ) { + $host = wp_parse_url( $source_url, PHP_URL_HOST ); + if ( $host ) { + $host = preg_replace( '/^www\./', '', $host ); + $parts = explode( '.', $host ); + if ( count( $parts ) >= 2 ) { + $domain_name = $parts[ count( $parts ) - 2 ]; + $source_names[] = str_replace( '-', ' ', $domain_name ); + $source_names[] = $domain_name; + } + } + } + + // Try exact (case-insensitive) match against known source names. + foreach ( $source_names as $name ) { + foreach ( $separators as $sep ) { + $suffix = $sep . $name; + $suffix_len = strlen( $suffix ); + if ( strlen( $text ) > $suffix_len && strcasecmp( substr( $text, -$suffix_len ), $suffix ) === 0 ) { + return rtrim( substr( $text, 0, -$suffix_len ) ); + } + } + } + + // Fallback: strip trailing " - Short Publisher Name" patterns (1-5 words). + // Only strips if the part after the separator looks like a publisher name + // (starts with uppercase, no sentence-ending punctuation). + $pattern = '/\s+[-\x{2013}\x{2014}|]\s+[A-Z][A-Za-z0-9\s.\'\-]{0,50}$/u'; + $candidate = preg_replace( $pattern, '', $text ); + if ( $candidate !== $text && strlen( $candidate ) > strlen( $text ) * 0.4 ) { + return rtrim( $candidate ); + } + + return $text; + } + /** * GET /analytics/top */ diff --git a/public/css/public-style.css b/public/css/public-style.css index e4c4079..a687282 100644 --- a/public/css/public-style.css +++ b/public/css/public-style.css @@ -35,10 +35,6 @@ --pn-source-color: #1f2937; --pn-source-bg: #f3f4f6; - /* Tag Colors */ - --pn-tag-bg: #eff6ff; - --pn-tag-color: #1e40af; - /* Focus & System */ --pn-focus-ring: #2563eb; --pn-error: #dc2626; @@ -71,10 +67,6 @@ --pn-source-color: #e2e8f0; --pn-source-bg: #1e293b; - /* Tag Colors */ - --pn-tag-bg: #1e3a8a; - --pn-tag-color: #bfdbfe; - /* Focus & System */ --pn-focus-ring: #60a5fa; --pn-error: #f87171; @@ -258,42 +250,6 @@ content: ''; } -/* ──────────────────────────────────────────────────────────────── - TAGS - ────────────────────────────────────────────────────────────── */ - -.pn-tags { - display: flex; - flex-wrap: wrap; - gap: 8px; - margin: auto 0 0 0; -} - -.pn-tag { - display: inline-block; - padding: 4px 10px; - font-size: 12px; - font-weight: 500; - line-height: 1.3; - letter-spacing: 0.15px; - color: var(--pn-tag-color); - background: var(--pn-tag-bg); - border-radius: 20px; - cursor: pointer; - transition: all 0.15s ease; -} - -.pn-tag:hover { - background: color-mix(in srgb, var(--pn-tag-bg) 70%, var(--pn-tag-color) 30%); - transform: translateY(-1px); - box-shadow: 0 4px 12px rgba(37, 99, 235, 0.15); -} - -.pn-tag:focus-visible { - outline: 2px solid var(--pn-focus-ring); - outline-offset: 2px; -} - /* ══════════════════════════════════════════════════════════════════ LOADING STATES & SKELETON ══════════════════════════════════════════════════════════════════ */ @@ -367,19 +323,6 @@ animation: pn-shimmer 2s infinite 0.4s; } -.pn-skeleton-tags { - display: flex; - gap: 8px; - margin-top: 16px; -} - -.pn-skeleton-tag { - height: 10px; - width: 60px; - border-radius: 20px; - animation: pn-shimmer 2s infinite 0.5s; -} - @keyframes pn-shimmer { 0% { background-position: 200% 0; } 100% { background-position: -200% 0; } @@ -597,16 +540,6 @@ font-size: 12px; } - .pn-tag { - padding: 3px 8px; - font-size: 11.5px; - } - - .pn-tags { - gap: 6px; - margin-top: 10px; - } - .pn-skeleton-card { padding: 16px; margin-bottom: 16px; @@ -699,11 +632,6 @@ color: var(--pn-text-tertiary); } - .pn-tag { - background: var(--pn-tag-bg); - color: var(--pn-tag-color); - } - .pn-page-btn { background: transparent; border-color: var(--pn-border-light); @@ -748,8 +676,7 @@ color: var(--pn-text-tertiary); } - .pn-skeleton-line, - .pn-skeleton-tag { + .pn-skeleton-line { background: linear-gradient( 90deg, var(--pn-loading) 0%, @@ -781,8 +708,7 @@ .pn-article-title a:focus-visible, .pn-page-btn:focus-visible, - .pn-retry-btn:focus-visible, - .pn-tag:focus-visible { + .pn-retry-btn:focus-visible { outline-width: 3px; } } @@ -791,11 +717,9 @@ @media (prefers-reduced-motion: reduce) { .pn-article, .pn-article-title a, - .pn-tag, .pn-page-btn, .pn-retry-btn, - .pn-skeleton-line, - .pn-skeleton-tag { + .pn-skeleton-line { animation: none !important; transition: none !important; } diff --git a/public/js/peptide-news-feed.js b/public/js/peptide-news-feed.js index c319f9f..773628c 100644 --- a/public/js/peptide-news-feed.js +++ b/public/js/peptide-news-feed.js @@ -148,17 +148,6 @@ function trackClick(articleId) { /* ── Components ─────────────────────────────────────────────────────── */ -/** - * Single keyword tag pill. - */ -const Tag = memo(function Tag({ - label -}) { - return /*#__PURE__*/React.createElement("span", { - className: "pn-tag" - }, label); -}); - /** * Single article card. */ @@ -166,8 +155,6 @@ const ArticleCard = memo(function ArticleCard({ article }) { const summary = decodeEntities(article.ai_summary || article.excerpt || ''); - const tagSource = article.tags || article.categories || ''; - const tags = tagSource ? tagSource.split(',').map(t => t.trim()).filter(Boolean).slice(0, 5) : []; const handleClick = useCallback(() => { trackClick(article.id); }, [article.id]); @@ -196,14 +183,7 @@ const ArticleCard = memo(function ArticleCard({ className: "pn-article-excerpt" }, summary), article.author && /*#__PURE__*/React.createElement("span", { className: "pn-author" - }, article.author), tags.length > 0 && /*#__PURE__*/React.createElement("div", { - className: "pn-tags", - role: "list", - "aria-label": "Keywords" - }, tags.map(tag => /*#__PURE__*/React.createElement(Tag, { - key: tag, - label: tag - })))); + }, article.author)); }); /** @@ -229,15 +209,7 @@ function LoadingSkeleton({ className: "pn-skeleton-line pn-skeleton-excerpt" }), /*#__PURE__*/React.createElement("div", { className: "pn-skeleton-line pn-skeleton-excerpt-short" - }), /*#__PURE__*/React.createElement("div", { - className: "pn-skeleton-tags" - }, /*#__PURE__*/React.createElement("div", { - className: "pn-skeleton-tag" - }), /*#__PURE__*/React.createElement("div", { - className: "pn-skeleton-tag" - }), /*#__PURE__*/React.createElement("div", { - className: "pn-skeleton-tag" - }))))); + })))); } /**