|
1 | 1 | // app/lib/utils/referrers.js |
2 | 2 |
|
3 | 3 | /** |
4 | | - * Handle chained referrer URLs for back navigation |
5 | | - * Allows deep linking while maintaining proper back navigation chains |
| 4 | + * Referrer Chain Navigation System |
| 5 | + * |
| 6 | + * This module handles multi-level back navigation by maintaining a chain of URLs |
| 7 | + * that grows as users navigate deeper and shrinks as they navigate back. |
| 8 | + * |
| 9 | + * ## Mental Model |
| 10 | + * |
| 11 | + * Think of the referrer chain as a breadcrumb trail: |
| 12 | + * - As you navigate TO deeper pages, add current URL to the chain |
| 13 | + * - As you navigate BACK, pop the last URL from the chain |
| 14 | + * - The chain is stored as a query parameter: ?referrerChain=/page1,/page2,/page3 |
| 15 | + * - In templates, `referrerChain` refers to the template variable (populated from query.referrerChain) |
| 16 | + * |
| 17 | + * ## The Three Navigation Patterns |
| 18 | + * |
| 19 | + * ### 1. Navigating TO a page (extending the chain) |
| 20 | + * Use when: Going deeper into a flow, user should be able to return to current page |
| 21 | + * |
| 22 | + * ```nunjucks |
| 23 | + * {# Extending an existing chain (most common in multi-level flows) #} |
| 24 | + * <a href="{{ '/next-page' | urlWithReferrer(referrerChain | appendReferrer(currentUrl)) }}"> |
| 25 | + * |
| 26 | + * {# Starting a new chain (no existing referrer) #} |
| 27 | + * <a href="{{ '/next-page' | urlWithReferrer(currentUrl) }}"> |
| 28 | + * ``` |
| 29 | + * |
| 30 | + * ### 2. Navigating BACK (consuming the chain) |
| 31 | + * Use when: On a "Continue" or "Save and return" button |
| 32 | + * |
| 33 | + * ```nunjucks |
| 34 | + * {# Extracts destination from chain, includes remaining chain #} |
| 35 | + * {{ button({ |
| 36 | + * text: "Continue", |
| 37 | + * href: '../fallback' | getReturnUrl(referrerChain) |
| 38 | + * }) }} |
| 39 | + * ``` |
| 40 | + * |
| 41 | + * ### 3. Building chains manually (advanced) |
| 42 | + * Use when: Pre-building chains for complex flows (e.g., "add" buttons that bypass review page) |
| 43 | + * |
| 44 | + * ```nunjucks |
| 45 | + * {# Build chain for add journey that returns through review page #} |
| 46 | + * {% set addReferrerChain = currentUrl | appendReferrer('/review') %} |
| 47 | + * <a href="{{ '/add' | urlWithReferrer(addReferrerChain) }}">Add item</a> |
| 48 | + * ``` |
| 49 | + * |
| 50 | + * ## Real-World Example: Check Information Flow |
| 51 | + * |
| 52 | + * Starting point: User is on `/clinics/123/events/456/check-information` |
| 53 | + * |
| 54 | + * 1. **check-information** → **confirm-information/medical-history** |
| 55 | + * ```nunjucks |
| 56 | + * {# Start new chain with current page #} |
| 57 | + * <a href="{{ './confirm-information/medical-history' | urlWithReferrer(currentUrl) }}"> |
| 58 | + * {# Result: ?referrerChain=/clinics/123/events/456/check-information #} |
| 59 | + * ``` |
| 60 | + * |
| 61 | + * 2. **confirm-information/medical-history** → **edit form** |
| 62 | + * ```nunjucks |
| 63 | + * {# Extend chain by appending current page to existing chain #} |
| 64 | + * <a href="{{ './edit/123' | urlWithReferrer(referrerChain | appendReferrer(currentUrl)) }}"> |
| 65 | + * {# Result: ?referrerChain=/check-information,/confirm-information/medical-history #} |
| 66 | + * ``` |
| 67 | + * |
| 68 | + * 3. **edit form** → **back to confirm-information/medical-history** |
| 69 | + * ```nunjucks |
| 70 | + * {# Navigate back: pops last URL from chain and goes there #} |
| 71 | + * {{ button({ href: '../fallback' | getReturnUrl(referrerChain) }) }} |
| 72 | + * {# Goes to: /confirm-information/medical-history?referrerChain=/check-information #} |
| 73 | + * ``` |
| 74 | + * |
| 75 | + * 4. **confirm-information/medical-history** → **back to check-information** |
| 76 | + * ```nunjucks |
| 77 | + * {# Navigate back again: pops last remaining URL #} |
| 78 | + * {{ button({ href: '../fallback' | getReturnUrl(referrerChain) }) }} |
| 79 | + * {# Goes to: /check-information (no chain param, it's exhausted) #} |
| 80 | + * ``` |
| 81 | + * |
| 82 | + * ## Common Mistakes to Avoid |
| 83 | + * |
| 84 | + * ❌ **Using urlWithReferrer for back links** |
| 85 | + * ```nunjucks |
| 86 | + * {# WRONG: Adds referrer instead of consuming it #} |
| 87 | + * <a href="{{ '../previous' | urlWithReferrer(referrerChain) }}">Back</a> |
| 88 | + * ``` |
| 89 | + * ✅ **Use getReturnUrl for back links** |
| 90 | + * ```nunjucks |
| 91 | + * {# CORRECT: Extracts destination from referrer chain #} |
| 92 | + * <a href="{{ '../previous' | getReturnUrl(referrerChain) }}">Back</a> |
| 93 | + * ``` |
| 94 | + * |
| 95 | + * ❌ **Forgetting to extend the chain** |
| 96 | + * ```nunjucks |
| 97 | + * {# WRONG: Replaces chain instead of extending it #} |
| 98 | + * <a href="{{ '/next' | urlWithReferrer(currentUrl) }}">Continue</a> |
| 99 | + * ``` |
| 100 | + * ✅ **Extend the chain when going deeper** |
| 101 | + * ```nunjucks |
| 102 | + * {# CORRECT: Extends existing chain with current URL #} |
| 103 | + * <a href="{{ '/next' | urlWithReferrer(referrerChain | appendReferrer(currentUrl)) }}"> |
| 104 | + * ``` |
| 105 | + * |
| 106 | + * ❌ **Not providing fallback for getReturnUrl** |
| 107 | + * ```nunjucks |
| 108 | + * {# WRONG: Returns empty string if no referrer #} |
| 109 | + * <a href="{{ '' | getReturnUrl(referrerChain) }}">Back</a> |
| 110 | + * ``` |
| 111 | + * ✅ **Always provide a sensible fallback** |
| 112 | + * ```nunjucks |
| 113 | + * {# CORRECT: Falls back to sensible default #} |
| 114 | + * <a href="{{ '../parent-page' | getReturnUrl(referrerChain) }}">Back</a> |
| 115 | + * ``` |
6 | 116 | */ |
7 | 117 |
|
8 | 118 | /** |
@@ -144,6 +254,12 @@ const appendReferrer = (existingReferrerChain, newUrl) => { |
144 | 254 | if (!existingReferrerChain) return newUrl |
145 | 255 |
|
146 | 256 | const chain = parseReferrerChain(existingReferrerChain) |
| 257 | + |
| 258 | + // Don't append if it's already the last item in the chain (prevents duplicates) |
| 259 | + if (chain.length > 0 && chain[chain.length - 1] === newUrl) { |
| 260 | + return existingReferrerChain |
| 261 | + } |
| 262 | + |
147 | 263 | chain.push(newUrl) |
148 | 264 | return chain.join(',') |
149 | 265 | } |
|
0 commit comments