PageSourceSearch

https://my.hoist.nz/login

html hoist.nz collected 2026-10-05 22:33:44 UTC 14,628 bytes, 257 lines download raw bytes

1<!doctype html>
2<html lang="en">
3    <head>
4        <meta charset="utf-8" />
5        <!-- DNS prefetch and preconnect for faster API calls -->
6        <link rel="dns-prefetch" href="https://api.hoist.nz" />
7        <link rel="preconnect" href="https://api.hoist.nz" crossorigin />
8        <meta name="viewport" content="width=device-width, initial-scale=1" />
9        <meta name="referrer" content="strict-origin" />
10        <!-- Mobile browser chrome follows the navbar: brand green in light, near-black in dark. -->
11        <meta name="theme-color" content="#032B3A" media="(prefers-color-scheme: light)" />
12        <meta name="theme-color" content="#1A1B1E" media="(prefers-color-scheme: dark)" />
13        <meta name="description" content="New Zealand's Newest Workshop Automation Software!" />
14        <title>Hoist</title>
15
16        <!-- Google Analytics -->
vendor: 72 bytes, lines 16-17
16
17        <script async src="https://www.googletagmanager.com/gtag/js?id=
17G-G5WM9S295M
vendor: 20 bytes, lines 17-18
17"></script>
18        
18<script>
19            window.dataLayer = window.dataLayer || [];
20            function gtag() {
21                dataLayer.push(arguments);
22            }
23            gtag('js', new Date());
24
25            // Analytics fire ONLY from production. Staging (staging.0h.nz) and
26            // local dev were streaming events into the live GA4 properties and the
27            // Meta pixel, polluting audiences, funnels and conversion counts.
28            var HOIST_ANALYTICS_ON = window.location.hostname === 'my.hoist.nz';
29
30            // Cross-domain measurement with the marketing site (hoist.nz).
31            // Must be set before the config commands so inbound _gl links are read.
32            gtag('set', 'linker', {
33                domains: ['hoist.nz', 'www.hoist.nz', 'my.hoist.nz'],
34                accept_incoming: true,
35            });
36
37            // Product analytics — this app's own property.
38            if (HOIST_ANALYTICS_ON) gtag('config', 'G-G5WM9S295M');
39
40            // Marketing property, but ONLY when the visitor enters at /register.
41            //
42            // Cross-domain attribution needs both domains reporting to one property,
43            // otherwise the hoist.nz -> my.hoist.nz/register journey splits into two
44            // sessions and no trial signup can be credited to the page that produced
45            // it. Configuring it on every route instead meant logged-in workshops
46            // reported their whole working day into the marketing property - it hit
47            // 71% app traffic within a day, inflating sessions and engagement and
48            // depressing conversion rate against a denominator full of existing
49            // customers.
50            //
51            // Entering at /register is exactly the journey worth measuring, and it
52            // is the only path the marketing site links to for signup. Once
53            // configured the property stays active for that page session, so the
54            // rest of the signup flow is still captured. Someone opening the app at
55            // /dashboard for their day's work never configures it.
56            if (HOIST_ANALYTICS_ON && window.location.pathname.replace(/\/+$/, '') === '/register') {
57                gtag('config', 'G-NB4C45G109');
58                // Google Ads conversion destination. Reads the _gcl_aw cookie written
59                // on hoist.nz (shared via .hoist.nz) so the "Hoist trial signup"
60                // conversion fired in analytics.js signUp() attributes to the ad
61                // click that produced it.
62                gtag('config', 'AW-18374508068');
63            }
64        </script>
64
65
66        <!--
67      Meta pixel.
68
69      The _fbc cookie carrying the ad click is written on hoist.nz and is scoped
70      to that host, so it does not exist here - which would leave every signup
71      produced by a Meta ad unattributed and report the campaign as dead. The
72      marketing site therefore appends the click ID as a _fbc query parameter on
73      its links to this domain (see MetaPixel.tsx there), and the block below
74      reinstates it as a cookie BEFORE fbq('init') so the pixel picks it up.
75
76      This is the Meta counterpart to the GA linker configured above, which
77      solves the same split-session problem for Google Analytics.
78
79      Only written when no _fbc cookie is already present: if Meta ever scopes
80      its own cookie to .hoist.nz, that one is authoritative and must win.
81    -->
82        
82<script>
83            (function () {
84                try {
85                    var param = new URLSearchParams(window.location.search).get('_fbc');
86                    if (!param) return;
87                    if (document.cookie.indexOf('_fbc=') !== -1) return;
88                    // 90 days, matching Meta's own click attribution window.
89                    document.cookie =
90                        '_fbc=' + encodeURIComponent(param) + ';path=/;max-age=' + 90 * 24 * 60 * 60 + ';SameSite=Lax';
91                } catch (e) {
92                    /* Attribution is never worth breaking the app over. */
93                }
94            })();
95        </script>
95
96        
96<script>
97            // Whether this page session can ever fire a Meta conversion. CompleteRegistration
98            // (utils/analytics.js) is the only Meta event this app sends, and it has exactly one caller:
99            // analytics.signUp() in features/auth/Register.jsx, which renders only at /register. Anywhere
100            // else the pixel could not fire even if it were loaded.
101            //
102            // The GATE WRAPS THE LOADER, not just fbq('init'). The snippet below injects fbevents.js as a
103            // side effect of running at all, so gating init alone would still have downloaded the script
104            // on every page and saved nothing -- every signed-in workshop pulled it all day, landing about
105            // 1.2s into a cold first open and competing with the app's own bundle for the connection.
106            //
107            // Production only, as before: staging and dev must never feed the live pixel (they were
108            // inflating retargeting audiences). Still init only, deliberately no PageView -- every page a
109            // signed-in workshop opened was landing in the pixel, 7,891 events over three days in August
110            // 2026, roughly 75% of the whole dataset, putting existing customers into the retargeting and
111            // lookalike pools the acquisition campaigns spend against.
112            var HOIST_META_PIXEL_ON =
113                window.location.hostname === 'my.hoist.nz' &&
114                window.location.pathname.replace(/\/+$/, '') === '/register';
115
116            if (HOIST_META_PIXEL_ON) {
117                !(function (f, b, e, v, n, t, s) {
118                    if (f.fbq) return;
119                    n = f.fbq = function () {
120                        n.callMethod ? n.callMethod.apply(n, arguments) : n.queue.push(arguments);
121                    };
122                    if (!f._fbq) f._fbq = n;
123                    n.push = n;
124                    n.loaded = !0;
125                    n.version = '2.0';
126                    n.queue = [];
127                    t = b.createElement(e);
128                    t.async = !0;
129                    t.src = v;
130                    s = b.getElementsByTagName(e)[0];
131                    s.parentNode.insertBefore(t, s);
132                })(window, document, 'script', 'https://connect.facebook.net/en_US/fbevents.js');
133                fbq('init', '1582817223439516');
134            }
135        </script>
135
136
137        <style>
138            /* Boot screen only. Values are the resolved --muted ground and --brand-edge ink from
139         src/index.css and src/styles/brand.css; they are literals because no stylesheet has
140         loaded yet at this point. Keep them in step with those tokens. */
141            #boot-loader {
142                position: fixed;
143                inset: 0;
144                display: flex;
145                align-items: center;
146                justify-content: center;
147                background: #faf9f6;
148            }
149            /* The track is the brand ink at low alpha rather than a second grey, so it sits on either
150         ground without needing a third colour defined per theme. */
151            #boot-loader .boot-track {
152                position: relative;
153                width: 190px;
154                height: 4px;
155                border-radius: 999px;
156                overflow: hidden;
157                background: rgba(3, 43, 58, 0.14);
158            }
159            #boot-loader .boot-fill {
160                position: absolute;
161                top: 0;
162                bottom: 0;
163                border-radius: 999px;
164                background: #032b3a;
165                animation: boot-slide 1.25s ease-in-out infinite;
166            }
167            #boot-loader.boot-dark {
168                background: #141414;
169            }
170            #boot-loader.boot-dark .boot-track {
171                background: rgba(217, 217, 217, 0.18);
172            }
173            #boot-loader.boot-dark .boot-fill {
174                background: #d9d9d9;
175            }
176            /* Indeterminate: the segment sweeps the track and never lands, because there is no way
177               to know how far through a lazy import the browser is. Animating the two insets rather
178               than translating a fixed-width block keeps part of the segment on the track for the
179               whole cycle; a translate large enough to clear the track leaves it visibly empty for
180               a good fraction of every loop. */
181            @keyframes boot-slide {
182                0% {
183                    left: -35%;
184                    right: 100%;
185                }
186                100% {
187                    left: 100%;
188                    right: -35%;
189                }
190            }
191            @media (prefers-reduced-motion: reduce) {
192                /* A sweeping bar is exactly the motion this setting is asking us not to make, so the bar
193           holds still and reads as a static indicator instead. */
194                #boot-loader .boot-fill {
195                    animation: none;
196                    left: 0;
197                    right: 0;
198                    opacity: 0.45;
199                }
200            }
201        </style>
202      
202<script type="module" crossorigin src="/assets/index-_SHl6aX8.js"></script>
202
203      <link rel="modulepreload" crossorigin href="/assets/vendor-react-CuNWJD29.js">
204      <link rel="modulepreload" crossorigin href="/assets/vendor-query-CmJ3zgYk.js">
205      <link rel="modulepreload" crossorigin href="/assets/vendor-signalr-B6NuNrLI.js">
206      <link rel="stylesheet" crossorigin href="/assets/index-Bs-GP6zO.css">
207    </head>
208    <body>
209        <noscript>You need to enable JavaScript to run this app.</noscript>
210        <!--
211      Boot screen.
212
213      #root is empty until React's first paint, which measured 438-572ms on a throttled cold load:
214      the user stared at a blank white page for half a second before anything at all appeared. This
215      is raw markup and inline CSS on purpose — it has to paint from the HTML alone, before a single
216      byte of the bundle has been parsed, so it cannot come from a component.
217
218      It sits on the SAME ground as the app's own loading state, so React taking over changes the
219      content but never the backdrop. A slim indeterminate bar rather than the steering wheel: the
220      wheel is a 19KB PNG, and index.html is served no-cache, so inlining it would have added ~25KB
221      of base64 to every single load, while linking it would have put a blocking request in front of
222      the one screen whose whole job is to appear immediately.
223
224      Removed by src/index.jsx once React has painted, with a timeout here as a backstop so a
225      failed boot can never leave the overlay covering the error screen underneath.
226    -->
227        <div id="boot-loader" role="status" aria-label="Loading Hoist">
228            <div class="boot-track"><div class="boot-fill"></div></div>
229        </div>
230        
230<script>
231            (function () {
232                // Mirrors ThemeContext exactly: stored choice, defaulting to light, with 'system' deferring
233                // to the OS. Resolving it here rather than in React is what stops a white flash before a
234                // dark-mode user's app paints.
235                try {
236                    var saved = localStorage.getItem('theme') || 'light';
237                    var dark =
238                        saved === 'dark' ||
239                        (saved === 'system' && window.matchMedia('(prefers-color-scheme: dark)').matches);
240                    if (dark) document.getElementById('boot-loader').classList.add('boot-dark');
241                } catch (e) {
242                    /* Private mode or blocked storage: the light default is already correct. */
243                }
244                // Backstop only. index.jsx removes this as soon as React paints; this covers the case where
245                // the bundle never runs at all, so the overlay cannot hide whatever is underneath forever.
246                setTimeout(function () {
247                    var el = document.getElementById('boot-loader');
248                    if (el) el.remove();
249                }, 15000);
250            })();
251        </script>
251
252        <div id="root"></div>
253        <div id="datepicker-portal"></div>
254        <!-- Vite injects the module entry point here at build time -->
255    
255<script>(function(){function c(){var b=a.contentDocument||(a.contentWindow&&a.contentWindow.document);if(b){var d=b.createElement('script');d.innerHTML="window.__CF$cv$params={r:'a45fe3bfa9c2ba2c',t:'MTc5MTIzOTYyMw=='};var a=document.createElement('script');a.src='/cdn-cgi/challenge-platform/scripts/jsd/main.js';document.getElementsByTagName('head')[0].appendChild(a);";b.getElementsByTagName('head')[0].appendChild(d)}}if(document.body){var a=document.createElement('iframe');a.height=1;a.width=1;a.style.position='absolute';a.style.top=0;a.style.left=0;a.style.border='none';a.style.visibility='hidden';document.body.appendChild(a);if('loading'!==document.readyState)c();else if(window.addEventListener)document.addEventListener('DOMContentLoaded',c);else{var e=document.onreadystatechange||function(){};document.onreadystatechange=function(b){e(b);'loading'!==document.readyState&&(document.onreadystatechange=e,c())}}}})();</script>
255<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/v31edd6df95cf4e85bb4c19e7a9bdbcba1788362987495" integrity="sha512-iIg7k2xntmwu6/uSb5tpc/hySgZc4eoL31yB29W6tJFo2akwjPWcEqnCEdJvGexCL0KEQwVYv5BlowfhVz26hg==" data-cf-beacon='{"version":"2024.11.0","token":"c69b62ffc99d4342b91f3ce0c88908d4","spa":2}' crossorigin="anonymous"></script>
255
256</body>
257</html>

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.