-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathsites.js
More file actions
894 lines (814 loc) · 37.3 KB
/
Copy pathsites.js
File metadata and controls
894 lines (814 loc) · 37.3 KB
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
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
import archived from "./archived.json" with { type: "json" };
import pinned from "./pinned.json" with { type: "json" };
import community from "./11ty-community.json" with { type: "json" };
import emeritus from "./11ty-emeritus.json" with { type: "json" };
import starters from "./11ty-starters.json" with { type: "json" };
/**
* Starter projects and templates.
*
* Mostly imported from 11ty/docs, which is the canonical list — its `demo`
* field is the deployed site, and entries it marks `excludedFromLeaderboards`
* are honored rather than second-guessed. `EXTRA_STARTERS` is for anything
* spotted in the community list that upstream does not carry.
*
* The union also filters the community category: a starter that appears in both
* lists belongs here, and deleting it from the generated community file would
* only last until the next import. Excluding it at this level keeps that file
* exactly as upstream sends it.
*/
const EXTRA_STARTERS = [
"https://11skeleton.vercel.app/",
"https://eleventy-test-bed-dollplayer2501.netlify.app/",
"https://eleventy-step.netlify.app/",
"https://multilingual.lea-tortay.com/",
"https://yetty.netlify.app/",
];
const STARTERS = [...new Set([...starters.urls, ...EXTRA_STARTERS])];
/**
* Sites that used to be built with Eleventy or Build Awesome.
*
* `config/11ty-emeritus.json` holds the bulk of them, recovered from the
* community repo's git history. `EXTRA_EMERITUS` is the place to add one by
* hand — a site you know has moved on but whose page says nothing either way,
* so the generator rule cannot see it.
*
* Nothing needs adding here for the ordinary case: a site measured as a
* different generator is moved automatically at report time. This is only for
* what measurement cannot detect.
*
* Like STARTERS, the union also filters the community category, so a URL listed
* here leaves Built Awesome rather than appearing in both.
*
* Normalized form: https, trailing slash on a bare origin, none on a path.
*/
const EXTRA_EMERITUS = [
"https://annotator.incubator.apache.org/",
/*
* Redirects to scottpdawson.substack.com, and deleted from the community
* repo upstream.
*
* Needed here for both halves. The generator rule cannot move it: every
* measurement of the old URL came back a Cloudflare bot check, so the page
* never reported a generator at all — and a redirect off the domain is not
* something that rule reads anyway.
*
* And the deletion upstream does not place it either. `11ty-emeritus.json` is
* hand-curated, recovered once from the community repo's history rather than
* regenerated, so nothing picks up a deletion made after that. Without this
* line the site simply leaves the corpus on the next import: no page, no
* history, no record that it was ever built with Eleventy.
*/
"https://scottpdawson.com/",
];
/**
* Community sites added by hand.
*
* `config/11ty-community.json` is generated by scripts/import-11ty-community.mjs
* and rewritten whole on every import, so a URL added there lasts until the next
* run. This is where one goes to stay — for sites that belong on the list but
* are not on it upstream, or were dropped from it and should not have been.
*
* Anything here almost certainly wants pinning too: the category moves a site
* out when its page reports a generator other than Eleventy, and a site being
* re-added by hand is usually one that reports something unusual. See
* `pinned` below.
*/
const EXTRA_COMMUNITY = [
"https://cassettenest.com/",
"https://open-wc.org/",
"https://www.ryzokuken.dev/",
];
const EMERITUS = [...new Set([...emeritus.urls, ...EXTRA_EMERITUS])];
/**
* The list of sites to measure.
*
* Add or remove entries here — everything else (results, history, the static
* site) keys off of the `url`, so renaming a site keeps its history but
* changing a URL starts a fresh history for that URL.
*/
/**
* Sites taken out of circulation.
*
* Archiving is the middle option. Deleting a URL loses its measurements and
* leaves an orphaned history behind; leaving it in place keeps a dead site in
* the rankings and keeps sending traffic to it. An archived site keeps its
* record, stops being measured, and appears only as plain text — no row, no
* link, no page.
*
* Applies across every category at once, so a URL listed in two of them does
* not need archiving twice.
*/
const ARCHIVED = archived.urls;
/**
* Sites held where this config puts them.
*
* Categories normally reclassify themselves: `requireGenerator` moves a site out
* of Built Awesome when its page reports something else, and `rejectGenerator`
* moves it back out of Emeritus when it reports Eleventy again. That is right
* almost always, and it re-applies on every build — so a category change made by
* hand does not survive the next report unless the detection agrees with it.
*
* This is the exception list for when detection is reading the page correctly
* and still getting the answer wrong: a stale generator tag left in a template,
* a proxy or platform stamping its own, a site built with one thing and
* reporting another. A URL here keeps whatever categories it is listed in.
*
* Not a way to disagree with a correct detection. A site that really has moved
* to Astro belongs in Emeritus, and pinning it there would make this list a
* quiet second source of truth about what built the web.
*/
const PINNED = pinned.urls;
/**
* The two cadences, and the one politeness setting, that repeat across
* categories.
*
* Spread into a group rather than retyped: these are the same decision applied
* to several lists, and seven copies of the same four lines is how one of them
* ends up a hundred hours out of step with the rest for no reason anybody can
* reconstruct.
*/
/** Hand-maintained handfuls: measured daily, stale after two days. */
const DAILY = { freshnessHours: 24, staleAfterHours: 48 };
/** The big imported lists: weekly, stale after a fortnight. */
const WEEKLY = { freshnessHours: 24 * 7, staleAfterHours: 24 * 7 * 2 };
/**
* Other people's sites, mostly on modest hosting, measured at a deliberate
* pace. Measurement is already sequential — one Chrome, one run at a time — so
* nothing is ever requested concurrently; this governs the pace on top of that.
*
* At 3s between sites plus ~15s to measure one, each person's server sees a
* handful of page loads once per cycle: no more than a single visitor would
* generate. The host cooldown is belt and braces, for two entries that turn out
* to share a domain.
*/
const POLITE = { rateLimit: { delayMs: 3000, hostCooldownMs: 60000 } };
/*
* The four tooling categories' site lists, named so the combined category below
* can reference them rather than repeat them. Each still has its own page; the
* union is an additional view, not a replacement.
*/
const SSG_SITES = [
{ name: "Eleventy", url: "https://www.11ty.dev/" },
{ name: "Astro", url: "https://astro.build/" },
{ name: "Svelte", url: "https://svelte.dev/" },
{ name: "Hugo", url: "https://gohugo.io/" },
{ name: "Jekyll", url: "https://jekyllrb.com/" },
{ name: "VuePress", url: "https://vuepress.vuejs.org/" },
{ name: "VitePress", url: "https://vitepress.dev/" },
{ name: "Docusaurus", url: "https://docusaurus.io/" },
{ name: "Gridsome", url: "https://gridsome.org/" },
{ name: "Next.js", url: "https://nextjs.org/" },
{ name: "Gatsby", url: "https://www.gatsbyjs.com/" },
{ name: "Lume", url: "https://lume.land/" },
{ name: "Hexo", url: "https://hexo.io/" },
{ name: "Remix", url: "https://remix.run/" },
{ name: "Nuxt", url: "https://nuxt.com/" },
// Added here rather than in the imported order above: the upstream
// list predates it.
{ name: "Zola", url: "https://www.getzola.org/" },
];
const TEST_RUNNER_SITES = [
{ name: "QUnit", url: "https://qunitjs.com/" },
{ name: "webhint", url: "https://webhint.io/" },
{ name: "Mocha", url: "https://mochajs.org/" },
{ name: "ESLint", url: "https://eslint.org/" },
{ name: "Istanbul", url: "https://istanbul.js.org/" },
{ name: "gulp", url: "https://gulpjs.com/" },
{ name: "Grunt", url: "https://gruntjs.com/" },
{ name: "WebdriverIO", url: "https://webdriver.io/" },
{ name: "Intern", url: "https://theintern.io/" },
// The three that took over after this list was written: on npm,
// Vitest and Playwright run at roughly 97M and 86M a week.
{ name: "Vitest", url: "https://vitest.dev/" },
{ name: "Playwright", url: "https://playwright.dev/" },
{ name: "Puppeteer", url: "https://pptr.dev/" },
];
const PACKAGE_MANAGER_SITES = [
{ name: "npm", url: "https://www.npmjs.com/" },
{ name: "Yarn", url: "https://yarnpkg.com/" },
{ name: "JSR", url: "https://jsr.io/" },
{ name: "npmx", url: "https://npmx.dev/" },
{ name: "PyPI", url: "https://pypi.org/" },
{ name: "crates.io", url: "https://crates.io/" },
{ name: "Packagist", url: "https://packagist.org/" },
{ name: "RubyGems", url: "https://rubygems.org/" },
{ name: "Maven Central", url: "https://central.sonatype.com/" },
{ name: "NuGet", url: "https://www.nuget.org/" },
{ name: "pkg.go.dev", url: "https://pkg.go.dev/" },
{ name: "pub.dev", url: "https://pub.dev/" },
{ name: "Hex", url: "https://hex.pm/" },
{ name: "Homebrew", url: "https://brew.sh/" },
/*
* Documentation sites, not package browsers (left out for now)
*/
// { name: "pnpm", url: "https://pnpm.io/" },
// { name: "vlt", url: "https://www.vlt.io/" },
// { name: "jspm", url: "https://jspm.org/" },
// { name: "uv", url: "https://docs.astral.sh/uv/" },
// { name: "Chocolatey", url: "https://chocolatey.org/" },
];
const HOST_SITES = [
{ name: "Netlify", url: "https://www.netlify.com/", groups: ["11ty-emeritus"] },
{ name: "Vercel", url: "https://vercel.com/" },
{ name: "Cloudflare", url: "https://www.cloudflare.com/" },
{ name: "Cloudflare Pages", url: "https://pages.cloudflare.com/" },
{ name: "GitHub Pages", url: "https://pages.github.com/" },
{ name: "GitLab Pages", url: "https://docs.gitlab.com/user/project/pages/" },
{ name: "Fly.io", url: "https://fly.io/" },
{ name: "Render", url: "https://render.com/" },
{ name: "Railway", url: "https://railway.com/" },
{ name: "Deno Deploy", url: "https://deno.com/deploy" },
{ name: "Fastly", url: "https://www.fastly.com/" },
{ name: "Bunny", url: "https://bunny.net/" },
{ name: "DigitalOcean", url: "https://www.digitalocean.com/" },
{ name: "Heroku", url: "https://www.heroku.com/" },
{ name: "Surge", url: "https://surge.sh/" },
{ name: "Neocities", url: "https://neocities.org/" },
{ name: "Codeberg Pages", url: "https://codeberg.page/" },
// Product pages rather than roots: these three are hosting products
// inside far larger sites, and the company's front door would be
// measuring something else entirely.
{ name: "Firebase Hosting", url: "https://firebase.google.com/products/hosting" },
{ name: "AWS Amplify", url: "https://aws.amazon.com/amplify/" },
{ name: "Azure Static Web Apps", url: "https://azure.microsoft.com/en-us/products/app-service/static" },
];
/*
* The largest technology companies, by market capitalization.
*
* Ordered by rank at the time of writing (2026), which is a moving target — the
* top of this list reshuffles every few months and the bottom of it turns over
* every year. The order is therefore a record of when the list was made rather
* than a claim about today; what matters for the category is that these are all
* unambiguously in the top tier, not that number nine is really ninth.
*
* Each company's primary public site, which for most of them is a storefront or
* a marketing site — apple.com and samsung.com sell things, and there is no
* reason to hold amazon.com to a different rule than the ones beside it.
*
* Alphabet is the exception that proves it: the holding company's page and the
* search page it is famous for are different enough to be worth measuring as two
* entries rather than choosing between them.
*
* A note on what to expect: some of these sit behind bot protection and will
* come back as an interstitial rather than a page. That is recorded honestly
* where it happens rather than hidden, and a site that never resolves will fall
* out on the usual failure count.
*/
/*
* The AI companies, measured on the same terms as everyone else.
*
* A category with a particular reason to exist: these are the newest and
* best-funded engineering organizations on the web, most of them shipping
* heavily client-rendered product pages, and several of them selling tools that
* write other people's front ends. What they ship for themselves is worth a
* look beside the sites built with Eleventy.
*
* Chosen for public profile rather than from a ranking — there is no league
* table for this the way there is a market cap for Big Tech, so this is a
* judgment call about who is best known, not a measurement. Five labs, one
* answer engine, four places models are shared or written with, and three that
* serve them.
*
* Cohere was here and came out: real company, serious funding, but enterprise
* B2B with no product the public types into, which makes it the weakest fit for
* a list picked on public profile.
*
* No image, video or voice generators: Midjourney, ElevenLabs and Runway were
* here and came out. They are consumer media products rather than developer
* infrastructure, which is what the rest of this corpus is about.
*
* Four of the thirteen did not answer an ordinary fetch when this list was
* written: openai.com, x.ai and perplexity.ai returned 403, devin.ai 429.
* Lighthouse drives a real Chrome and may get through where a plain request
* does not; where it does not, the interstitial detection already flags the
* result as a challenge page rather than the site. Better to measure them and
* find out than to leave the category without OpenAI in it.
*
* windsurf.com is deliberately absent: it now redirects to devin.ai/desktop,
* so it would be the same site measured twice under two names.
*/
const AI_SITES = [
// The labs.
{ name: "OpenAI", url: "https://openai.com/" },
{ name: "Anthropic", url: "https://www.anthropic.com/" },
{ name: "xAI", url: "https://x.ai/" },
{ name: "Mistral", url: "https://mistral.ai/" },
{ name: "DeepSeek", url: "https://www.deepseek.com/" },
// Answers rather than models: the product is the thing you type into.
{ name: "Perplexity", url: "https://www.perplexity.ai/" },
// Where the models are shared, and the tools that write code with them.
{ name: "Hugging Face", url: "https://huggingface.co/" },
{ name: "Cursor", url: "https://cursor.com/" },
{ name: "Devin", url: "https://devin.ai/" },
{ name: "bolt.new", url: "https://bolt.new/" },
/*
* Serving and running the models. Groq in particular belongs on a site that
* measures speed for a living: it sells inference latency, so its own page
* is a claim worth checking.
*/
{ name: "Groq", url: "https://groq.com/" },
{ name: "Replicate", url: "https://replicate.com/" },
{ name: "Ollama", url: "https://ollama.com/" },
];
const BIG_TECH_SITES = [
{ name: "Nvidia", url: "https://www.nvidia.com/" },
{ name: "Apple", url: "https://www.apple.com/" },
{ name: "Microsoft", url: "https://www.microsoft.com/" },
{ name: "Alphabet", url: "https://about.google/" },
// The same company's search page, listed separately. It is the most
// requested page on the web and one of the lightest, which makes it
// the useful end of every chart here — and nothing about the holding
// company's own site would tell you that.
{ name: "Google", url: "https://www.google.com/" },
{ name: "Amazon", url: "https://www.amazon.com/" },
{ name: "Meta", url: "https://www.meta.com/" },
{ name: "Broadcom", url: "https://www.broadcom.com/" },
{ name: "TSMC", url: "https://www.tsmc.com/english" },
{ name: "Tesla", url: "https://www.tesla.com/" },
{ name: "Oracle", url: "https://www.oracle.com/" },
{ name: "Netflix", url: "https://www.netflix.com/" },
{ name: "SAP", url: "https://www.sap.com/" },
{ name: "Salesforce", url: "https://www.salesforce.com/" },
{ name: "AMD", url: "https://www.amd.com/" },
{ name: "Adobe", url: "https://www.adobe.com/" },
{ name: "Cisco", url: "https://www.cisco.com/" },
{ name: "IBM", url: "https://www.ibm.com/" },
{ name: "Qualcomm", url: "https://www.qualcomm.com/" },
{ name: "Intel", url: "https://www.intel.com/" },
{ name: "Texas Instruments", url: "https://www.ti.com/" },
{ name: "Samsung", url: "https://www.samsung.com/" },
{ name: "Sony", url: "https://www.sony.com/" },
{ name: "ServiceNow", url: "https://www.servicenow.com/" },
{ name: "Uber", url: "https://www.uber.com/" },
{ name: "Palantir", url: "https://www.palantir.com/" },
];
/*
* UI frameworks, which none of the four lists above is the right home for: not
* generators, not runners, not registries, not hosts. They exist so Developer
* Tooling can carry them — the category is about the people who build the tools,
* and these are the tools most of the sites in it are built with.
*/
const FRAMEWORK_SITES = [
{ name: "React", url: "https://react.dev/" },
{ name: "Vue", url: "https://vuejs.org/" },
{ name: "Angular", url: "https://angular.dev/" },
{ name: "Preact", url: "https://preactjs.com/" },
{ name: "Lit", url: "https://lit.dev/" },
// In both this list and SSG_SITES, deliberately: svelte.dev documents Svelte
// and SvelteKit together, so the one page is a framework's home and a site
// generator's. loadConfig dedupes by URL, so the site is measured once and
// simply belongs to both categories.
{ name: "Svelte", url: "https://svelte.dev/" },
{ name: "SolidJS", url: "https://www.solidjs.com/" },
{ name: "Qwik", url: "https://qwik.dev/" },
{ name: "React Router", url: "https://reactrouter.com/" },
{ name: "TanStack", url: "https://tanstack.com/" },
// The no-build-step position, which is the argument this project's readers
// are most likely to be having. Small by download count and loud by
// influence — worth measuring for what their own pages do.
{ name: "Alpine.js", url: "https://alpinejs.dev/" },
{ name: "htmx", url: "https://htmx.org/" },
// The older guard. Both are past the point where anyone surveys them, and
// both are still on a great many more pages than that suggests.
{ name: "Ember", url: "https://emberjs.com/" },
{ name: "jQuery", url: "https://jquery.com/" },
];
/*
* Package managers whose home page is documentation rather than a registry.
*
* They are commented out of PACKAGE_MANAGER_SITES on purpose — that category is
* package *browsers*, and a docs site beside a search UI compares two different
* jobs. Here the question is only whether the tool has one, so they belong.
*/
const PACKAGE_TOOL_SITES = [
{ name: "pnpm", url: "https://pnpm.io/" },
// vlt.sh redirects to vlt.io; pinned to the destination.
{ name: "vlt", url: "https://www.vlt.io/" },
{ name: "jspm", url: "https://jspm.org/" },
];
/*
* What compiles and bundles the rest. Not frameworks and not test runners, but
* the same kind of thing as both: a tool whose home page is its shop window.
*/
const BUILD_TOOL_SITES = [
{ name: "TypeScript", url: "https://www.typescriptlang.org/" },
{ name: "Vite", url: "https://vite.dev/" },
{ name: "esbuild", url: "https://esbuild.github.io/" },
{ name: "webpack", url: "https://webpack.js.org/" },
];
export default {
// Read by lib/config.js, which marks the matching sites as it flattens the
// groups — one place rather than a filter on each category's list.
archived: ARCHIVED,
// Exempt from the generator rules, same mechanism. A site entry can also
// carry `pinGroup: true` directly, for the hand-written categories; this list
// is for the ones whose entries come from a generated file.
pinned: PINNED,
// Lighthouse runs per URL per measurement. The median run is kept.
// 3 is a decent tradeoff between noise and wall-clock time.
runs: 3,
// Whether to keep Lighthouse's loading filmstrip: "filmstrip" or "none",
// overridable per category and per site.
//
// These files are committed, since measure and publish are separate
// workflows on separate checkouts and the repository is the only channel
// between them. A deduplicated strip measured 19–84 KB per site, so at this
// corpus size expect roughly 75 MB in the tree, rewritten weekly as sites
// are re-measured. Set a category to "none" to keep it out.
screenshots: "filmstrip",
// "mobile" (throttled, Moto G Power emulation) or "desktop"
formFactor: "mobile",
// Skip a URL if it was already measured within this many hours.
// Set to 0 to always re-measure.
freshnessHours: 24 * 7,
// How many sites a single `speedlify measure` will do before stopping.
// Measurement is a rolling process: each run spends this budget on whichever
// sites are most out of date, so you size this to the time you're willing to
// give one invocation, not to the length of the site list.
//
// With ~2,300 sites this must not be null — an unbounded run would try the
// whole list and take about eleven hours. At 40, a run is roughly 12 minutes,
// and four shards every two hours cover every site in a bit over a day.
batchSize: 40,
// Data older than this is shown as stale in the report and in `speedlify list`.
staleAfterHours: 24 * 7 * 2,
// How many recent points the report charts. Since the build reads the compact
// series.json rather than the raw records, this is a readability choice
// rather than a performance guard — set it to null to chart everything.
historyLimit: 120,
// How long a site whose last attempt errored waits before being tried again.
// An error is not data, so a failure never earns the site a freshness window
// — this is the only thing holding it back. 0 means the very next run.
//
// Overridable per category, same as freshnessHours: a handful of framework
// home pages can retry immediately while thousands of personal sites wait.
retryErrorsAfterHours: 6,
// Escalate that wait exponentially with each consecutive failure — 1h, 2, 4,
// 8, 16, capped at 24 — so a URL that has been dead for a week stops
// consuming a slot in every batch. The two compose: the wait is whichever of
// the curve and retryErrorsAfterHours is longer.
failureBackoff: true,
// A redirect must lead to the same destination on this many consecutive
// successful measurements before it is treated as a real site move. Guards
// against A/B tests, geo splits and maintenance pages, which all look like
// redirects for a run or two.
redirectConfirmations: 3,
// Any category can be switched off with `enabled: false`. It stops being
// measured and stops appearing on the site, but nothing is deleted — its
// stored history stays put and comes straight back when it is re-enabled.
//
// A site listed in a disabled category and nowhere else drops out of the
// corpus entirely, so its history will read as orphaned meanwhile.
/*
* Addresses that used to mean something, and where they go now.
*
* Two kinds, both for the same reason: a URL people linked to and search
* engines remember should not become a 404.
*
* The bare paths are the original Speedlify's scheme, where a category was
* served from the root — `/ssg/` rather than `/group/ssg/`. This instance
* replaced that site at the same domain, so those links are still out there.
*
* Destinations are checked at report time against the categories and sites
* that actually exist — see lib/report.js — so an entry pointing at something
* since renamed is dropped rather than published as a link to nowhere.
*/
redirects: {
"/ssg/": "/group/ssg/",
"/test-runners/": "/group/test-runners/",
"/zachleat.com/": "/group/zachleat/",
"/sample/": "/site/www-speedlify-dev/",
},
groups: {
/*
* Site Generators, Test Runners, Package Managers and Web Hosts, in one
* place.
*
* An additional view rather than a replacement: every site here is still
* on its own category page, and still ranked there against its own kind.
* What this adds is the comparison those pages cannot make — a generator's
* site against the host it deploys to against the registry it publishes
* to, all built by people who do this for a living.
*
* Listed first, which makes it every member's primary category: `groups`
* is walked in order and the first match wins. So a hosting company reads
* "Developer Tooling" on the leaderboard, with "Web Hosts" as the second
* chip — the general label first, the specific one after it.
*
* The sites are the same objects the four use, so a URL added to one of
* them appears here on the next build without a second edit.
*/
/*
* Ahead of Developer Tooling, which also carries these sites. `groups` is
* walked in order and the first match wins, so this is what makes "AI" the
* chip a reader sees first — Developer Tooling reads oddly as the primary
* category for a model lab, however true it is of Cursor and Ollama.
*/
ai: {
/*
* `showHealth: true` puts a Good/Warn/Bad badge beside the category's
* name, from the share of its measured sites showing no red ring — the
* same question the leaderboard's first ranking step asks of one site.
*
* Opt-in on every category here, but a flag rather than a default: it
* is a verdict on sites other people maintain, and a category should
* ask for one. Omit it and the badge simply does not render.
*/
showHealth: true,
name: "AI",
enabled: true,
description:
"Model labs, coding agents, and the infrastructure that serves them.",
...DAILY,
/*
* Polite, like Big Tech: these are commercial sites that did not ask to
* be measured, and several of them are already behind bot protection.
*/
...POLITE,
sites: AI_SITES,
},
"dev-tooling": {
showHealth: true,
showOnHomePageCategoryList: true,
name: "Developer Tooling",
enabled: true,
description:
"Site generators, test runners, package managers, web hosts and AI together.",
...DAILY,
sites: [
...SSG_SITES,
...TEST_RUNNER_SITES,
...PACKAGE_MANAGER_SITES,
...HOST_SITES,
...FRAMEWORK_SITES,
...BUILD_TOOL_SITES,
...PACKAGE_TOOL_SITES,
...AI_SITES,
// This instance, in its own leaderboard. Not in any of the lists
// above because there is no list of one — and measuring the thing
// that does the measuring is the least it can do.
{ name: "Speedlify", url: "https://www.speedlify.dev/" },
],
},
/*
* The other end of the corpus.
*
* Every other category here is people who build for the web: generators,
* hosts, starters, personal sites. These are the companies with the most
* money and the largest engineering organizations on earth, measured on
* exactly the same terms — same runner, same throttling, same thresholds.
*
*/
"big-tech": {
showHealth: true,
showOnHomePageCategoryList: true,
name: "Big Tech",
enabled: true,
description:
"The largest technology companies by market capitalization.",
...DAILY,
...POLITE,
sites: BIG_TECH_SITES,
},
ssg: {
showHealth: true,
showOnHomePageCategoryList: true,
name: "Site Generators",
enabled: true,
description: "Popular site generators and frameworks.",
...DAILY,
// The list from https://www.speedlify.dev/ssg/, in its order.
sites: SSG_SITES,
},
frameworks: {
showHealth: true,
showOnHomePageCategoryList: true,
name: "Frameworks and Libraries",
enabled: true,
description:
"Tools that developers use to build the frontends of websites.",
...DAILY,
sites: FRAMEWORK_SITES,
},
"test-runners": {
showHealth: true,
name: "Test Runners",
enabled: true,
description: "Test runners, linters, and other testing tools.",
...DAILY,
sites: TEST_RUNNER_SITES,
},
"package-managers": {
showHealth: true,
name: "Package Managers",
enabled: true,
description:
"Package browsers: the searchable front ends to package registries.",
...DAILY,
sites: PACKAGE_MANAGER_SITES,
},
hosts: {
showHealth: true,
showOnHomePageCategoryList: true,
name: "Web Hosts",
enabled: true,
description:
"Web hosting and deployment platforms.",
...DAILY,
sites: HOST_SITES,
},
builders: {
showHealth: true,
showOnHomePageCategoryList: true,
name: "Website Builders",
enabled: true,
description:
"Hosted tools that help nontechnical users build and serve websites.",
...DAILY,
sites: [
{ name: "WordPress.com", url: "https://wordpress.com/" },
{ name: "Shopify", url: "https://www.shopify.com/" },
{ name: "Wix", url: "https://www.wix.com/" },
{ name: "Squarespace", url: "https://www.squarespace.com/" },
{ name: "Tilda", url: "https://tilda.cc/" },
{ name: "Webflow", url: "https://webflow.com/" },
{ name: "Duda", url: "https://www.duda.co/" },
{ name: "Framer", url: "https://www.framer.com/" },
{ name: "Nordcraft", url: "https://nordcraft.com/" },
],
},
zachleat: {
showHealth: true,
// Your own pages, so there is nobody else to ask. Same rule as the two
// community registers below.
showEmbed: true,
name: "zachleat",
enabled: true,
description: "Pages from zachleat.com, and the other sites Zach maintains.",
...WEEKLY,
...POLITE,
sites: [
{ url: "https://www.zachleat.com/" },
{ url: "https://www.zachleat.com/about/" },
{ url: "https://www.zachleat.com/resume/" },
{ url: "https://www.zachleat.com/twitter/" },
{ url: "https://www.zachleat.com/web/" },
{ url: "https://www.zachleat.com/web/eleventy/" },
{ url: "https://www.zachleat.com/web/fonts/" },
{ url: "https://www.zachleat.com/web/speedlify/" },
{ url: "https://www.zachleat.com/web/lighthouse-in-footer/" },
{ url: "https://www.zachleat.com/web/comprehensive-webfonts/" },
{ url: "https://www.zachleat.com/web/google-fonts-display/" },
/*
* Not on zachleat.com, but the same person answers for them.
*
* Each is already measured under another category — 11ty.dev and
* speedlify.dev in Developer Tooling, the Font Awesome blog in Built
* Awesome — so this adds a membership rather than a site, and the
* cadence stays whatever the strictest of those categories asks for.
*/
{ url: "https://www.11ty.dev/" },
{ url: "https://www.speedlify.dev/" },
{ url: "https://blog.fontawesome.com/" },
{ url: "https://esif.dev/" },
{ url: "https://nejsconf.com/" },
],
},
"11ty-community": {
showHealth: true,
showOnHomePageCategoryList: true,
// These are sites people submitted themselves, so offering a live badge
// for one is offering it to whoever put it here. See `showEmbed` in the
// README: the section is off everywhere it is not asked for.
showEmbed: true,
name: "Built Awesome",
enabled: true,
description:
'Community-submitted sites from the <a href="https://github.com/11ty/11ty-community">11ty/11ty-community</a> repository.',
// Where someone adds a site to this category, and what that act is
// called. The list is imported from an issue template upstream, so the
// answer is a form rather than a pull request against this repository —
// which is not something a reader could guess from the description.
//
// Categories without this get no call to action: for the hand-written
// ones the answer really is "open a PR", and a button saying so is a
// worse version of the repository link in the footer.
submitUrl: "https://github.com/11ty/11ty-community/issues/new?template=built-with-eleventy.yml",
submitLabel: "Add your site",
...POLITE,
...WEEKLY,
// for storage reasons
screenshots: "none",
// The list is a record of what was submitted, not of what is true now.
// A site measured as something other than Eleventy has moved on, so it
// moves to the category below rather than inflating this one.
//
// Matched on the detected generator's id, and only when there is one:
// no generator tag is the normal case for a static site and proves
// nothing either way, so those stay put. Both names for the same
// project qualify — "Build Awesome" is Eleventy's newer branding and
// reports under its own id, so it has to be listed explicitly.
requireGenerator: ["eleventy", "build-awesome"],
emeritusGroup: "11ty-emeritus",
// Most static sites emit no generator tag at all, so a blank is the
// common case here rather than a suspicious one. Being on this list is
// itself a claim about what built the site — shown faded, because it is
// what someone submitted rather than what we measured.
presumedGenerator: "build-awesome",
// Generated by scripts/import-11ty-community.mjs — re-run it to refresh.
//
// Two lists are filtered out rather than deleted from the generated
// file, which a re-import would only undo: starters, and anything
// hand-listed as emeritus. Adding a URL to config/11ty-emeritus.json is
// therefore the whole of moving it — it appears there and leaves here.
//
// The automatic path still works alongside this: a site measured as a
// different generator is moved by requireGenerator below, no editing
// needed. This is for the cases measurement cannot see.
sites: [...new Set([...community.urls, ...EXTRA_COMMUNITY])]
.filter((url) => !STARTERS.includes(url) && !EMERITUS.includes(url))
.map((url) => ({ url })),
},
"11ty-starters": {
showHealth: true,
showOnHomePageCategoryList: true,
// These are sites people submitted themselves, so offering a live badge
// for one is offering it to whoever put it here. See `showEmbed` in the
// README: the section is off everywhere it is not asked for.
showEmbed: true,
name: "Build Awesome Starters",
description:
"Starter projects and templates built with Build Awesome and Eleventy.",
// A directory of data files in 11ty/docs rather than an issue form:
// this list is imported from there, so a starter is added by adding a
// file upstream. See EXTRA_STARTERS above for the ones we carry that
// upstream does not.
submitUrl: "https://github.com/11ty/docs/tree/main/src/_data/starters",
submitLabel: "Add a starter",
...POLITE,
...WEEKLY,
// for storage reasons
// A starter's whole point is what it renders, and half of these ship
// nothing without scripts — so this category keeps its pictures, both
// the filmstrip and the pair behind the Without JavaScript section.
screenshots: "filmstrip",
// The ones that came from the community list were filtered out of it,
// not missing from it. Without this they would be flagged "unlisted"
// — told to submit something that is already submitted.
listedIn: "11ty-community",
// Kept out of the home page's Perfect Scores board. A starter is a
// near-empty demonstration page, so a perfect score there is the
// expected result rather than an achievement, and enough of them would
// crowd out the real sites the board exists to show. They still rank
// normally on their own category page and in the global leaderboard.
excludeFromPerfectScores: true,
// Same reasoning as the community list above: a starter emits no
// generator tag any more often than a finished site does, and being on
// this list is itself a claim about what built it. Shown faded, because
// it is what the docs say rather than what we measured.
//
// No requireGenerator to go with it, unlike the community list. A
// starter that stops being Eleventy stops being an Eleventy starter and
// gets removed upstream; there is no "used to be a starter" category for
// it to age into.
presumedGenerator: "build-awesome",
// Generated by scripts/import-11ty-starters.mjs — re-run it to refresh.
sites: STARTERS.map((url) => ({ url })),
},
"11ty-emeritus": {
showHealth: true,
name: "11ty Emeritus",
// Not a competition. These sites left, and ranking what they scored on
// the way out invites a reading nobody intended — that the list is a
// wall of shame, or that the site at the top is the best of a bad lot.
// The page still measures them; it just does not place them.
ranked: false,
// And no "Biggest movers" panel, for the same reason: a site that has
// moved on is not a contestant whose form is worth remarking on.
movers: false,
enabled: true,
description:
"Sites that were previously built with Build Awesome or Eleventy but have migrated to use something else (since April 2018).",
...POLITE,
...WEEKLY,
// for storage reasons
screenshots: "none",
// Two ways in. Sites listed here were deleted from the community repo
// upstream — recovered from its git history, since a deletion removes
// the URL entirely and the generator rule never gets to see it.
//
// The rest arrive at report time: anything still on the community list
// that measures as a different generator is moved here automatically.
// The category is defined by what a site *used* to be built with, so one
// measuring as Eleventy again does not belong here — it has come back,
// and saying otherwise would state the opposite of the truth. The
// mirror image of requireGenerator above.
//
// As there, an undetected generator decides nothing: most static sites
// emit no tag, and absence is not evidence of a return.
rejectGenerator: ["eleventy", "build-awesome"],
rejectGroup: "11ty-community",
sites: EMERITUS.map((url) => ({ url })),
},
},
};