Received: from mail.netlandish.com (mail.netlandish.com [174.136.98.166])
	by code.netlandish.com (Postfix) with ESMTP id 40037195
	for <~netlandish/links-dev@lists.code.netlandish.com>; Wed, 01 Jul 2026 00:09:24 +0000 (UTC)
Received-SPF: Pass (mailfrom) identity=mailfrom; client-ip=74.125.224.44; helo=mail-yx1-f44.google.com; envelope-from=peter@netlandish.com; receiver=<UNKNOWN> 
Authentication-Results: mail.netlandish.com;
	dkim=pass (1024-bit key; unprotected) header.d=netlandish.com header.i=@netlandish.com header.b=YfcrJBlI
Received: from mail-yx1-f44.google.com (mail-yx1-f44.google.com [74.125.224.44])
	by mail.netlandish.com (Postfix) with ESMTP id 4683B1D80BF
	for <~netlandish/links-dev@lists.code.netlandish.com>; Wed, 01 Jul 2026 00:09:44 +0000 (UTC)
Received: by mail-yx1-f44.google.com with SMTP id 956f58d0204a3-664d910cc75so46195d50.1
        for <~netlandish/links-dev@lists.code.netlandish.com>; Tue, 30 Jun 2026 17:09:44 -0700 (PDT)
DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=netlandish.com; s=google; t=1782864583; x=1783469383; darn=lists.code.netlandish.com;
        h=content-transfer-encoding:mime-version:message-id:date:subject:cc
         :to:from:from:to:cc:subject:date:message-id:reply-to;
        bh=4JVZ/I8vdFnhwVHKErxVeSQCBG/WK4bPZfJOWOMlfOw=;
        b=YfcrJBlIexWVQA4ZdUYRc8T+DLlpNl0KazyRz7XjkV2mogjhYnVPBch/kZaTrUsPoN
         OQtFOn7LZEILBSvjgTuz12v7s0TipNjBE4q3JT9F4HXsMeQO3Engw7V1g/hGq1218ZhU
         Ddvpf1Peaw3qPWLaGDXWfTE0XNr5U6jXMJRRI=
X-Google-DKIM-Signature: v=1; a=rsa-sha256; c=relaxed/relaxed;
        d=1e100.net; s=20251104; t=1782864583; x=1783469383;
        h=content-transfer-encoding:mime-version:message-id:date:subject:cc
         :to:from:x-gm-gg:x-gm-message-state:from:to:cc:subject:date
         :message-id:reply-to;
        bh=4JVZ/I8vdFnhwVHKErxVeSQCBG/WK4bPZfJOWOMlfOw=;
        b=Xe1nMF19PZNhXbEZ0IRpLQq1RNm5D5yLEMRwbNMnw8i0RmijuDjOtx5/Cpx4yREly2
         trdUxUs3p7XItJxSSuQQ9kR0sn8DvMyBokbEcSw3mBY5LqK2AjR4wWZHfl0cVDSE397V
         Qcq94bHc//oxYf39gX532tD9tgYiP9gW0dQ9hjqzc3+n1rSRx+VawPbGcd82GSzrENBb
         P3Bjmz/e2b4w+p9YI0YwHQjXBOLB7bittrh1uZRbbUhVN2JFxAFPEzsvdiatZFMIuuX1
         f5F7Ysqwfzj9fFt2tHSh2xz1ArLe7weR9WpvSTaCohwuPLMPI2/5jpL/RJXSKEAVVFvd
         j7Pg==
X-Gm-Message-State: AOJu0Yw25fDgxtgcecm8kkbtZK009oLe+8hRWHlMcRj/lkrt4HOS3kBe
	Gu+n0WOl0l74k63y+pKnbPfEXCF+qlHGWEeKuJjNVa003kC2llKzCssSXsBR1KGDo5zsfExtaO0
	vZ9K/6+23vA==
X-Gm-Gg: AfdE7ckYdj2bAQWjJNvQ5UNyhwCn5HWHD2tJKYehCqXCrNlTc/yCVNdfpMYb3EyNRAU
	IUuWFuyfzbzL0+raZqwNlohtPGVbNc0c2SE9PkJDHTiqj3r/aQgnb4u9SM3LwyMbiOFddpkLQBc
	dA5RIOjp9J8OHN3W2wPkLzhRtMK36NdfupmHiPpd6BV9OVE2KcQD1hRambtgxzLj/7lTQqXojPr
	5BTHT/Qj8hgx5yWlBLYa1/zzmHQL42nKacEIu3QB9hqzrAhC1dU2wC9tSLln15eLGAELrCZ+py+
	qwuFyVznFyU4TdBJwD5L42oLAMCS86ZjhfWvsESwrOEUCC4VjAU5sIs0K4Q+TAWLo8OeT5IBrO/
	ju/skziyo2zCBe2VbNYP97xCoCvoKHIMxEp0TUO/R0tTSfjQ69SvLUg8aOOWh631G4EPlTAzJNc
	G91pi8N7dx9A==
X-Received: by 2002:a53:bac2:0:b0:664:ae6a:f00 with SMTP id 956f58d0204a3-664f9a8aa4emr4272405d50.74.1782864583309;
        Tue, 30 Jun 2026 17:09:43 -0700 (PDT)
Received: from localhost ([186.77.196.89])
        by smtp.gmail.com with ESMTPSA id 956f58d0204a3-665014bb808sm1502963d50.12.2026.06.30.17.09.42
        (version=TLS1_3 cipher=TLS_AES_256_GCM_SHA384 bits=256/256);
        Tue, 30 Jun 2026 17:09:42 -0700 (PDT)
From: Peter Sanchez <peter@netlandish.com>
To: ~netlandish/links-dev@lists.code.netlandish.com
Cc: Peter Sanchez <peter@netlandish.com>
Subject: [PATCH links] tags: fix bug where tags duplicated due to an organizations larger follower count
Date: Tue, 30 Jun 2026 18:09:36 -0600
Message-ID: <20260701000938.21712-1-peter@netlandish.com>
X-Mailer: git-send-email 2.54.0
MIME-Version: 1.0
Content-Transfer-Encoding: 8bit

---
 models/base_url.go | 14 ++++++++++----
 models/org_link.go |  9 ++++++---
 2 files changed, 16 insertions(+), 7 deletions(-)

diff --git a/models/base_url.go b/models/base_url.go
index 926a75d..cde810f 100644
--- a/models/base_url.go
+++ b/models/base_url.go
@@ -59,19 +59,25 @@ func GetBaseURLs(ctx context.Context, opts *database.FilterOptions) ([]*BaseURL,
 		opts = &database.FilterOptions{}
 	}
 	tz := timezone.ForContext(ctx)
-	tagOrder := TagRelationOrderString(GetDefaultTagOrder(ctx))
 	urls := make([]*BaseURL, 0)
 	if err := database.WithTx(ctx, database.TxOptionsRO, func(tx *sql.Tx) error {
 		q := opts.GetBuilder(nil)
 		rows, err := q.
 			Columns("b.id", "b.url", "b.title", "b.counter", "b.data", "b.public_ready", "b.hash",
 				"b.parse_attempts", "b.last_parse_attempt", "b.created_on", "b.updated_on", "b.visibility",
-				fmt.Sprintf("json_agg(CASE WHEN t.id IS NOT NULL THEN json_build_object('id', t.id, 'name', tl.name, 'slug', t.slug, 'createdOn', t.created_on) END ORDER BY %s)::jsonb", tagOrder)).
+				`COALESCE((
+					SELECT json_agg(t ORDER BY LOWER(t.name) ASC) FROM (
+						SELECT t.id, MIN(tl.name) AS name, t.slug, t.created_on AS "createdOn"
+						FROM tag_links tl
+						JOIN org_links _ol ON _ol.id = tl.org_link_id
+						JOIN tags t ON t.id = tl.tag_id
+						WHERE _ol.base_url_id = b.id
+						GROUP BY t.id, t.slug, t.created_on
+					) t
+				), '[]'::json)::jsonb`).
 			From("base_urls b").
 			LeftJoin("org_links ol ON ol.base_url_id = b.id").
 			LeftJoin("organizations o ON o.id = ol.org_id").
-			LeftJoin("tag_links tl ON tl.org_link_id = ol.id").
-			LeftJoin("tags t ON t.id = tl.tag_id").
 			GroupBy("b.id").
 			Distinct().
 			PlaceholderFormat(database.GetPlaceholderFormat()).
diff --git a/models/org_link.go b/models/org_link.go
index 19a0a1e..0b83a3c 100644
--- a/models/org_link.go
+++ b/models/org_link.go
@@ -37,14 +37,17 @@ func GetOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink,
 		rows, err := q.
 			Columns("ol.id", "ol.title", "ol.url", "ol.description", "ol.base_url_id", "ol.org_id", "ol.user_id",
 				"ol.visibility", "ol.unread", "ol.starred", "ol.archive_url", "ol.type", "ol.hash",
-				"ol.created_on", "ol.updated_on", "o.slug", "u.full_name", fmt.Sprintf("json_agg(CASE WHEN t.id IS NOT NULL THEN json_build_object('id', t.id, 'name', tl.name, 'slug', t.slug, 'createdOn', t.created_on) END ORDER BY %s)::jsonb", tagOrder), "b.data", "b.counter", "b.hash").
+				"ol.created_on", "ol.updated_on", "o.slug", "u.full_name", fmt.Sprintf(`COALESCE((
+					SELECT json_agg(json_build_object('id', t.id, 'name', tl.name, 'slug', t.slug, 'createdOn', t.created_on) ORDER BY %s)
+					FROM tag_links tl
+					JOIN tags t ON t.id = tl.tag_id
+					WHERE tl.org_link_id = ol.id
+				), '[]'::json)::jsonb`, tagOrder), "b.data", "b.counter", "b.hash").
 			From("org_links ol").
 			Join("organizations o ON o.id = ol.org_id").
 			Join("users u ON ol.user_id = u.id").
 			Join("base_urls b ON b.id = ol.base_url_id").
 			LeftJoin("organization_users ou ON ou.org_id = ol.org_id").
-			LeftJoin("tag_links tl ON tl.org_link_id = ol.id").
-			LeftJoin("tags t ON t.id = tl.tag_id").
 			LeftJoin("followers f ON f.org_id = ol.org_id").
 			GroupBy("ol.id", "o.slug", "u.full_name", "b.data", "b.counter", "b.hash").
 			Distinct().
-- 
2.54.0

