Skip to content

Add published collections: public directory, homepage discovery, password bypass - #26

Merged
ebulgakov merged 2 commits into
mainfrom
worktree-published-collections
Aug 1, 2026
Merged

Add published collections: public directory, homepage discovery, password bypass#26
ebulgakov merged 2 commits into
mainfrom
worktree-published-collections

Conversation

@ebulgakov

@ebulgakov ebulgakov commented Aug 1, 2026

Copy link
Copy Markdown
Owner

Summary

  • Adds published (boolean) and imageUrl fields to collections. published is independent of shared: public access gate becomes shared OR published everywhere a collection's reachability is checked, and password protection is bypassed whenever a collection is published (hasPassword = password !== null && !published).
  • New public directory page /published-collections listing all published collections with fuse.js-powered search over name/description; new public listing endpoint GET /api/shared.
  • Guest homepage now samples 5 random published collections (SSR/client hydration-safe via useState) with a link to the full directory; nav link added to the app header.
  • Collection form gains a Published toggle and Image URL field; the password field's visibility is now tri-state (hidden by default → shown once Shared is on → hidden again once Published is on), and the stored password is preserved (not cleared) across toggles.
  • Owner-facing collection page and list now also show the share link / a "Published" badge for published-only (not necessarily shared) collections.

Test plan

  • pnpm type-check, pnpm lint, pnpm test all green (171/171 tests, 15 new)
  • Manual: toggled Shared → Published → back on a real collection in dev; confirmed password field hides/reappears with value preserved
  • Manual: confirmed /shared/:slug bypasses a stored password once published=true, and re-enforces it once unpublished
  • Manual: /published-collections lists published collections, search filters by name and by description, empty-state renders for no matches
  • Manual: guest homepage shows the random sample + "view all" link; hard-reloaded multiple times, no hydration-mismatch console warning
  • Migration 0002_needy_calypso has been applied to the shared dev database (additive, defaults to false/NULL, safe against main)

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added published collections with optional images and public sharing.
    • Added a published collections page with search, responsive cards, and loading or empty states.
    • Added homepage previews and header navigation for published collections.
    • Added publication controls, image URL support, validation, and published status badges.
    • Published collections no longer require passwords.
  • Bug Fixes

    • Updated sharing warnings when changing the slug of a published collection.
  • Tests

    • Expanded coverage for publication settings, password visibility, search, and random collection selection.

…word bypass

Collections can now be marked published independently of shared, making
them listable on a new /published-collections search page and sampled on
the guest homepage, with password protection bypassed while published.
The collection form's password field now toggles visibility based on
shared/published state, and owners can see the share link and a
"Published" badge for published-only collections too.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@vercel

vercel Bot commented Aug 1, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
linkfolio Ready Ready Preview Aug 1, 2026 6:08am

@coderabbitai

coderabbitai Bot commented Aug 1, 2026

Copy link
Copy Markdown

Review Change Stack

Warning

Review limit reached

@ebulgakov, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 39 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.
You're only billed for reviews past your plan's rate limits ($0.25/file).

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 631c1f04-9557-405c-b851-23d38f60b48d

📥 Commits

Reviewing files that changed from the base of the PR and between 95b8994 and c63dfc2.

📒 Files selected for processing (5)
  • app/features/published-collections/index.ts
  • app/features/published-collections/model/use-homepage-published-collections.ts
  • app/features/published-collections/model/use-random-published-collections.ts
  • app/pages/index.vue
  • server/api/collections/[id].patch.ts
📝 Walkthrough

Walkthrough

This change adds published collections with database fields, validated CRUD support, public access rules, listing and search APIs, random selection, collection cards, form controls, homepage integration, and localized navigation.

Changes

Published collections

Layer / File(s) Summary
Publication data and write API
app/shared/api/collections.ts, app/shared/api/shared-collections.ts, server/db/..., server/api/collections/...
Collection contracts and database records now include published and nullable imageUrl fields. Create and update endpoints validate and persist both fields.
Collection form and public access rules
app/features/collection-form/..., app/pages/collections/[id]/index.vue, server/utils/shared-collection-links.ts, server/api/shared/[slug]...
The form manages publication, image URLs, and conditional password visibility. Published collections are accepted by shared routes and bypass password enforcement.
Published collection API and models
server/api/shared/index.get.ts, app/shared/api/..., app/features/published-collections/...
A public listing API and client composables support loading, fuzzy search, random selection, and card rendering. Tests cover search, selection limits, uniqueness, caching, and form behavior.
Published collection pages and navigation
app/pages/index.vue, app/pages/published-collections.vue, app/ui/app-header/app-header.vue, app/features/collections/ui/collections-list.vue, i18n/locales/*, package.json
Guests see random published collections on the homepage. A dedicated searchable page, header navigation, published badges, translations, and the Fuse.js dependency were added.

Estimated code review effort: 4 (Complex) | ~60 minutes

Possibly related PRs

  • ebulgakov/linkfolio#15: Introduced the collection schema and migration extended here with publication and image fields.
  • ebulgakov/linkfolio#16: Introduced the collection form, API types, and CRUD endpoints extended here.
  • ebulgakov/linkfolio#22: Introduced the shared-collection APIs and UI extended here for published collections.

Sequence Diagram(s)

sequenceDiagram
  participant Guest
  participant Homepage
  participant usePublishedCollections
  participant PublishedAPI
  participant Database
  Guest->>Homepage: Open homepage
  Homepage->>usePublishedCollections: Load published collections
  usePublishedCollections->>PublishedAPI: GET /api/shared
  PublishedAPI->>Database: Query published collections
  Database-->>PublishedAPI: Return public metadata
  PublishedAPI-->>usePublishedCollections: Return collection list
  usePublishedCollections-->>Homepage: Select and render random cards
Loading
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 20.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly summarizes the main changes: published collection discovery, homepage integration, and password bypass behavior.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch worktree-published-collections

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@ebulgakov
ebulgakov marked this pull request as ready for review August 1, 2026 05:47

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 2

🧹 Nitpick comments (4)
server/db/migrations/0002_needy_calypso.sql (1)

1-2: 🗄️ Data Integrity & Integration | 🔵 Trivial

Apply this migration before deploying the new API contract.

The schema and API routes now read and write collections.published and collections.image_url. If 0002_needy_calypso is not applied in an environment, collection requests will fail with missing-column errors. Apply and verify the migration in shared development and every deployment target before enabling the new code.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/db/migrations/0002_needy_calypso.sql` around lines 1 - 2, Apply and
verify the 0002_needy_calypso migration, ensuring the collections table contains
the published and image_url columns, before deploying or enabling the API
changes in shared development and every deployment target.
app/shared/api/use-published-collections.ts (1)

1-1: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Import PublishedCollection through the shared API barrel.

Replace the relative import with ~/shared/api. This keeps shared API imports on the public segment boundary.

Proposed fix
-import type { PublishedCollection } from "./shared-collections";
+import type { PublishedCollection } from "~/shared/api";

As per coding guidelines, “Import shared modules through their segment barrels (~/shared/api, ~/shared/ui, or ~/shared/lib) rather than deep paths.”

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/shared/api/use-published-collections.ts` at line 1, Update the
PublishedCollection import in use-published-collections.ts to use the shared API
barrel at ~/shared/api instead of the relative shared-collections path,
preserving the existing type-only import.

Source: Coding guidelines

app/features/published-collections/model/use-random-published-collections.ts (1)

4-7: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick win

Replace the sort-based shuffle with Fisher-Yates.

shuffled.sort(() => Math.random() - 0.5) does not produce a uniform random shuffle. The bias depends on the sort algorithm's comparison pattern, so some items are more likely than others to land in the first count slots that slice selects. This affects which collections actually get shown, not just their order.

Use Fisher-Yates for a correct, unbiased shuffle.

♻️ Proposed fix: Fisher-Yates shuffle
 function pickRandom<T>(list: T[], count: number): T[] {
-  const shuffled = [...list].sort(() => Math.random() - 0.5);
+  const shuffled = [...list];
+  for (let i = shuffled.length - 1; i > 0; i--) {
+    const j = Math.floor(Math.random() * (i + 1));
+    [shuffled[i], shuffled[j]] = [shuffled[j], shuffled[i]];
+  }
   return shuffled.slice(0, count);
 }
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/features/published-collections/model/use-random-published-collections.ts`
around lines 4 - 7, Replace the sort-based shuffle in pickRandom with an
in-place Fisher-Yates shuffle on the copied list, selecting a random index from
each remaining range before swapping. Preserve the existing behavior of
returning up to count items from the shuffled copy without mutating the input
list.
app/pages/index.vue (1)

24-38: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick win

Move the guest-only fetch/pick logic into a feature composable.

This branch decides whether to fetch published collections, and computes the random pick, directly in the page. This is feature logic, not page composition.

Extract this into a composable in the published-collections feature model (for example useGuestHomepageCollections()), exported through the feature's index.ts. Have the page call that composable and bind its result in the template.

As per coding guidelines, "Keep route pages thin: compose exported feature components rather than containing feature logic."

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@app/pages/index.vue` around lines 24 - 38, Extract the guest-only fetch and
random selection from the page’s randomPublishedCollections block into a
published-collections feature composable such as useGuestHomepageCollections(),
preserving the awaited source loading and unauthenticated-only behavior. Export
the composable through the feature index.ts, then have the page call it and bind
the returned collections to the existing template rendering.

Source: Coding guidelines

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@server/api/collections/`[id].patch.ts:
- Around line 73-74: Update the partial update payload around
parsed.data.imageUrl so an omitted imageUrl remains undefined and does not
overwrite the stored value; preserve explicit null as a deliberate clear, and
only include the imageUrl column when the property is present.
- Around line 17-23: Align the PATCH contract across the validation schema,
CollectionInput, updateCollection, and the PATCH handler: allow partial payloads
without published, and ensure imageUrl is set to null only when the request
explicitly includes that field, preserving the existing value when omitted.

---

Nitpick comments:
In
`@app/features/published-collections/model/use-random-published-collections.ts`:
- Around line 4-7: Replace the sort-based shuffle in pickRandom with an in-place
Fisher-Yates shuffle on the copied list, selecting a random index from each
remaining range before swapping. Preserve the existing behavior of returning up
to count items from the shuffled copy without mutating the input list.

In `@app/pages/index.vue`:
- Around line 24-38: Extract the guest-only fetch and random selection from the
page’s randomPublishedCollections block into a published-collections feature
composable such as useGuestHomepageCollections(), preserving the awaited source
loading and unauthenticated-only behavior. Export the composable through the
feature index.ts, then have the page call it and bind the returned collections
to the existing template rendering.

In `@app/shared/api/use-published-collections.ts`:
- Line 1: Update the PublishedCollection import in use-published-collections.ts
to use the shared API barrel at ~/shared/api instead of the relative
shared-collections path, preserving the existing type-only import.

In `@server/db/migrations/0002_needy_calypso.sql`:
- Around line 1-2: Apply and verify the 0002_needy_calypso migration, ensuring
the collections table contains the published and image_url columns, before
deploying or enabling the API changes in shared development and every deployment
target.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 72dde8ca-8478-475c-a165-4d66deceabe9

📥 Commits

Reviewing files that changed from the base of the PR and between f646bbd and 95b8994.

⛔ Files ignored due to path filters (1)
  • pnpm-lock.yaml is excluded by !**/pnpm-lock.yaml
📒 Files selected for processing (33)
  • app/features/collection-form/model/__tests__/use-collection-form.test.ts
  • app/features/collection-form/model/use-collection-form.ts
  • app/features/collection-form/ui/collection-form.vue
  • app/features/collections/ui/collections-list.vue
  • app/features/published-collections/index.ts
  • app/features/published-collections/model/__tests__/use-published-collections-search.test.ts
  • app/features/published-collections/model/__tests__/use-random-published-collections.test.ts
  • app/features/published-collections/model/use-published-collections-search.ts
  • app/features/published-collections/model/use-random-published-collections.ts
  • app/features/published-collections/ui/published-collection-card.vue
  • app/pages/collections/[id]/index.vue
  • app/pages/index.vue
  • app/pages/published-collections.vue
  • app/shared/api/collections.ts
  • app/shared/api/index.ts
  • app/shared/api/shared-collections.ts
  • app/shared/api/use-published-collections.ts
  • app/shared/testing/mocks/use-state.ts
  • app/ui/app-header/app-header.vue
  • i18n/locales/en.json
  • i18n/locales/ru.json
  • package.json
  • server/api/collections/[id].patch.ts
  • server/api/collections/index.post.ts
  • server/api/shared/[slug].get.ts
  • server/api/shared/[slug]/links/index.get.ts
  • server/api/shared/[slug]/unlock.post.ts
  • server/api/shared/index.get.ts
  • server/db/migrations/0002_needy_calypso.sql
  • server/db/migrations/meta/0002_snapshot.json
  • server/db/migrations/meta/_journal.json
  • server/db/schema.ts
  • server/utils/shared-collection-links.ts

Comment on lines +17 to +23
password: z.string().trim().max(255).optional().nullable(),
published: z.boolean(),
imageUrl: z
.url({ protocol: /^https?$/ })
.max(2048)
.optional()
.nullable()

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

rg -n -C 5 'PATCH|/api/collections|published|imageUrl' \
  app server \
  --glob '*.ts' --glob '*.tsx' --glob '*.vue'

Repository: ebulgakov/linkfolio

Length of output: 50376


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '--- candidate files ---\n'
git ls-files | rg '(^app/shared/api/collections.ts$|^app/features/collections/|^app/shared/(components|testing)/|^server/api/collections/\[id\]\.patch\.ts$|^server/api/collections/index\.post\.ts$)' || true

printf '\n--- server PATCH ---\n'
cat -n server/api/collections/[id].patch.ts | sed -n '1,130p'

printf '\n--- collections api shared ---\n'
cat -n app/shared/api/collections.ts | sed -n '1,120p'

printf '\n--- likely collection form files ---\n'
git ls-files app/features/collections | sort

Repository: ebulgakov/linkfolio

Length of output: 248


🏁 Script executed:

#!/bin/bash
set -euo pipefail

printf '%s\n' '--- candidate files ---'
git ls-files | rg '(^app/shared/api/collections.ts$|^app/features/collections/|^app/shared/(components|testing)/|^server/api/collections/\[id\]\.patch\.ts$|^server/api/collections/index\.post\.ts$)' || true

printf '%s\n' ''
printf '%s\n' '--- server PATCH ---'
cat -n server/api/collections/[id].patch.ts | sed -n '1,140p'

printf '%s\n' ''
printf '%s\n' '--- collections api shared ---'
cat -n app/shared/api/collections.ts | sed -n '1,120p'

printf '%s\n' ''
printf '%s\n' '--- likely collection form files ---'
git ls-files app/features/collections | sort

Repository: ebulgakov/linkfolio

Length of output: 9265


Align PATCH validation with the update contract.

updateCollection() and the PATCH handler are documented/implemented as PATCH routes, but published is required in both CollectionInput and server/api/collections/[id].patch.ts. Partial payloads without published fail before update. Make imageUrl optional in PATCH validation and only null it when the field is sent; if published remains PATCH-required, make it required in CollectionInput too.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@server/api/collections/`[id].patch.ts around lines 17 - 23, Align the PATCH
contract across the validation schema, CollectionInput, updateCollection, and
the PATCH handler: allow partial payloads without published, and ensure imageUrl
is set to null only when the request explicitly includes that field, preserving
the existing value when omitted.

Comment thread server/api/collections/[id].patch.ts Outdated
- Preserve imageUrl on partial PATCH instead of clobbering it to null when
  omitted, matching the existing password preserve-if-omitted contract.
- Replace the biased sort-based shuffle in pickRandom with Fisher-Yates.
- Extract the guest homepage's published-collections fetch/pick into a
  useHomepagePublishedCollections() feature composable, keeping the page thin.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant