~netlandish/links-dev

links: Showing save count on all relevant listing pages. v1 APPLIED

Peter Sanchez: 1
 Showing save count on all relevant listing pages.

 13 files changed, 1098 insertions(+), 32 deletions(-)
Export patchset (mbox)
How do I use this?

Copy & paste the following snippet into your terminal to import this patchset into git:

curl -s https://lists.code.netlandish.com/~netlandish/links-dev/patches/128/mbox | git am -3
Learn more about email & git

[PATCH links] Showing save count on all relevant listing pages. Export this patch

New "saved" page will list all the users/orgs who bookmarked a specific
link.

Implements: https://todo.code.netlandish.com/~netlandish/links/106
Changelog-added: Save count is displayed on all relevant listing pages.
Changelog-added: New page to view all users/orgs who bookmarked a
  specific link.
---
 api/graph/generated.go        | 811 +++++++++++++++++++++++++++++++++-
 api/graph/model/models_gen.go |   9 +
 api/graph/schema.graphqls     |  14 +
 api/graph/schema.resolvers.go | 113 ++++-
 cmd/links/main.go             |   3 +
 cmd/test/helpers.go           |   3 +
 core/routes.go                | 120 +++++
 models/models.go              |  12 +-
 models/org_link.go            |   7 +-
 models/utils.go               |  16 +-
 templates/feed.html           |   1 +
 templates/link_list.html      |  20 +-
 values.go                     |   1 +
 13 files changed, 1098 insertions(+), 32 deletions(-)

diff --git a/api/graph/generated.go b/api/graph/generated.go
index 61befad..c964512 100644
--- a/api/graph/generated.go
+++ b/api/graph/generated.go
@@ -130,6 +130,15 @@ type ComplexityRoot struct {
		Status func(childComplexity int) int
	}

	BookmarkCursor struct {
		Count       func(childComplexity int) int
		Description func(childComplexity int) int
		Result      func(childComplexity int) int
		TagCloud    func(childComplexity int) int
		Title       func(childComplexity int) int
		URL         func(childComplexity int) int
	}

	DeletePayload struct {
		Message  func(childComplexity int) int
		ObjectID func(childComplexity int) int
@@ -277,25 +286,27 @@ type ComplexityRoot struct {
	}

	OrgLink struct {
		ArchiveURL  func(childComplexity int) int
		Author      func(childComplexity int) int
		BaseURLData func(childComplexity int) int
		BaseURLID   func(childComplexity int) int
		CreatedOn   func(childComplexity int) int
		Description func(childComplexity int) int
		Hash        func(childComplexity int) int
		ID          func(childComplexity int) int
		OrgID       func(childComplexity int) int
		OrgSlug     func(childComplexity int) int
		Starred     func(childComplexity int) int
		Tags        func(childComplexity int) int
		Title       func(childComplexity int) int
		Type        func(childComplexity int) int
		URL         func(childComplexity int) int
		Unread      func(childComplexity int) int
		UpdatedOn   func(childComplexity int) int
		UserID      func(childComplexity int) int
		Visibility  func(childComplexity int) int
		ArchiveURL     func(childComplexity int) int
		Author         func(childComplexity int) int
		BaseURLCounter func(childComplexity int) int
		BaseURLData    func(childComplexity int) int
		BaseURLHash    func(childComplexity int) int
		BaseURLID      func(childComplexity int) int
		CreatedOn      func(childComplexity int) int
		Description    func(childComplexity int) int
		Hash           func(childComplexity int) int
		ID             func(childComplexity int) int
		OrgID          func(childComplexity int) int
		OrgSlug        func(childComplexity int) int
		Starred        func(childComplexity int) int
		Tags           func(childComplexity int) int
		Title          func(childComplexity int) int
		Type           func(childComplexity int) int
		URL            func(childComplexity int) int
		Unread         func(childComplexity int) int
		UpdatedOn      func(childComplexity int) int
		UserID         func(childComplexity int) int
		Visibility     func(childComplexity int) int
	}

	OrgLinkCursor struct {
@@ -392,6 +403,7 @@ type ComplexityRoot struct {
		GetAdminOrgStats      func(childComplexity int, id int) int
		GetAdminOrganizations func(childComplexity int, input *model.GetAdminOrganizationsInput) int
		GetAuditLogs          func(childComplexity int, input *model.AuditLogInput) int
		GetBookmarks          func(childComplexity int, hash string, tags *string) int
		GetDomain             func(childComplexity int, id int) int
		GetDomains            func(childComplexity int, orgSlug *string, service *model.DomainService) int
		GetFeed               func(childComplexity int, input *model.GetFeedInput) int
@@ -532,6 +544,7 @@ type QueryResolver interface {
	GetPaymentHistory(ctx context.Context, input *model.GetPaymentInput) (*model.PaymentCursor, error)
	GetPopularLinks(ctx context.Context, input *model.PopularLinksInput) (*model.PopularLinkCursor, error)
	GetOrgLink(ctx context.Context, hash string) (*models.OrgLink, error)
	GetBookmarks(ctx context.Context, hash string, tags *string) (*model.BookmarkCursor, error)
	GetOrgLinks(ctx context.Context, input *model.GetLinkInput) (*model.OrgLinkCursor, error)
	GetOrgMembers(ctx context.Context, orgSlug string) ([]*models.User, error)
	GetDomains(ctx context.Context, orgSlug *string, service *model.DomainService) ([]*models.Domain, error)
@@ -857,6 +870,48 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.BillingSettings.Status(childComplexity), true

	case "BookmarkCursor.count":
		if e.complexity.BookmarkCursor.Count == nil {
			break
		}

		return e.complexity.BookmarkCursor.Count(childComplexity), true

	case "BookmarkCursor.description":
		if e.complexity.BookmarkCursor.Description == nil {
			break
		}

		return e.complexity.BookmarkCursor.Description(childComplexity), true

	case "BookmarkCursor.result":
		if e.complexity.BookmarkCursor.Result == nil {
			break
		}

		return e.complexity.BookmarkCursor.Result(childComplexity), true

	case "BookmarkCursor.tagCloud":
		if e.complexity.BookmarkCursor.TagCloud == nil {
			break
		}

		return e.complexity.BookmarkCursor.TagCloud(childComplexity), true

	case "BookmarkCursor.title":
		if e.complexity.BookmarkCursor.Title == nil {
			break
		}

		return e.complexity.BookmarkCursor.Title(childComplexity), true

	case "BookmarkCursor.url":
		if e.complexity.BookmarkCursor.URL == nil {
			break
		}

		return e.complexity.BookmarkCursor.URL(childComplexity), true

	case "DeletePayload.message":
		if e.complexity.DeletePayload.Message == nil {
			break
@@ -1738,6 +1793,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.OrgLink.Author(childComplexity), true

	case "OrgLink.baseUrlCounter":
		if e.complexity.OrgLink.BaseURLCounter == nil {
			break
		}

		return e.complexity.OrgLink.BaseURLCounter(childComplexity), true

	case "OrgLink.baseUrlData":
		if e.complexity.OrgLink.BaseURLData == nil {
			break
@@ -1745,6 +1807,13 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.OrgLink.BaseURLData(childComplexity), true

	case "OrgLink.baseUrlHash":
		if e.complexity.OrgLink.BaseURLHash == nil {
			break
		}

		return e.complexity.OrgLink.BaseURLHash(childComplexity), true

	case "OrgLink.baseUrlId":
		if e.complexity.OrgLink.BaseURLID == nil {
			break
@@ -2307,6 +2376,18 @@ func (e *executableSchema) Complexity(typeName, field string, childComplexity in

		return e.complexity.Query.GetAuditLogs(childComplexity, args["input"].(*model.AuditLogInput)), true

	case "Query.getBookmarks":
		if e.complexity.Query.GetBookmarks == nil {
			break
		}

		args, err := ec.field_Query_getBookmarks_args(context.TODO(), rawArgs)
		if err != nil {
			return 0, false
		}

		return e.complexity.Query.GetBookmarks(childComplexity, args["hash"].(string), args["tags"].(*string)), true

	case "Query.getDomain":
		if e.complexity.Query.GetDomain == nil {
			break
@@ -4289,6 +4370,65 @@ func (ec *executionContext) field_Query_getAuditLogs_argsInput(
	return zeroVal, nil
}

func (ec *executionContext) field_Query_getBookmarks_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}
	arg0, err := ec.field_Query_getBookmarks_argsHash(ctx, rawArgs)
	if err != nil {
		return nil, err
	}
	args["hash"] = arg0
	arg1, err := ec.field_Query_getBookmarks_argsTags(ctx, rawArgs)
	if err != nil {
		return nil, err
	}
	args["tags"] = arg1
	return args, nil
}
func (ec *executionContext) field_Query_getBookmarks_argsHash(
	ctx context.Context,
	rawArgs map[string]interface{},
) (string, error) {
	// We won't call the directive if the argument is null.
	// Set call_argument_directives_with_null to true to call directives
	// even if the argument is null.
	_, ok := rawArgs["hash"]
	if !ok {
		var zeroVal string
		return zeroVal, nil
	}

	ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("hash"))
	if tmp, ok := rawArgs["hash"]; ok {
		return ec.unmarshalNString2string(ctx, tmp)
	}

	var zeroVal string
	return zeroVal, nil
}

func (ec *executionContext) field_Query_getBookmarks_argsTags(
	ctx context.Context,
	rawArgs map[string]interface{},
) (*string, error) {
	// We won't call the directive if the argument is null.
	// Set call_argument_directives_with_null to true to call directives
	// even if the argument is null.
	_, ok := rawArgs["tags"]
	if !ok {
		var zeroVal *string
		return zeroVal, nil
	}

	ctx = graphql.WithPathContext(ctx, graphql.NewPathWithField("tags"))
	if tmp, ok := rawArgs["tags"]; ok {
		return ec.unmarshalOString2ᚖstring(ctx, tmp)
	}

	var zeroVal *string
	return zeroVal, nil
}

func (ec *executionContext) field_Query_getDomain_args(ctx context.Context, rawArgs map[string]interface{}) (map[string]interface{}, error) {
	var err error
	args := map[string]interface{}{}
@@ -7039,6 +7179,323 @@ func (ec *executionContext) fieldContext_BillingSettings_status(_ context.Contex
	return fc, nil
}

func (ec *executionContext) _BookmarkCursor_result(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_BookmarkCursor_result(ctx, field)
	if err != nil {
		return graphql.Null
	}
	ctx = graphql.WithFieldContext(ctx, fc)
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.Result, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.([]*models.OrgLink)
	fc.Result = res
	return ec.marshalNOrgLink2ᚕᚖlinksᚋmodelsᚐOrgLink(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_BookmarkCursor_result(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "BookmarkCursor",
		Field:      field,
		IsMethod:   false,
		IsResolver: false,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			switch field.Name {
			case "id":
				return ec.fieldContext_OrgLink_id(ctx, field)
			case "title":
				return ec.fieldContext_OrgLink_title(ctx, field)
			case "description":
				return ec.fieldContext_OrgLink_description(ctx, field)
			case "url":
				return ec.fieldContext_OrgLink_url(ctx, field)
			case "hash":
				return ec.fieldContext_OrgLink_hash(ctx, field)
			case "baseUrlId":
				return ec.fieldContext_OrgLink_baseUrlId(ctx, field)
			case "orgId":
				return ec.fieldContext_OrgLink_orgId(ctx, field)
			case "userId":
				return ec.fieldContext_OrgLink_userId(ctx, field)
			case "visibility":
				return ec.fieldContext_OrgLink_visibility(ctx, field)
			case "unread":
				return ec.fieldContext_OrgLink_unread(ctx, field)
			case "starred":
				return ec.fieldContext_OrgLink_starred(ctx, field)
			case "archiveUrl":
				return ec.fieldContext_OrgLink_archiveUrl(ctx, field)
			case "type":
				return ec.fieldContext_OrgLink_type(ctx, field)
			case "tags":
				return ec.fieldContext_OrgLink_tags(ctx, field)
			case "author":
				return ec.fieldContext_OrgLink_author(ctx, field)
			case "orgSlug":
				return ec.fieldContext_OrgLink_orgSlug(ctx, field)
			case "createdOn":
				return ec.fieldContext_OrgLink_createdOn(ctx, field)
			case "updatedOn":
				return ec.fieldContext_OrgLink_updatedOn(ctx, field)
			case "baseUrlData":
				return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
			case "baseUrlCounter":
				return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
			case "baseUrlHash":
				return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
		},
	}
	return fc, nil
}

func (ec *executionContext) _BookmarkCursor_title(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_BookmarkCursor_title(ctx, field)
	if err != nil {
		return graphql.Null
	}
	ctx = graphql.WithFieldContext(ctx, fc)
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.Title, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(string)
	fc.Result = res
	return ec.marshalNString2string(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_BookmarkCursor_title(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "BookmarkCursor",
		Field:      field,
		IsMethod:   false,
		IsResolver: false,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			return nil, errors.New("field of type String does not have child fields")
		},
	}
	return fc, nil
}

func (ec *executionContext) _BookmarkCursor_description(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_BookmarkCursor_description(ctx, field)
	if err != nil {
		return graphql.Null
	}
	ctx = graphql.WithFieldContext(ctx, fc)
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.Description, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(string)
	fc.Result = res
	return ec.marshalNString2string(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_BookmarkCursor_description(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "BookmarkCursor",
		Field:      field,
		IsMethod:   false,
		IsResolver: false,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			return nil, errors.New("field of type String does not have child fields")
		},
	}
	return fc, nil
}

func (ec *executionContext) _BookmarkCursor_url(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_BookmarkCursor_url(ctx, field)
	if err != nil {
		return graphql.Null
	}
	ctx = graphql.WithFieldContext(ctx, fc)
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.URL, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(string)
	fc.Result = res
	return ec.marshalNString2string(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_BookmarkCursor_url(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "BookmarkCursor",
		Field:      field,
		IsMethod:   false,
		IsResolver: false,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			return nil, errors.New("field of type String does not have child fields")
		},
	}
	return fc, nil
}

func (ec *executionContext) _BookmarkCursor_count(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_BookmarkCursor_count(ctx, field)
	if err != nil {
		return graphql.Null
	}
	ctx = graphql.WithFieldContext(ctx, fc)
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.Count, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(int)
	fc.Result = res
	return ec.marshalNInt2int(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_BookmarkCursor_count(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "BookmarkCursor",
		Field:      field,
		IsMethod:   false,
		IsResolver: false,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			return nil, errors.New("field of type Int does not have child fields")
		},
	}
	return fc, nil
}

func (ec *executionContext) _BookmarkCursor_tagCloud(ctx context.Context, field graphql.CollectedField, obj *model.BookmarkCursor) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_BookmarkCursor_tagCloud(ctx, field)
	if err != nil {
		return graphql.Null
	}
	ctx = graphql.WithFieldContext(ctx, fc)
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.TagCloud, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		return graphql.Null
	}
	res := resTmp.([]*models.Tag)
	fc.Result = res
	return ec.marshalOTag2ᚕᚖlinksᚋmodelsᚐTag(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_BookmarkCursor_tagCloud(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "BookmarkCursor",
		Field:      field,
		IsMethod:   false,
		IsResolver: false,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			switch field.Name {
			case "id":
				return ec.fieldContext_Tag_id(ctx, field)
			case "name":
				return ec.fieldContext_Tag_name(ctx, field)
			case "slug":
				return ec.fieldContext_Tag_slug(ctx, field)
			case "createdOn":
				return ec.fieldContext_Tag_createdOn(ctx, field)
			case "count":
				return ec.fieldContext_Tag_count(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type Tag", field.Name)
		},
	}
	return fc, nil
}

func (ec *executionContext) _DeletePayload_success(ctx context.Context, field graphql.CollectedField, obj *model.DeletePayload) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_DeletePayload_success(ctx, field)
	if err != nil {
@@ -11132,6 +11589,10 @@ func (ec *executionContext) fieldContext_Mutation_addLink(ctx context.Context, f
				return ec.fieldContext_OrgLink_updatedOn(ctx, field)
			case "baseUrlData":
				return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
			case "baseUrlCounter":
				return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
			case "baseUrlHash":
				return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
		},
@@ -11259,6 +11720,10 @@ func (ec *executionContext) fieldContext_Mutation_updateLink(ctx context.Context
				return ec.fieldContext_OrgLink_updatedOn(ctx, field)
			case "baseUrlData":
				return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
			case "baseUrlCounter":
				return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
			case "baseUrlHash":
				return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
		},
@@ -11481,6 +11946,10 @@ func (ec *executionContext) fieldContext_Mutation_addNote(ctx context.Context, f
				return ec.fieldContext_OrgLink_updatedOn(ctx, field)
			case "baseUrlData":
				return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
			case "baseUrlCounter":
				return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
			case "baseUrlHash":
				return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
		},
@@ -15358,6 +15827,94 @@ func (ec *executionContext) fieldContext_OrgLink_baseUrlData(_ context.Context,
	return fc, nil
}

func (ec *executionContext) _OrgLink_baseUrlCounter(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
	if err != nil {
		return graphql.Null
	}
	ctx = graphql.WithFieldContext(ctx, fc)
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.BaseURLCounter, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(int)
	fc.Result = res
	return ec.marshalNInt2int(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_OrgLink_baseUrlCounter(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "OrgLink",
		Field:      field,
		IsMethod:   false,
		IsResolver: false,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			return nil, errors.New("field of type Int does not have child fields")
		},
	}
	return fc, nil
}

func (ec *executionContext) _OrgLink_baseUrlHash(ctx context.Context, field graphql.CollectedField, obj *models.OrgLink) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
	if err != nil {
		return graphql.Null
	}
	ctx = graphql.WithFieldContext(ctx, fc)
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		ctx = rctx // use context from middleware stack in children
		return obj.BaseURLHash, nil
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(string)
	fc.Result = res
	return ec.marshalNString2string(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_OrgLink_baseUrlHash(_ context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "OrgLink",
		Field:      field,
		IsMethod:   false,
		IsResolver: false,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			return nil, errors.New("field of type String does not have child fields")
		},
	}
	return fc, nil
}

func (ec *executionContext) _OrgLinkCursor_result(ctx context.Context, field graphql.CollectedField, obj *model.OrgLinkCursor) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_OrgLinkCursor_result(ctx, field)
	if err != nil {
@@ -15435,6 +15992,10 @@ func (ec *executionContext) fieldContext_OrgLinkCursor_result(_ context.Context,
				return ec.fieldContext_OrgLink_updatedOn(ctx, field)
			case "baseUrlData":
				return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
			case "baseUrlCounter":
				return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
			case "baseUrlHash":
				return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
		},
@@ -18913,6 +19474,10 @@ func (ec *executionContext) fieldContext_Query_getOrgLink(ctx context.Context, f
				return ec.fieldContext_OrgLink_updatedOn(ctx, field)
			case "baseUrlData":
				return ec.fieldContext_OrgLink_baseUrlData(ctx, field)
			case "baseUrlCounter":
				return ec.fieldContext_OrgLink_baseUrlCounter(ctx, field)
			case "baseUrlHash":
				return ec.fieldContext_OrgLink_baseUrlHash(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type OrgLink", field.Name)
		},
@@ -18931,6 +19496,107 @@ func (ec *executionContext) fieldContext_Query_getOrgLink(ctx context.Context, f
	return fc, nil
}

func (ec *executionContext) _Query_getBookmarks(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_Query_getBookmarks(ctx, field)
	if err != nil {
		return graphql.Null
	}
	ctx = graphql.WithFieldContext(ctx, fc)
	defer func() {
		if r := recover(); r != nil {
			ec.Error(ctx, ec.Recover(ctx, r))
			ret = graphql.Null
		}
	}()
	resTmp, err := ec.ResolverMiddleware(ctx, func(rctx context.Context) (interface{}, error) {
		directive0 := func(rctx context.Context) (interface{}, error) {
			ctx = rctx // use context from middleware stack in children
			return ec.resolvers.Query().GetBookmarks(rctx, fc.Args["hash"].(string), fc.Args["tags"].(*string))
		}

		directive1 := func(ctx context.Context) (interface{}, error) {
			scope, err := ec.unmarshalNAccessScope2linksᚋapiᚋgraphᚋmodelᚐAccessScope(ctx, "LINKS")
			if err != nil {
				var zeroVal *model.BookmarkCursor
				return zeroVal, err
			}
			kind, err := ec.unmarshalNAccessKind2linksᚋapiᚋgraphᚋmodelᚐAccessKind(ctx, "RO")
			if err != nil {
				var zeroVal *model.BookmarkCursor
				return zeroVal, err
			}
			if ec.directives.Access == nil {
				var zeroVal *model.BookmarkCursor
				return zeroVal, errors.New("directive access is not implemented")
			}
			return ec.directives.Access(ctx, nil, directive0, scope, kind)
		}

		tmp, err := directive1(rctx)
		if err != nil {
			return nil, graphql.ErrorOnPath(ctx, err)
		}
		if tmp == nil {
			return nil, nil
		}
		if data, ok := tmp.(*model.BookmarkCursor); ok {
			return data, nil
		}
		return nil, fmt.Errorf(`unexpected type %T from directive, should be *links/api/graph/model.BookmarkCursor`, tmp)
	})
	if err != nil {
		ec.Error(ctx, err)
		return graphql.Null
	}
	if resTmp == nil {
		if !graphql.HasFieldError(ctx, fc) {
			ec.Errorf(ctx, "must not be null")
		}
		return graphql.Null
	}
	res := resTmp.(*model.BookmarkCursor)
	fc.Result = res
	return ec.marshalNBookmarkCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐBookmarkCursor(ctx, field.Selections, res)
}

func (ec *executionContext) fieldContext_Query_getBookmarks(ctx context.Context, field graphql.CollectedField) (fc *graphql.FieldContext, err error) {
	fc = &graphql.FieldContext{
		Object:     "Query",
		Field:      field,
		IsMethod:   true,
		IsResolver: true,
		Child: func(ctx context.Context, field graphql.CollectedField) (*graphql.FieldContext, error) {
			switch field.Name {
			case "result":
				return ec.fieldContext_BookmarkCursor_result(ctx, field)
			case "title":
				return ec.fieldContext_BookmarkCursor_title(ctx, field)
			case "description":
				return ec.fieldContext_BookmarkCursor_description(ctx, field)
			case "url":
				return ec.fieldContext_BookmarkCursor_url(ctx, field)
			case "count":
				return ec.fieldContext_BookmarkCursor_count(ctx, field)
			case "tagCloud":
				return ec.fieldContext_BookmarkCursor_tagCloud(ctx, field)
			}
			return nil, fmt.Errorf("no field named %q was found under type BookmarkCursor", field.Name)
		},
	}
	defer func() {
		if r := recover(); r != nil {
			err = ec.Recover(ctx, r)
			ec.Error(ctx, err)
		}
	}()
	ctx = graphql.WithFieldContext(ctx, fc)
	if fc.Args, err = ec.field_Query_getBookmarks_args(ctx, field.ArgumentMap(ec.Variables)); err != nil {
		ec.Error(ctx, err)
		return fc, err
	}
	return fc, nil
}

func (ec *executionContext) _Query_getOrgLinks(ctx context.Context, field graphql.CollectedField) (ret graphql.Marshaler) {
	fc, err := ec.fieldContext_Query_getOrgLinks(ctx, field)
	if err != nil {
@@ -26956,6 +27622,67 @@ func (ec *executionContext) _BillingSettings(ctx context.Context, sel ast.Select
	return out
}

var bookmarkCursorImplementors = []string{"BookmarkCursor"}

func (ec *executionContext) _BookmarkCursor(ctx context.Context, sel ast.SelectionSet, obj *model.BookmarkCursor) graphql.Marshaler {
	fields := graphql.CollectFields(ec.OperationContext, sel, bookmarkCursorImplementors)

	out := graphql.NewFieldSet(fields)
	deferred := make(map[string]*graphql.FieldSet)
	for i, field := range fields {
		switch field.Name {
		case "__typename":
			out.Values[i] = graphql.MarshalString("BookmarkCursor")
		case "result":
			out.Values[i] = ec._BookmarkCursor_result(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				out.Invalids++
			}
		case "title":
			out.Values[i] = ec._BookmarkCursor_title(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				out.Invalids++
			}
		case "description":
			out.Values[i] = ec._BookmarkCursor_description(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				out.Invalids++
			}
		case "url":
			out.Values[i] = ec._BookmarkCursor_url(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				out.Invalids++
			}
		case "count":
			out.Values[i] = ec._BookmarkCursor_count(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				out.Invalids++
			}
		case "tagCloud":
			out.Values[i] = ec._BookmarkCursor_tagCloud(ctx, field, obj)
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}
	}
	out.Dispatch(ctx)
	if out.Invalids > 0 {
		return graphql.Null
	}

	atomic.AddInt32(&ec.deferred, int32(len(deferred)))

	for label, dfs := range deferred {
		ec.processDeferredGroup(graphql.DeferredGroup{
			Label:    label,
			Path:     graphql.GetPath(ctx),
			FieldSet: dfs,
			Context:  ctx,
		})
	}

	return out
}

var deletePayloadImplementors = []string{"DeletePayload"}

func (ec *executionContext) _DeletePayload(ctx context.Context, sel ast.SelectionSet, obj *model.DeletePayload) graphql.Marshaler {
@@ -28339,6 +29066,16 @@ func (ec *executionContext) _OrgLink(ctx context.Context, sel ast.SelectionSet,
			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&out.Invalids, 1)
			}
		case "baseUrlCounter":
			out.Values[i] = ec._OrgLink_baseUrlCounter(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&out.Invalids, 1)
			}
		case "baseUrlHash":
			out.Values[i] = ec._OrgLink_baseUrlHash(ctx, field, obj)
			if out.Values[i] == graphql.Null {
				atomic.AddUint32(&out.Invalids, 1)
			}
		default:
			panic("unknown field " + strconv.Quote(field.Name))
		}
@@ -29272,6 +30009,28 @@ func (ec *executionContext) _Query(ctx context.Context, sel ast.SelectionSet) gr
					func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
			}

			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
		case "getBookmarks":
			field := field

			innerFunc := func(ctx context.Context, fs *graphql.FieldSet) (res graphql.Marshaler) {
				defer func() {
					if r := recover(); r != nil {
						ec.Error(ctx, ec.Recover(ctx, r))
					}
				}()
				res = ec._Query_getBookmarks(ctx, field)
				if res == graphql.Null {
					atomic.AddUint32(&fs.Invalids, 1)
				}
				return res
			}

			rrm := func(ctx context.Context) graphql.Marshaler {
				return ec.OperationContext.RootResolverMiddleware(ctx,
					func(ctx context.Context) graphql.Marshaler { return innerFunc(ctx, out) })
			}

			out.Concurrently(i, func(ctx context.Context) graphql.Marshaler { return rrm(innerCtx) })
		case "getOrgLinks":
			field := field
@@ -30636,6 +31395,20 @@ func (ec *executionContext) marshalNBaseURLData2linksᚋmodelsᚐBaseURLData(ctx
	return ec._BaseURLData(ctx, sel, &v)
}

func (ec *executionContext) marshalNBookmarkCursor2linksᚋapiᚋgraphᚋmodelᚐBookmarkCursor(ctx context.Context, sel ast.SelectionSet, v model.BookmarkCursor) graphql.Marshaler {
	return ec._BookmarkCursor(ctx, sel, &v)
}

func (ec *executionContext) marshalNBookmarkCursor2ᚖlinksᚋapiᚋgraphᚋmodelᚐBookmarkCursor(ctx context.Context, sel ast.SelectionSet, v *model.BookmarkCursor) graphql.Marshaler {
	if v == nil {
		if !graphql.HasFieldError(ctx, graphql.GetFieldContext(ctx)) {
			ec.Errorf(ctx, "the requested element is null which the schema does not allow")
		}
		return graphql.Null
	}
	return ec._BookmarkCursor(ctx, sel, v)
}

func (ec *executionContext) unmarshalNBoolean2bool(ctx context.Context, v interface{}) (bool, error) {
	res, err := graphql.UnmarshalBoolean(v)
	return res, graphql.ErrorOnPath(ctx, err)
diff --git a/api/graph/model/models_gen.go b/api/graph/model/models_gen.go
index 2404e55..5d3bc9f 100644
--- a/api/graph/model/models_gen.go
+++ b/api/graph/model/models_gen.go
@@ -120,6 +120,15 @@ type AuditLogInput struct {
	Limit     *int    `json:"limit,omitempty"`
}

type BookmarkCursor struct {
	Result      []*models.OrgLink `json:"result"`
	Title       string            `json:"title"`
	Description string            `json:"description"`
	URL         string            `json:"url"`
	Count       int               `json:"count"`
	TagCloud    []*models.Tag     `json:"tagCloud,omitempty"`
}

type CompleteRegisterInput struct {
	Name     string `json:"name"`
	Username string `json:"username"`
diff --git a/api/graph/schema.graphqls b/api/graph/schema.graphqls
index 5987562..e170c97 100644
--- a/api/graph/schema.graphqls
+++ b/api/graph/schema.graphqls
@@ -244,6 +244,8 @@ type OrgLink {
    createdOn: Time!
    updatedOn: Time!
    baseUrlData: BaseURLData!
    baseUrlCounter: Int!
    baseUrlHash: String!
}

type LinkShort {
@@ -373,6 +375,15 @@ type UserCursor {
    pageInfo: PageInfo
}

type BookmarkCursor {
    result: [OrgLink]!
    title: String!
    description: String!
    url: String!
    count: Int!
    tagCloud: [Tag]
}

type OrgLinkCursor {
    result: [OrgLink]!
    pageInfo: PageInfo
@@ -805,6 +816,9 @@ type Query {
    "Returns a specific organization link"
    getOrgLink(hash: String!): OrgLink @access(scope: LINKS, kind: RO)

    "Returns saved links (bookmarks) for a given URL. Accepts BaseURL hash or full url"
    getBookmarks(hash: String!, tags: String): BookmarkCursor! @access(scope: LINKS, kind: RO)

    "Returns an array of organization links"
    getOrgLinks(input: GetLinkInput): OrgLinkCursor! @access(scope: LINKS, kind: RO)

diff --git a/api/graph/schema.resolvers.go b/api/graph/schema.resolvers.go
index bfab68a..ec38373 100644
--- a/api/graph/schema.resolvers.go
+++ b/api/graph/schema.resolvers.go
@@ -4778,7 +4778,7 @@ func (r *queryResolver) Version(ctx context.Context) (*model.Version, error) {
	return &model.Version{
		Major:           0,
		Minor:           3,
		Patch:           0,
		Patch:           1,
		DeprecationDate: nil,
	}, nil
}
@@ -5103,6 +5103,117 @@ func (r *queryResolver) GetOrgLink(ctx context.Context, hash string) (*models.Or
	return orgLinks[0], nil
}

// GetBookmarks is the resolver for the getBookmarks field.
func (r *queryResolver) GetBookmarks(ctx context.Context, hash string, tags *string) (*model.BookmarkCursor, error) {
	tokenUser := oauth2.ForContext(ctx)
	if tokenUser == nil {
		return nil, valid.ErrAuthorization
	}
	user := tokenUser.User.(*models.User)
	lang := links.GetLangFromRequest(server.EchoForContext(ctx).Request(), user)
	lt := localizer.GetLocalizer(lang)

	ctx = timezone.Context(ctx, links.GetUserTZ(user))
	validator := valid.New(ctx)

	opts := &database.FilterOptions{
		Filter: sq.Eq{"b.public_ready": true},
		Limit:  1,
	}
	_, err := url.Parse(hash)
	if err != nil {
		opts.Filter = sq.And{
			opts.Filter,
			sq.Eq{"b.url": links.StripURLFragment(hash)},
		}
	} else {
		opts.Filter = sq.And{
			opts.Filter,
			sq.Eq{"b.hash": hash},
		}
	}

	burls, err := models.GetBaseURLs(ctx, opts)
	if err != nil {
		return nil, err
	}
	if len(burls) == 0 {
		validator.Error("%s", lt.Translate("BaseURL Not Found")).
			WithCode(valid.ErrNotFoundCode)
		return nil, nil
	}
	burl := burls[0]

	cloudOrder := model.CloudOrderTypeNameAsc

	opts = &database.FilterOptions{
		Filter:  sq.Eq{"ol.base_url_id": burl.ID},
		OrderBy: "ol.id DESC",
		Limit:   100,
	}

	if tags != nil && *tags != "" {
		f := links.NewTagQuery("tag_links", "org_link_id")
		subQText, subQVal, err := f.GetSubQuery(tags, nil)
		if err != nil {
			return nil, err
		}
		opts.Filter = sq.And{
			opts.Filter,
			sq.Expr("ol.id IN ("+subQText+")", subQVal...),
		}
	}

	if !user.IsAuthenticated() {
		opts.Filter = sq.And{
			opts.Filter,
			sq.Eq{"ol.visibility": models.OrgLinkVisibilityPublic},
		}
	} else {
		// User can see public or all links belonging to an org they have at least
		// read access to.
		opts.Filter = sq.And{
			opts.Filter,
			sq.Or{
				// link is public
				sq.Eq{"ol.visibility": models.OrgLinkVisibilityPublic},
				// current user is owner of link
				sq.Eq{"ol.user_id": user.ID},
				// current user is owner of link organization
				sq.And{
					sq.Eq{"o.is_active": true},
					sq.Eq{"o.owner_id": user.ID},
				},
				// current user has read permission of link organization
				sq.And{
					sq.Eq{"ou.user_id": user.ID},
					sq.Eq{"o.is_active": true},
					sq.Eq{"(o.settings->'billing'->>'status')": models.BillingStatusBusiness},
				},
			},
		}
	}

	orgLinks, err := models.GetOrgLinks(ctx, opts)
	if err != nil {
		return nil, err
	}

	ltags, err := models.LinkTagCloud(ctx, orgLinks, models.TagCloudOrdering(cloudOrder))
	if err != nil {
		return nil, err
	}

	return &model.BookmarkCursor{
		Result:      orgLinks,
		TagCloud:    ltags,
		Title:       burl.Title,
		Description: burl.Data.Meta.Description,
		Count:       burl.Counter,
		URL:         burl.URL,
	}, nil
}

// GetOrgLinks is the resolver for the getOrgLinks field.
func (r *queryResolver) GetOrgLinks(ctx context.Context, input *model.GetLinkInput) (*model.OrgLinkCursor, error) {
	if input == nil {
diff --git a/cmd/links/main.go b/cmd/links/main.go
index 9698153..2e26c81 100644
--- a/cmd/links/main.go
+++ b/cmd/links/main.go
@@ -307,6 +307,9 @@ func run() error {
				return "tag-normal"
			}
		},
		"showCounter": func(obj any) bool {
			return models.ShowLinkCounter(obj)
		},
	})
	srv.TemplateAllowOverride(
		"seoData",
diff --git a/cmd/test/helpers.go b/cmd/test/helpers.go
index 967f17e..1396162 100644
--- a/cmd/test/helpers.go
+++ b/cmd/test/helpers.go
@@ -144,6 +144,9 @@ func NewWebTestServer(t *testing.T) (*server.Server, *echo.Echo) {
				return "tag-normal"
			}
		},
		"showCounter": func(obj any) bool {
			return models.ShowLinkCounter(obj)
		},
	})
	err = srv.LoadTemplatesFS(links.TemplateFS, "templates/*.html", "templates/*.txt")
	if err != nil {
diff --git a/core/routes.go b/core/routes.go
index ce9f659..54fa248 100644
--- a/core/routes.go
+++ b/core/routes.go
@@ -57,6 +57,7 @@ func (s *Service) RegisterRoutes() {
	s.Group.GET("/link/:hash", s.OrgLinkDetail).Name = s.RouteName("link_detail")
	s.Group.GET("/click/:hash", s.OrgLinkRedirect).Name = s.RouteName("link_redirect")
	s.Group.GET("/tag-autocomplete", s.TagAutocomplete).Name = s.RouteName("tag_autocomplete")
	s.Group.GET("/bookmarks/:hash", s.GetBookmarks).Name = s.RouteName("bookmarks")

	s.Group.Use(auth.AuthRequired())
	s.Group.GET("/:slug/follow-toggle/:action", s.FollowToggle).Name = s.RouteName("follow-toggle")
@@ -1790,6 +1791,8 @@ func (s *Service) UserFeed(c echo.Context) error {
						siteName
					}
				}
				baseUrlCounter
				baseUrlHash
			}
			pageInfo {
				cursor
@@ -1918,6 +1921,121 @@ func (s *Service) UserFeed(c echo.Context) error {
	return s.Render(c, http.StatusOK, "feed.html", gmap)
}

// GetBookmarks will show all bookmarks for a specific BaseURL
func (s *Service) GetBookmarks(c echo.Context) error {
	lhash := c.Param("hash")
	if lhash == "" {
		return echo.NotFoundHandler(c)
	}

	err := links.TagAbuseRedirect(c)
	if err != nil {
		return err
	}

	type GraphQLResponse struct {
		OrgLinks struct {
			Result      []models.OrgLink `json:"result"`
			Title       string           `json:"title"`
			Description string           `json:"description"`
			URL         string           `json:"url"`
			Count       int              `json:"count"`
			TagCloud    []models.Tag     `json:"tagCloud"`
		} `json:"getBookmarks"`
	}

	var result GraphQLResponse
	op := gqlclient.NewOperation(
		`query GetBookmarks($hash: String!, $tags: String) {
			getBookmarks(hash: $hash, tags: $tags) {
				result {
					id
					title
					url
					visibility
					description
					userId
					createdOn
					author
					orgSlug
					unread
					starred
					archiveUrl
					type
					hash
					tags {
						id
						name
						slug
					}
				}
				title
				description
				url
				count
				tagCloud {
					name
					slug
					count
				}
			}
		}`)
	op.Var("hash", lhash)

	var tag string
	queries := make(url.Values)
	if c.QueryParam("tag") != "" {
		tag = c.QueryParam("tag")
		op.Var("tags", tag)
		queries.Add("tag", tag)
	}

	err = links.Execute(links.LangContext(c), op, &result)
	if err != nil {
		if graphError, ok := err.(*gqlclient.Error); ok {
			err = links.ParseInputErrors(c, graphError, gobwebs.Map{})
		}
		return err
	}

	lt := localizer.GetSessionLocalizer(c)
	pd := localizer.NewPageData(lt.Translate("Saved Bookmarks"))
	pd.Data["tags"] = lt.Translate("Tags")
	pd.Data["no_links"] = lt.Translate("This feed has no links. Booo!")
	orgLinks := result.OrgLinks.Result

	seoData := links.GetSEOData(c)
	url := links.GetLinksDomainURL(c)
	url.Path = c.Echo().Reverse(s.RouteName("bookmarks"))
	seoData.Description = lt.Translate(
		"Recent bookmarks for URL %s added on LinkTaco.com",
		result.OrgLinks.URL,
	)
	seoData.Title = pd.Title
	seoData.Keywords = lt.Translate("recent, public, links, linktaco")
	seoData.URL = url.String()
	seoData.TwitterURL = url.String()

	navFlag := "bookmarks"
	gmap := gobwebs.Map{
		"pd":            pd,
		"seoData":       seoData,
		"links":         orgLinks,
		"navFlag":       navFlag,
		"currURL":       seoData.URL,
		"tagCloud":      result.OrgLinks.TagCloud,
		"pTitle":        result.OrgLinks.Title,
		"pDescription":  result.OrgLinks.Description,
		"pURL":          result.OrgLinks.URL,
		"pCount":        result.OrgLinks.Count,
		"queries":       template.URL(queries.Encode()),
		"tagFilter":     strings.Replace(tag, ",", ", ", -1),
		"hideSaveCount": true,
	}

	return s.Render(c, http.StatusOK, "link_list.html", gmap)
}

// OrgLinksList ...
func (s *Service) OrgLinksList(c echo.Context) error {
	gctx := c.(*server.Context)
@@ -1984,6 +2102,8 @@ func (s *Service) OrgLinksList(c echo.Context) error {
								siteName
							}
						}
						baseUrlCounter
						baseUrlHash
					}
				pageInfo {
					cursor
diff --git a/models/models.go b/models/models.go
index d343917..f38a5f2 100644
--- a/models/models.go
+++ b/models/models.go
@@ -87,11 +87,13 @@ type OrgLink struct {
	CreatedOn   time.Time `db:"created_on" json:"createdOn"`
	UpdatedOn   time.Time `db:"updated_on" json:"updatedOn"`

	OrgSlug     string        `db:"-" json:"orgSlug"`
	BaseURLData BaseURLData   `db:"-" json:"baseUrlData"`
	Tags        []Tag         `db:"-" json:"tags"`
	Clicks      sql.NullInt64 `db:"-" json:"-"`
	Author      string        `db:"-"`
	OrgSlug        string        `db:"-" json:"orgSlug"`
	BaseURLData    BaseURLData   `db:"-" json:"baseUrlData"`
	BaseURLCounter int           `db:"-" json:"baseUrlCounter"`
	BaseURLHash    string        `db:"-" json:"baseUrlHash"`
	Tags           []Tag         `db:"-" json:"tags"`
	Clicks         sql.NullInt64 `db:"-" json:"-"`
	Author         string        `db:"-"`
}

// OrgUser ...
diff --git a/models/org_link.go b/models/org_link.go
index 39238b1..5fa11e1 100644
--- a/models/org_link.go
+++ b/models/org_link.go
@@ -38,7 +38,7 @@ 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", "json_agg(t)::jsonb", "b.data").
				"ol.created_on", "ol.updated_on", "o.slug", "u.full_name", "json_agg(t)::jsonb", "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").
@@ -47,7 +47,7 @@ func GetOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink,
			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").
			GroupBy("ol.id", "o.slug", "u.full_name", "b.data", "b.counter", "b.hash").
			Distinct().
			PlaceholderFormat(sq.Dollar).
			RunWith(tx).
@@ -65,7 +65,8 @@ func GetOrgLinks(ctx context.Context, opts *database.FilterOptions) ([]*OrgLink,
			var tags string
			if err = rows.Scan(&o.ID, &o.Title, &o.URL, &o.Description, &o.BaseURLID, &o.OrgID,
				&o.UserID, &o.Visibility, &o.Unread, &o.Starred, &o.ArchiveURL, &o.Type, &o.Hash,
				&o.CreatedOn, &o.UpdatedOn, &o.OrgSlug, &o.Author, &tags, &o.BaseURLData); err != nil {
				&o.CreatedOn, &o.UpdatedOn, &o.OrgSlug, &o.Author, &tags,
				&o.BaseURLData, &o.BaseURLCounter, &o.BaseURLHash); err != nil {
				return err
			}
			re := regexp.MustCompile(`(,\s)?null,?`)
diff --git a/models/utils.go b/models/utils.go
index be59bc5..7a45864 100644
--- a/models/utils.go
+++ b/models/utils.go
@@ -19,7 +19,7 @@ func getShortCode(ctx context.Context, tx *sql.Tx, table, field string, filter s
	counter := 0

	for {
		for i := 0; i < codelen; i++ {
		for i := range codelen {
			code[i] = chars[r.Intn(len(chars))]
		}

@@ -81,3 +81,17 @@ func TagsToString(t []Tag) string {
	}
	return tags
}

// ShowLinkCounter says whether or not to show a link counter
func ShowLinkCounter(obj any) bool {
	switch v := any(obj).(type) {
	case *BaseURL, BaseURL:
		return true
	case *OrgLink:
		return v.BaseURLCounter > 0
	case OrgLink:
		return v.BaseURLCounter > 0
	default:
		return false
	}
}
diff --git a/templates/feed.html b/templates/feed.html
index 279245d..9c6aa7a 100644
--- a/templates/feed.html
+++ b/templates/feed.html
@@ -53,6 +53,7 @@
        <div class="d-flex items-center">
          <h3 class="is-marginless mr-1">
            <a class="text-dark" href="{{.URL}}" target="_blank">{{truncate .Title 60}}</a>
	    {{ if showCounter . }}<a href="{{ reverse "core:bookmarks" .BaseURLHash }}" class="button outline primary tag tag-popular is-small">{{if $.isPopular}}{{ .Counter }}{{ else }}{{ .BaseURLCounter }}{{ end }}</a>{{ end }}
          </h3>
        </div>
        {{if .Description}}
diff --git a/templates/link_list.html b/templates/link_list.html
index fae218b..cf9ff5c 100644
--- a/templates/link_list.html
+++ b/templates/link_list.html
@@ -1,5 +1,6 @@
{{template "base" .}}
{{ define "title" }}{{ .title }}{{ end }}
{{ if ne .navFlag "bookmarks" }}
<form method="GET"
    action="{{ if .isUserDomain }}/{{ else }}{{if .isPopular}}{{reverse "core:popular_link_list"}}{{else if .isOrgLink}}{{.currURL}}{{else}}{{reverse "core:recent_link_list"}}{{end}}{{ end }}{{if .hasUnreadFilter}}?filter=unread{{else if .hasStarredFilter}}?filter=starred{{end}}"
    id="advanced-search-form"
@@ -41,6 +42,7 @@
    </section>
    {{end}}
</form>
{{ end }}
<section class="card shadow-card container">
  <div class="row">
      <div class="col-9">
@@ -73,6 +75,20 @@
    </p>
    {{ end }}
  {{end}}
  {{ if eq .navFlag "bookmarks" }}
  <div class="row">
    <h3>{{ .pTitle }}
    <div class="button outline primary tag tag-popular is-small">{{ .pCount }}</div>
    </h3>
  </div>
  <div class="row">
    <p class="is-marginless">
    {{ if .pDescription }}<p>{{ htmlSafe (truncate .pDescription 200) }}</p>{{ end }}
    <small>{{ .pURL }}</small>
    </p>
  </div>
  <hr>
  {{ end }}
  <ul class="list-unstyled pl-0">
    {{range .links}}
    <li class="mb-1">
@@ -99,9 +115,7 @@
          {{end}}
          <h3 class="is-marginless mr-1">
            <a class="text-dark" href="{{.URL}}" target="_blank">{{truncate .Title 60}}</a>
            {{if $.isPopular}}
	      <span class="button outline primary tag tag-popular is-small">{{ .Counter }}</span>
            {{end}}
	      {{ if not $.hideSaveCount }}{{ if showCounter . }}<a href="{{ if $.isPopular }}{{ reverse "core:bookmarks" .Hash }}{{ else }}{{ reverse "core:bookmarks" .BaseURLHash }}{{ end }}" class="button outline primary tag tag-popular is-small">{{if $.isPopular}}{{ .Counter }}{{ else }}{{ .BaseURLCounter }}{{ end }}</a>{{ end }}{{ end }}
          </h3>
          {{if $.isOrgLink}}
            {{if eq .UserID $.currentUserID}}
diff --git a/values.go b/values.go
index 73fca25..4d6ce69 100644
--- a/values.go
+++ b/values.go
@@ -95,6 +95,7 @@ var InvalidSlugs = []string{
	"services",
	"integrations",
	"xmpp",
	"bookmarks",
}

// Interval used to display data in analytics engagement chart
-- 
2.47.2
Applied.

To git@git.code.netlandish.com:~netlandish/links
   25bdb98..6d9a153  master -> master