-
Notifications
You must be signed in to change notification settings - Fork 4
/
nuxt.config.ts
657 lines (612 loc) · 23.8 KB
/
nuxt.config.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
import eslintPlugin from 'vite-plugin-eslint'
import { VitePWA } from 'vite-plugin-pwa'
import { sentryVitePlugin } from '@sentry/vite-plugin'
import { splitVendorChunkPlugin } from 'vite'
import config from './config'
// @ts-ignore
export default defineNuxtConfig({
// Rendering modes are confusing.
//
// - target can be:
// - static: can host on static hosting such as Azure Static Web Apps
// - server: requires a node server.
// - ssr can be:
// - true: renders at
// - generate time for https://github.com/nuxt/framework/discussions/4523: static, or
// - in node server for target: server)
// - false: renders on client.
//
// Ideally we'd use SSR so that we could render pages on the server or client depending on our hosting choice.
// - But not all dependencies we use support SSR.
// - Crucially, we use Bootstrap and bootstrap-vue-next.
// - These do not yet support SSR.
//
// So we can't render full pages on the server any time soon. Can we just render purely on the client?
//
// Crawlers nowadays are smart enough to render pages on the client. So that would be fine.
// But Facebook link preview isn't, and we want that to work.
//
// However to get that preview working:
// - We only really need the meta tags which are added in the setup() calls of
// individual pages.
// - We don't need the full DOM rendered.
// - So we can mask out bootstrap-containing elements using <client-only>, and use async component
// loading to avoid pulling in code if need be.
//
// We handle most of this in the pages, rather than in the components - pages are where we set the meta tags for
// preview.
//
// Unfortunately:
// - Nuxt/Vue has issues setting meta tags via useHead() when using the options API, where you can get
// an error saying the nuxt instance is not available if you've done an await first.
// - Sometimes we do want to do that, e.g. to get a group so that we can use the info in the meta tags.
// - In that case we've reworked the pages to use <script setup>.
// - For historical reasons and preference we use the options API everywhere else.
//
// Sometimes when debugging it's useful to set ssr: false, because the errors are clearer when generated on the client.
// @ts-ignore
target: 'server',
ssr: true,
spaLoadingTemplate: false,
// This makes Netlify serve assets from the perm link for the build, which avoids missing chunk problems when
// a new deploy happens. See https://github.com/nuxt/nuxt/issues/20950.
//
// We still want to serve them below our domain, though, otherwise some security software gets tetchy. So we
// do that and then the _redirects file will proxy it to the correct location.
$production: {
app: {
cdnURL: process.env.DEPLOY_URL
? '/netlify/' + process.env.DEPLOY_URL.replace('https://', '')
: '',
},
},
routeRules: {
// Nuxt3 has some lovely features to do with how routes are generated/cached. We use:
//
// prerender: true - this will be generated at build time.
// static: true - this is generated on demand, and then cached until the next build
// isr: 'time' - this is generated on demand each 'time' period.
// ssr: false - this is client-side rendered.
//
// There are potential issues where a deployment happens while a page is partway through loading assets, or
// later loads assets which are no longer present. Nuxt3 now has a fallback of reloading the page when
// it detects a failed chunk load.
'/': { prerender: true },
'/explore': { prerender: true },
'/unsubscribe**': { prerender: true },
'/about': { prerender: true },
'/disclaimer': { prerender: true },
'/donate': { prerender: true },
'/find': { prerender: true },
'/forgot': { prerender: true },
'/give': { prerender: true },
'/help': { prerender: true },
'/maintenance': { prerender: true },
'/mobile': { prerender: true },
'/privacy': { prerender: true },
'/unsubscribe': { prerender: true },
'/yahoologin': { prerender: true },
// These pages are for logged-in users, or aren't performance-critical enough to render on the server.
'/browse/**': { ssr: false },
'/chats/**': { ssr: false },
'/chitchat/**': { ssr: false },
'/donated': { ssr: false },
'/giftaid': { ssr: false },
'/job/**': { ssr: false },
'/jobs': { ssr: false },
'/merge/**': { ssr: false },
'/myposts': { ssr: false },
'/mypost/**': { ssr: false },
'/noticeboards/**': { ssr: false },
'/post': { ssr: false },
'/profile/**': { ssr: false },
'/promote': { ssr: false },
'/settings/**': { ssr: false },
'/stats/**': { ssr: false },
'/stories/**': { ssr: false },
'/teams': { ssr: false },
'/adtest': { ssr: false },
// Render on demand - may never be shown in a given build - then cache for a while.
'/explore/region/**': { isr: 3600 },
'/communityevent/**': { isr: 3600 },
'/communityevents/**': { isr: 3600 },
'/explore/**': { isr: 3600 },
'/message/**': { isr: 600 },
'/story/**': { isr: 3600 },
'/shortlink/**': { isr: 600 },
'/volunteering/**': { isr: 3600 },
'/volunteerings/**': { isr: 3600 },
// Allow CORS for chunk fetches - required for Netlify hosting.
'/_nuxt/**': {
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Headers':
'Origin, X-Requested-With, Content-Type, Accept, Authorization',
},
},
},
nitro: {
prerender: {
routes: ['/404.html', '/sitemap.xml'],
// Don't prerender the messages - too many.
ignore: ['/message/'],
crawlLinks: true,
},
},
render: {
bundleRenderer: {
shouldPrefetch: () => false,
shouldPreload: () => false,
},
},
experimental: {
emitRouteChunkError: 'reload',
asyncContext: true,
// Payload extraction breaks SSR with routeRules - see https://github.com/nuxt/nuxt/issues/22068
renderJsonPayloads: false,
payloadExtraction: false,
},
webpack: {
// Reduce size of CSS initial load.
extractCSS: true,
},
modules: [
'@pinia/nuxt',
'floating-vue/nuxt',
'@nuxt/image',
'nuxt-vite-legacy',
'@bootstrap-vue-next/nuxt',
],
hooks: {
'build:manifest': (manifest) => {
for (const item of Object.values(manifest)) {
item.dynamicImports = []
item.prefetch = false
// Removing preload links is the magic that drops the FCP on mobile
item.preload = false
}
},
},
// Environment variables the client needs.
runtimeConfig: {
public: {
APIv1: config.APIv1,
APIv2: config.APIv2,
OSM_TILE: config.OSM_TILE,
GEOCODE: config.GEOCODE,
FACEBOOK_APPID: config.FACEBOOK_APPID,
YAHOO_CLIENTID: config.YAHOO_CLIENTID,
GOOGLE_MAPS_KEY: config.GOOGLE_MAPS_KEY,
GOOGLE_API_KEY: config.GOOGLE_API_KEY,
GOOGLE_CLIENT_ID: config.GOOGLE_CLIENT_ID,
USER_SITE: config.USER_SITE,
IMAGE_SITE: config.IMAGE_SITE,
UPLOADCARE_PROXY: config.UPLOADCARE_PROXY,
SENTRY_DSN: config.SENTRY_DSN,
BUILD_DATE: new Date().toISOString(),
NETLIFY_DEPLOY_ID: process.env.DEPLOY_ID,
NETLIFY_SITE_NAME: process.env.SITE_NAME,
MATOMO_HOST: process.env.MATOMO_HOST,
COOKIEYES: config.COOKIEYES,
TRUSTPILOT_LINK: config.TRUSTPILOT_LINK,
},
},
css: [
'@fortawesome/fontawesome-svg-core/styles.css',
'/assets/css/global.scss',
'leaflet/dist/leaflet.css',
],
vite: {
css: {
preprocessorOptions: {
scss: {
additionalData:
// Include some CSS in all components.
// There are some other Bootstrap files we'd like to include, but doing this breaks the colours in a way
// I don't understand and can't fix.
'@import "@/assets/css/_color-vars.scss";',
},
},
},
plugins: [
splitVendorChunkPlugin(),
VitePWA({ registerType: 'autoUpdate' }),
// Make Lint errors cause build failures.
eslintPlugin(),
sentryVitePlugin({
org: 'freegle',
project: 'nuxt3',
}),
],
},
// Note that this is not the standard @vitejs/plugin-legacy, but https://www.npmjs.com/package/nuxt-vite-legacy
legacy: {
targets: ['chrome 49', 'since 2015', 'ios>=12', 'safari>=12'],
modernPolyfills: [
'es.global-this',
'es.object.from-entries',
'es.array.flat-map',
'es.array.flat',
'es.string.replace-all',
],
},
// Sentry needs sourcemaps.
sourcemap: {
client: true,
server: true,
},
// Sometimes we need to change the host when doing local testing with browser stack.
devServer: {
host: '127.0.0.1',
port: 3000,
},
app: {
head: {
htmlAttrs: {
lang: 'en',
},
title: "Freegle - Don't throw it away, give it away!",
script: [
{
// This is a polyfill for Safari12. Can't get it to work using modernPolyfills - needs to happen very
// early. Safari12 doesn't work well, but this makes it functional.
type: 'text/javascript',
innerHTML: `try { if (!window.globalThis) { window.globalThis = window; } } catch (e) { console.log('Polyfill error', e.message); }`,
},
// The ecosystem of advertising is complex.
// - The underlying ad service is Google Tags (GPT).
// - We use prebid (pbjs), which is some kind of ad broker which gives us a pipeline of ads to use.
// We can also define our own ads in GPT.
// - Google and prebid both require use of a Consent Management Platform (CMP) so that the
// user has indicated whether we have permission to show personalised ads. We use CookieYes.
// - So we need to signal to Google and prebid which CMP we're using, which we do via window.dataLayer,
// window.gtag and window.pbjs.
// - We also have to define the possible advertising slots available to prebid so that it knows what to bid on.
// We do this once, here, for all slots. Only some slots may appear on any given page - they are
// defined and added in ExternalDa.
// - When using prebid, we disable the initial ad load because it doesn't happen until after the prebid,
// inside ExternalDa.
//
// During development we don't have a CMP because CookieYes doesn't work on localhost. So in that case we
// don't disable initial ad load - so Google will load ads immediately.
//
// The order in which we load scripts is excruciatingly and critically important - see below.
//
// But we want to reduce LCP, so we defer all this by loading with async.
{
type: 'text/javascript',
body: true,
async: true,
innerHTML:
`try {
window.dataLayer = window.dataLayer || [];
function ce_gtag() {
window.dataLayer.push(arguments);
}
ce_gtag("consent", "default", {
ad_storage: "denied",
ad_user_data: "denied",
ad_personalization: "denied",
analytics_storage: "denied",
functionality_storage: "denied",
personalization_storage: "denied",
security_storage: "granted",
// wait_for_update shouldn't apply because we force the CMP to load before gtag.
wait_for_update: 2000,
});
ce_gtag("set", "ads_data_redaction", true);
ce_gtag("set", "url_passthrough", true);
console.log('Initialising pbjs and googletag...');
window.googletag = window.googletag || {};
window.googletag.cmd = window.googletag.cmd || [];
window.googletag.cmd.push(function() {
// On the dev server, where COOKIEYES is not set, we want ads to load immediately.
` +
(config.COOKIEYES
? `window.googletag.pubads().disableInitialLoad()`
: '') +
`
window.googletag.pubads().enableSingleRequest()
window.googletag.enableServices()
});
window.pbjs = window.pbjs || {};
window.pbjs.que = window.pbjs.que || [];
window.pbjs.que.push(function() {
// Custom rounding function recommended by Magnite.
const roundToNearestEvenIncrement = function (number) {
let ceiling = Math.ceil(number);
let ceilingIsEven = ceiling % 2 === 0;
if (ceilingIsEven) {
return ceiling;
} else {
return Math.floor(number);
}
}
window.pbjs.setConfig({
consentManagement: {
// We only need GDPR config. We are interested in UK users, who are (for GDPR purposes if not
// political purposes) inside the EU.
gdpr: {
cmpApi: 'iab',
allowAuctionWithoutConsent: false,
timeout: 3000
},
// usp: {
// timeout: 8000
// },
// gpp: {
// cmpApi: 'iab',
// timeout: 8000
// }
},
cache: {
url: 'https://prebid-server.rubiconproject.com/vtrack?a=26548',
ignoreBidderCacheKey: true,
vasttrack: true
},
s2sConfig: [{
accountId: '26548',
bidders: ['mgnipbs'], // PBS ‘bidder’ code that triggers the call to PBS
defaultVendor: 'rubicon',
coopSync: true,
userSyncLimit: 8, // syncs per page up to the publisher
defaultTtl: 300, // allow Prebid.js to cache bids for 5 minutes
allowUnknownBidderCodes: true, // so PBJS doesn't reject responses
extPrebid: {
cache: { // only needed if you're running video
vastxml: { returnCreative: false }
},
bidders: {
mgnipbs: {
wrappername: "26548_Freegle"
}
}
}
}],
targetingControls: {
addTargetingKeys: ['SOURCE']
},
cpmRoundingFunction : roundToNearestEvenIncrement,
useBidCache: true
});
// Gourmetads requires schain config.
pbjs.setBidderConfig({
"bidders": ['gourmetads'],
"config": {
"schain": {
"validation": "relaxed",
"config": {
"ver":"1.0",
"complete": 1,
"nodes": [
{
"asi":"gourmetads.com",
"sid":"16593",
"hp":1
}
]
}
},
}
});
});
window.pbjs.que.push(function() {
console.log('Add PBJS ad units', ` +
JSON.stringify(config.AD_PREBID_CONFIG) +
`);
window.pbjs.addAdUnits(` +
JSON.stringify(config.AD_PREBID_CONFIG) +
`)
});
function loadScript(url, block) {
if (url && url.length) {
console.log('Load script:', url);
var script = document.createElement('script');
script.defer = true;
script.type = 'text/javascript';
script.src = url;
if (block) {
// Block loading of this script until CookieYes has been authorised.
// It's not clear that this blocking works, but it does no harm to
// ask for it.
console.log('Set CookieYes script block', url);
script.setAttribute('data-cookieyes', 'cookieyes-advertisement')
}
document.head.appendChild(script);
}
}
function postCookieYes() {
console.log('Consider load of GPT and prebid');
if (!window.weHaveLoadedGPT) {
window.weHaveLoadedGPT = true;
// We need to load:
// - GPT, which needs to be loaded before prebid.
// - Prebid.
// The ordering is ensured by using defer and appending the script.
//
// prebid isn't compatible with older browsers which don't support Object.entries.
console.log('Load GPT and prebid');
if (Object.fromEntries) {
loadScript('https://securepubads.g.doubleclick.net/tag/js/gpt.js', true)
loadScript('/js/prebid.js', true)
}
} else {
console.log('GPT and prebid already loaded');
}
};
function postGSI() {
if ('` +
config.COOKIEYES +
`' != 'null') {
// First we load CookieYes, which needs to be loaded before anything else, so that
// we have the cookie consent.
console.log('Load CookieYes');
loadScript('` +
config.COOKIEYES +
`', false)
// Now we wait until the CookieYes script has set its own cookie.
// This might be later than when the script has loaded in pure JS terms, but we
// need to be sure it's loaded before we can move on.
function checkCookieYes() {
if (document.cookie.indexOf('cookieyes-consent') > -1) {
console.log('CookieYes cookie is set, so CookieYes is loaded');
// Check that we have set the TCF string. This only happens once the user
// has responded to the cookie banner.
if (window.__tcfapi) {
window.__tcfapi('getTCData', 2, (tcData, success) => {
if (success && tcData && tcData.tcString) {
console.log('TC data loaded and TC String set');
postCookieYes();
} else {
console.log('Failed to get TC data or string, retry.')
setTimeout(checkCookieYes, 100);
}
}, [1,2,3]);
} else {
console.log('TCP API not yet loaded')
setTimeout(checkCookieYes, 100);
}
} else {
console.log('CookieYes not yet loaded')
setTimeout(checkCookieYes, 100);
}
}
checkCookieYes();
} else {
console.log('No CookieYes to load')
postCookieYes();
}
}
window.onGoogleLibraryLoad = postGSI
// We have to load GSI before we load the cookie banner, otherwise the Google Sign-in button doesn't
// render.
loadScript('https://accounts.google.com/gsi/client')
} catch (e) {
console.error('Error initialising pbjs and googletag:', e.message);
}`,
},
],
meta: [
{ charset: 'utf-8' },
{ name: 'viewport', content: 'width=device-width, initial-scale=1' },
{ hid: 'author', name: 'author', content: 'Freegle' },
{ name: 'supported-color-schemes', content: 'light' },
{ name: 'color-scheme', content: 'light' },
{
name: 'facebook-domain-verification',
content: 'zld0jt8mvf06rt1c3fnxvls3zntxj6',
},
{ hid: 'og:type', property: 'og:type', content: 'website' },
{
hid: 'description',
name: 'description',
content:
"Give and get stuff for free in your local community. Don't just recycle - reuse, freecycle and freegle!",
},
{
hid: 'apple-mobile-web-app-title',
name: 'apple-mobile-web-app-title',
content:
"Give and get stuff for free in your local community. Don't just recycle - reuse, freecycle and freegle!",
},
{
hid: 'og:image',
property: 'og:image',
content: config.USER_SITE + '/icon.png',
},
{ hid: 'og:locale', property: 'og:locale', content: 'en_GB' },
{
hid: 'og:title',
property: 'og:title',
content: "Freegle - Don't throw it away, give it away!",
},
{ hid: 'og:site_name', property: 'og:site_name', content: 'Freegle' },
{
hid: 'og:url',
property: 'og:url',
content: 'https://www.ilovefreegle.org',
},
{
hid: 'fb:app_id',
property: 'fb:app_id',
content: config.FACEBOOK_APPID,
},
{
hid: 'og:description',
property: 'og:description',
content:
"Give and get stuff for free in your local community. Don't just recycle - reuse, freecycle and freegle!",
},
{
hid: 'fb:app_id',
property: 'og:site_name',
content: config.FACEBOOK_APPID,
},
{
hid: 'twitter:title',
name: 'twitter:title',
content: "Freegle - Don't throw it away, give it away!",
},
{
hid: 'twitter:description',
name: 'twitter:description',
content:
"Give and get stuff for free in your local community. Don't just recycle - reuse, freecycle and freegle!",
},
{
hid: 'twitter:image',
name: 'twitter:image',
content: config.USER_SITE + '/icon.png',
},
{
hid: 'twitter:image:alt',
name: 'twitter:image:alt',
content: 'The Freegle logo',
},
{
hid: 'twitter:card',
name: 'twitter:card',
content: 'summary_large_image',
},
{ hid: 'twitter:site', name: 'twitter:site', content: 'thisisfreegle' },
{
hid: 'OMG-Verify-V1',
name: 'OMG-Verify-V1',
content: '954a2917-d603-4df4-8802-f6a78846a9c0',
},
{
hid: 'Awin',
name: 'Awin',
content: 'Awin',
},
],
},
},
image: {
uploadcare: {
provider: 'uploadcare',
// On some machines we can't resolve ucarecdn.com but can resolve www.ucarecdn.com.
cdnURL: 'https://www.ucarecdn.com',
},
// We want sharp images on fancy screens.
densities: [1, 2],
// Uploadcare only supports images upto 3000. So we drop the top-level screen sizes, which get doubled
// to produce the images requested.
//
// We also want to match the Bootstrap breakpoints.
screens: {
xs: 320,
sm: 576,
md: 768,
lg: 992,
xl: 1200,
xxl: 1400,
'2xl': 1400,
},
providers: {
uploadcareProxy: {
provider: '~/providers/uploadcare-proxy.ts',
},
},
},
})