Skip to content

[GPT-163] Suggested articles improvements#484

Merged
Hackin7 merged 14 commits into
AdvisorySG:mainfrom
Hackin7:suggested-articles-improvements
Jun 8, 2026
Merged

[GPT-163] Suggested articles improvements#484
Hackin7 merged 14 commits into
AdvisorySG:mainfrom
Hackin7:suggested-articles-improvements

Conversation

@Hackin7

@Hackin7 Hackin7 commented May 6, 2026

Copy link
Copy Markdown
Contributor

Replaces the bottom-of-article widget's "any shared tag" matching
with Typesense semantic ranking on title+excerpt, with the existing
tag-matched SSR rendering serving as both the no-JS fallback AND the
topup pool when Typesense returns < 3 hits. Cross-collection scope.
3-card layout preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) noreply@anthropic.com

Hackin7 and others added 13 commits May 6, 2026 13:44
Replaces the bottom-of-article widget's "any shared tag" matching
with Typesense semantic ranking on title+excerpt, with the existing
tag-matched SSR rendering serving as both the no-JS fallback AND the
topup pool when Typesense returns < 3 hits. Cross-collection scope.
3-card layout preserved.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8 tasks: Typesense plumbing (config + wrapper), post-card data-slug,
Alpine component with card builder, partial wrapper changes, main.js
registration, README, end-to-end manual verification.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three cleanups from code review of Task 4:

1. The defensive filter dropping the source post compared h.slug to
   source.id — different value spaces (slug vs ObjectId hex), so the
   guard was dead code. Add 'id' to Typesense include_fields and
   compare on h.id, restoring the safety net the comment claimed.

2. Document the two intentional divergences from post-card.hbs in the
   buildCardElement comment block: hardcoded "feed-card post" instead
   of {{post_class}} (verified safe — no CSS targets per-post tag-{slug}
   classes), and omitted <img> when feature_image is empty (post-card
   uses a hidden placeholder; both are display:none).

3. pickPrimaryTag drops the type-confusing "|| hit.tags" fallback —
   tags.name is always requested in include_fields so the fallback
   couldn't help, and hit.tags has a different shape that would make
   names[i] return an object instead of a string.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Adds a new section explaining the three custom-config fields, the
search-only key security model, and where to override per-install.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Final-review item: makes the JS↔HBS mirror contract discoverable from
the HBS side. A future contributor changing post-card.hbs now has a
breadcrumb to assets/js/related-posts.js → buildCardElement().

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…empty

User reported the recommended-articles widget was including the article
itself. Live verification of Typesense's filter_by=id:!=X showed it
works correctly, so the bug must be on our side: source.id from
data-post-id="{{post.id}}" was likely empty for some reason, making
the wrapper's `if (excludeId)` gate skip the filter, AND making the
JS defensive filter useless (h.id !== '' is always true).

Three layers of defense added:

1. Partial now exposes data-post-slug="{{post.slug}}" alongside
   data-post-id. Slug is the most reliable identifier (also in the
   page URL and on every post-card).

2. typesense-search.js gains an excludeSlug option. When both are
   passed, filter_by combines them with && so a doc matching either
   identifier is excluded server-side.

3. JS defensive filter now checks id, slug, AND url — any one match
   triggers exclusion. Empty source identifiers are skipped (no false
   positives where empty string matches empty field).

Verified live: filter_by=slug:!=X excludes the named post correctly.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
…-site jumps)

Typesense indexes posts with production-absolute URLs
(https://advisory.sg/...). The JS card builder consumed hit.url verbatim
for both the inner div onclick and the stretch-link href, so clicking a
related card on localhost:2368 navigated the user OFF-SITE to the prod
domain. SSR cards were already correct because Ghost's {{url}} helper
resolves against @site.url.

Add a toLocalPath() helper that returns just pathname+search+hash from
any URL (absolute, relative, or malformed). The browser then resolves
the result against the current page's origin: localhost stays on
localhost, prod stays on prod, no environment-specific config.

Apply at three sites: the onclick interpolation, the stretch-link href,
and the defensive self-exclusion URL check (which previously compared
full URLs and never matched on localhost — now compares paths).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@wei2912
wei2912 self-requested a review May 19, 2026 14:31
@wei2912

wei2912 commented May 19, 2026

Copy link
Copy Markdown
Member

Notes to self to focus on:

  1. Look through code for sort/filtering
  2. Check recommendations logic

@wei2912 wei2912 linked an issue May 19, 2026 that may be closed by this pull request
@wei2912 wei2912 changed the title Suggested articles improvements [GPT-163] Suggested articles improvements May 19, 2026
…improvements

# Conflicts:
#	README.md
#	assets/js/main.js
#	assets/js/typesense-search.js
#	default.hbs
#	partials/post-card.hbs
@Hackin7
Hackin7 merged commit 52dd228 into AdvisorySG:main Jun 8, 2026
3 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

Stories: Suggestions don't seem to be filtered by tag

2 participants