├── .eleventy.js
├── .eleventyignore
├── .gitignore
├── .nvmrc
├── LICENSE
├── README.md
├── _data
├── isolatedKeys.js
├── lastruns.js
├── lighthouseMaximums.js
├── sites
│ ├── sample.js
│ ├── ssg.js
│ ├── test-runners.js
│ └── zachleat.js
└── urlsForApi.js
├── _includes
└── layout.njk
├── api-urls.11ty.js
├── api.11ty.js
├── assets
├── chart.js
├── css.njk
├── islands.js
├── script.njk
├── sparkline.css
├── style.css
└── timestamp-ago.js
├── categories.njk
├── index.njk
├── netlify.toml
├── package.json
├── plugins
└── keep-data-cache
│ ├── index.js
│ └── manifest.yml
├── run-tests.js
├── utils
├── calc.js
├── getObjectKey.js
└── sparkline.js
└── zip-results.js
/.eleventy.js:
--------------------------------------------------------------------------------
1 | const byteSize = require("byte-size");
2 | const shortHash = require("short-hash");
3 | const lodash = require("lodash");
4 | const getObjectKey = require("./utils/getObjectKey.js");
5 | const calc = require("./utils/calc.js");
6 | const Sparkline = require('./utils/sparkline.js');
7 |
8 | function isUrlMatch(haystackUrls, needleUrl) {
9 | if(!Array.isArray(haystackUrls) || haystackUrls.length === 0) {
10 | return false;
11 | }
12 |
13 | if(needleUrl && typeof needleUrl === "string") {
14 | // TODO lowercase just the origins
15 | needleUrl = needleUrl.toLowerCase();
16 | if(haystackUrls.indexOf(needleUrl) > -1 || needleUrl.endsWith("/") && haystackUrls.indexOf(needleUrl.substr(0, needleUrl.length - 1)) > -1) {
17 | return true;
18 | }
19 | }
20 | return false;
21 | }
22 |
23 | function hasUrl(urls, { url, requestedUrl }, skipUrls = []) {
24 | // urls comes from sites[vertical].urls, all requestedUrls (may not include trailing slash)
25 |
26 | // TODO lowercase just the origins
27 | let lowercaseUrls = urls.map(targetUrl => targetUrl.toLowerCase());
28 |
29 | if(isUrlMatch(skipUrls, requestedUrl) || isUrlMatch(skipUrls, url)) {
30 | return false;
31 | }
32 |
33 | // Requested url matches?
34 | if(isUrlMatch(lowercaseUrls, requestedUrl)) {
35 | return true;
36 | }
37 | if(isUrlMatch(lowercaseUrls, url)) {
38 | return true;
39 | }
40 |
41 | return false;
42 | }
43 |
44 | function showDigits(num, digits = 2) {
45 | let toNum = parseFloat(num);
46 | let afterFixed = toNum.toFixed(digits);
47 | return afterFixed;
48 | }
49 |
50 | function pad(num) {
51 | return (num < 10 ? "0" : "") + num;
52 | }
53 |
54 | function mapProp(prop, targetObj) {
55 | if(Array.isArray(prop)) {
56 | let otherprops = [];
57 | prop = prop.map(entry => {
58 | // TODO this only works as the first entry
59 | if(entry === ":newest") {
60 | entry = Object.keys(targetObj).sort().pop();
61 | } else if(entry.indexOf("||") > -1) {
62 | for(let key of entry.split("||")) {
63 | if(lodash.get(targetObj, [...otherprops, key])) {
64 | entry = key;
65 | break;
66 | }
67 | }
68 | }
69 | otherprops.push(entry);
70 |
71 | return entry;
72 | });
73 | }
74 |
75 | return prop;
76 | }
77 |
78 | function getLighthouseTotal(entry) {
79 | return entry.lighthouse.performance * 100 +
80 | entry.lighthouse.accessibility * 100 +
81 | entry.lighthouse.bestPractices * 100 +
82 | entry.lighthouse.seo * 100;
83 | }
84 |
85 | module.exports = function(eleventyConfig) {
86 | eleventyConfig.addFilter("shortHash", shortHash);
87 | eleventyConfig.setServerOptions({
88 | domDiff: false
89 | });
90 |
91 | eleventyConfig.addFilter("repeat", function(str, times) {
92 | let result = '';
93 |
94 | for (let i = 0; i < times; i++) {
95 | result += str;
96 | }
97 |
98 | return result;
99 | });
100 |
101 | // first ${num} entries (and the last entry too)
102 | eleventyConfig.addFilter("headAndLast", function(arr, num) {
103 | if(num && num < arr.length) {
104 | let newArr = arr.slice(0, num);
105 | newArr.push(arr[arr.length - 1]);
106 | return newArr;
107 | }
108 | return arr;
109 | });
110 |
111 | eleventyConfig.addFilter("displayUrl", function(url, keepWww = false) {
112 | if(!keepWww) {
113 | url = url.replace("https://www.", "");
114 | }
115 | url = url.replace("https://", "");
116 | if(url.endsWith("/index.html")) {
117 | url = url.replace("/index.html", "/");
118 | }
119 | return url;
120 | });
121 |
122 | eleventyConfig.addFilter("showDigits", function(num, digits) {
123 | return showDigits(num, digits);
124 | });
125 |
126 | eleventyConfig.addFilter("displayTime", function(time) {
127 | let num = parseFloat(time);
128 | if(num > 850) {
129 | return `${showDigits(num / 1000, 2)}s`;
130 | }
131 | return `${showDigits(num, 0)}ms`;
132 | });
133 |
134 | eleventyConfig.addFilter("displayFilesize", function(size) {
135 | let normalizedSize = byteSize(size, { units: 'iec', precision: 0 });
136 | let unit = normalizedSize.unit;
137 | let value = normalizedSize.value;
138 | return `${value}${unit.substr(0,1)} ${unit}`;
139 | });
140 |
141 | eleventyConfig.addFilter("displayDate", function(timestamp) {
142 | let months = ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
143 | let date = new Date(timestamp);
144 | let day = `${months[date.getMonth()]} ${pad(date.getDate())}`;
145 | return `${day} ${pad(date.getHours())}:${pad(date.getMinutes())}`;
146 | });
147 |
148 | eleventyConfig.addFilter("sortCumulativeScore", (obj) => {
149 | return obj.sort((a, b) => {
150 |
151 | let newestKeyA = Object.keys(a).sort().pop();
152 | let newestKeyB = Object.keys(b).sort().pop();
153 |
154 | // Lighthouse error
155 | // e.g. { url: 'https://mangoweb.net/', error: 'Unknown error.' }
156 | if(b[newestKeyB].error && a[newestKeyA].error) {
157 | return 0;
158 | } else if(b[newestKeyB].error) {
159 | return -1;
160 | } else if(a[newestKeyA].error) {
161 | return 1;
162 | }
163 |
164 | // lower is better
165 | return a[newestKeyA].ranks.cumulative - b[newestKeyB].ranks.cumulative;
166 | });
167 | });
168 |
169 | // Works with arrays too
170 | // Sort an object that has `order` props in values.
171 | // If prop is not passed in, sorts by object keys
172 | // Returns an array
173 | eleventyConfig.addFilter("sort", (obj, prop = "___key") => {
174 | let arr;
175 | let defaultKey = "___key";
176 | if(Array.isArray(obj)) {
177 | arr = obj;
178 | } else {
179 | arr = [];
180 |
181 | for(let key in obj) {
182 | if(prop === defaultKey) {
183 | obj[key][defaultKey] = key;
184 | }
185 | arr.push(obj[key]);
186 | }
187 | }
188 |
189 | let sorted = arr.sort((a, b) => {
190 | let aVal = lodash.get(a, mapProp(prop, a));
191 | let bVal = lodash.get(b, mapProp(prop, b));
192 | if(aVal > bVal) {
193 | return -1;
194 | }
195 | if(aVal < bVal) {
196 | return 1;
197 | }
198 | return 0;
199 | });
200 |
201 | if(!Array.isArray(obj)) {
202 | if(prop === defaultKey) {
203 | for(let entry of sorted) {
204 | delete entry[defaultKey];
205 | }
206 | }
207 | }
208 |
209 | return sorted;
210 | });
211 |
212 | eleventyConfig.addFilter("getObjectKey", getObjectKey);
213 |
214 | function filterResultsToUrls(obj, urls = [], skipKeys = [], skipUrls = []) {
215 | let arr = [];
216 | for(let key in obj) {
217 | if(skipKeys.indexOf(key) > -1) {
218 | continue;
219 | }
220 |
221 | let result;
222 | let newestFilename = Object.keys(obj[key]).sort().pop();
223 | result = obj[key][newestFilename];
224 | // urls comes from sites[vertical].urls, all requestedUrls (may not include trailing slash)
225 | if(urls === true || result && hasUrl(urls, result, skipUrls)) {
226 | arr.push(obj[key]);
227 | }
228 | }
229 | return arr;
230 | }
231 |
232 | eleventyConfig.addFilter("getSites", (results, sites, vertical, skipKeys = []) => {
233 | let urls = sites[vertical].urls;
234 | let skipUrls = sites[vertical].skipUrls;
235 | let isIsolated = sites[vertical].options && sites[vertical].options.isolated === true;
236 | let prunedResults = isIsolated ? results[vertical] : results;
237 | return filterResultsToUrls(prunedResults, urls, skipKeys, skipUrls);
238 | });
239 |
240 | // Deprecated, use `getSites` instead, it works with isolated categories
241 | eleventyConfig.addFilter("filterToUrls", filterResultsToUrls);
242 |
243 | eleventyConfig.addFilter("hundoCount", (entry) => {
244 | let count = 0;
245 | if(entry.lighthouse.performance === 1) {
246 | count++;
247 | }
248 | if(entry.lighthouse.accessibility === 1) {
249 | count++;
250 | }
251 | if(entry.lighthouse.bestPractices === 1) {
252 | count++;
253 | }
254 | if(entry.lighthouse.seo === 1) {
255 | count++;
256 | }
257 |
258 | return count;
259 | });
260 |
261 | eleventyConfig.addFilter("notGreenCircleCount", (entry) => {
262 | let count = 0;
263 | if(entry.lighthouse.performance < .9) {
264 | count++;
265 | }
266 | if(entry.lighthouse.accessibility < .9) {
267 | count++;
268 | }
269 | if(entry.lighthouse.bestPractices < .9) {
270 | count++;
271 | }
272 | if(entry.lighthouse.seo < .9) {
273 | count++;
274 | }
275 |
276 | return count;
277 | });
278 |
279 | eleventyConfig.addFilter("hundoCountTotals", (counts, entry) => {
280 | if(!entry.error && !isNaN(entry.lighthouse.performance)) {
281 | counts.total++;
282 | }
283 |
284 | if(entry.lighthouse.performance === 1) {
285 | counts.performance++;
286 | }
287 | if(entry.lighthouse.accessibility === 1) {
288 | counts.accessibility++;
289 | }
290 | if(entry.lighthouse.bestPractices === 1) {
291 | counts.bestPractices++;
292 | }
293 | if(entry.lighthouse.seo === 1) {
294 | counts.seo++;
295 | }
296 |
297 | if(entry.lighthouse.performance === 1 && entry.lighthouse.accessibility === 1 && entry.lighthouse.bestPractices === 1 && entry.lighthouse.seo === 1) {
298 | counts.perfect++;
299 | }
300 |
301 | return counts;
302 | });
303 |
304 | eleventyConfig.addFilter("lighthouseTotal", getLighthouseTotal);
305 |
306 | eleventyConfig.addFilter("addLighthouseTotals", (arr) => {
307 | /* special case */
308 | for(let obj of arr) {
309 | for(let entry in obj) {
310 | if(obj[entry].lighthouse) {
311 | obj[entry].lighthouse[":lhtotal"] = getLighthouseTotal(obj[entry]);
312 | }
313 | }
314 | }
315 | return arr;
316 | });
317 |
318 | eleventyConfig.addFilter("toJSON", function(obj) {
319 | return JSON.stringify(obj);
320 | });
321 |
322 | eleventyConfig.addFilter("calc", calc);
323 |
324 | eleventyConfig.addFilter("generatorImageUrl", (url) => {
325 | return `https://v1.generator.11ty.dev/image/${encodeURIComponent(url)}/`;
326 | });
327 |
328 | eleventyConfig.addFilter("hostingImageUrl", (url) => {
329 | // return `https://v1--eleventy-api-built-with.netlify.app/${encodeURIComponent(url)}/image/host/`;
330 | return `https://v1.builtwith.11ty.dev/${encodeURIComponent(url)}/image/host/`;
331 | });
332 |
333 | eleventyConfig.addPairedShortcode("starterMessage", (htmlContent) => {
334 | if(process.env.SITE_NAME !== "speedlify") {
335 | return htmlContent;
336 | }
337 | return "";
338 | });
339 |
340 | // Assets
341 | eleventyConfig.addPassthroughCopy({
342 | "./node_modules/chartist/dist/chartist.js": "chartist.js",
343 | "./node_modules/chartist/dist/chartist.css.map": "chartist.css.map",
344 | });
345 |
346 | eleventyConfig.addWatchTarget("./assets/");
347 |
348 | eleventyConfig.setBrowserSyncConfig({
349 | ui: false,
350 | ghostMode: false
351 | });
352 | eleventyConfig.addShortcode('lighthouseSparkline', (site) => {
353 | const timeSeries = Object.values(site).sort(
354 | (a, b) => a.timestamp - b.timestamp
355 | );
356 | const values = timeSeries.map((run) => run.lighthouse?.total || 0);
357 | return Sparkline({
358 | // red-orange-green gradient similar to usage in
359 | gradient: [
360 | { color: '#ff4e42', offset: '0%' },
361 | { color: '#ff4e42', offset: '30%' },
362 | { color: '#ffa400', offset: '70%' },
363 | { color: '#ffa400', offset: '85%' },
364 | { color: '#0cce6b', offset: '95%' },
365 | { color: '#0cce6b', offset: '100%' },
366 | ],
367 | values,
368 | min: 0,
369 | max: 400,
370 | timeSeries,
371 | });
372 | });
373 |
374 | eleventyConfig.addShortcode('weightSparkline', (site) => {
375 | const timeSeries = Object.values(site).sort(
376 | (a, b) => a.timestamp - b.timestamp
377 | );
378 | const values = timeSeries.map((run) => run.weight?.total || 0);
379 | return Sparkline({
380 | color: '#d151ff',
381 | values,
382 | min: 0,
383 | timeSeries,
384 | // Display raw bytes as pretty values on y axis, e.g. 49244 => 48K
385 | formatAxis: (num) => {
386 | const { value, unit } = byteSize(num, { units: 'iec', precision: 0 });
387 | return value === '0' ? value : value + unit.slice(0, 1);
388 | },
389 | });
390 | });
391 |
392 | eleventyConfig.setWatchJavaScriptDependencies(false);
393 | };
394 |
--------------------------------------------------------------------------------
/.eleventyignore:
--------------------------------------------------------------------------------
1 | README.md
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | node_modules
2 | .log
3 | _site
4 | _data/results/*
5 | _data/results-last-runs.json
6 | package-lock.json
7 | .env
8 | .cache
--------------------------------------------------------------------------------
/.nvmrc:
--------------------------------------------------------------------------------
1 | 18
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2020 Zach Leatherman @zachleat
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # speedlify
2 |
3 | After you make a fast web site, keep it fast by measuring it over time. Read [Use Speedlify to Continuously Measure Site Performance](https://www.zachleat.com/web/speedlify/). Created by [@zachleat](https://www.zachleat.com/).
4 |
5 | * Requires Node 12+
6 | * Each file in `_data/sites/*.js` is a category and contains a list of sites for comparison.
7 |
8 | ## Run locally
9 |
10 | _After cloning you’ll probably want to delete the initial `_data/sites/*.js` files and create your own file with a list of your own site URLs!_
11 |
12 | ```
13 | npm install
14 | npm run test-pages
15 | npm run start
16 | ```
17 |
18 | ## Related
19 |
20 | * [The Eleventy Leaderboards](https://www.zachleat.com/web/eleventy-leaderboard-speedlify/) are running on Speedlify
21 | * [speedlify.dev](https://www.speedlify.dev/) shows some sample categories
22 | * Use the [`` component](https://github.com/zachleat/speedlify-score) to show your scores on your page. Read more at [I added Lighthouse Scores to my Site’s Footer and You Can Too](https://www.zachleat.com/web/lighthouse-in-footer/)
23 | * The [Eleventy Starter Projects list](https://www.11ty.dev/docs/starter/) shows Lighthouse scores from Speedlify. Read more at [The Lighthouse Scores Will Continue Until Morale Improves](https://www.zachleat.com/web/11ty-lighthouse/).
24 |
25 | ## Deploy to Netlify
26 |
27 | Can run directly on Netlify (including your tests) and will save the results to a Netlify build cache (via Netlify Build Plugins, see `plugins/keep-data-cache/`).
28 |
29 | _After cloning you’ll probably want to delete the initial `_data/sites/*.js` files and create your own file with a list of your own site URLs!_
30 |
31 |
32 |
33 | Speedlify will also save your data to `/results.zip` so that you can download later. Though this has proved to be unnecessary so far, it does serve as a fallback backup mechanism in case the Netlify cache is lost. Just look up your previous build URL and download the data to restore.
34 |
35 | [](https://app.netlify.com/sites/speedlify/deploys)
36 |
37 | * **Run every day or week**: You can set Speedlify to run at a specified interval using a Netlify Build Hook, read more on the Eleventy docs: [Quick Tip #008—Trigger a Netlify Build Every Day with IFTTT](https://www.11ty.dev/docs/quicktips/netlify-ifttt/).
38 |
39 | ## Known Limitations
40 |
41 | * If you change a URL to remove a redirect (to remove or add a `www.`, moved domains, etc), you probably want to delete the old URL’s data otherwise you’ll have two entries in the results list.
42 | * When running on Netlify, a single category has a max limit on the number of sites it can test, upper bound on how many tests it can complete in the 15 minute Netlify build limit.
43 | * The same URL cannot be listed in two different categories (yet).
44 |
45 | ## Pay for something better
46 |
47 | Speedlify is intended as a stepping stone to more robust performance monitoring solutions like:
48 |
49 | * [SpeedCurve](https://speedcurve.com/)
50 | * [Calibre](https://calibreapp.com/)
51 | * [DebugBear](https://www.debugbear.com/)
--------------------------------------------------------------------------------
/_data/isolatedKeys.js:
--------------------------------------------------------------------------------
1 | const shortHash = require("short-hash");
2 | const fastglob = require("fast-glob");
3 |
4 | module.exports = async function() {
5 | let categories = await fastglob("./_data/sites/*.js", {
6 | caseSensitiveMatch: false
7 | });
8 |
9 | let isolated = new Set();
10 | for(let file of categories) {
11 | let category = file.split("/").pop().replace(/\.js$/, "");
12 | let categoryData = require(`./sites/${category}.js`);
13 | if(typeof categoryData === "function") {
14 | categoryData = await categoryData();
15 | }
16 |
17 | if(categoryData.options && categoryData.options.isolated) {
18 | isolated.add(category);
19 | }
20 | }
21 |
22 | return Array.from(isolated);
23 | };
--------------------------------------------------------------------------------
/_data/lastruns.js:
--------------------------------------------------------------------------------
1 | // Nunjucks workaround for dashes in global `results-last-runs.json` variable name
2 |
3 | let lastruns = {};
4 |
5 | try {
6 | lastruns = require("./results-last-runs.json");
7 | } catch(e) {
8 | }
9 |
10 | module.exports = lastruns;
--------------------------------------------------------------------------------
/_data/lighthouseMaximums.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | average: {
3 | fcp: 4000,
4 | si: 5800,
5 | lcp: 4000,
6 | tti: 7300,
7 | tbt: 600,
8 | cls: 0.25,
9 | },
10 | good: {
11 | fcp: 2336,
12 | si: 3387,
13 | lcp: 2500,
14 | tti: 3875,
15 | tbt: 287,
16 | cls: 0.1,
17 | // fid: 130,
18 | },
19 |
20 | };
--------------------------------------------------------------------------------
/_data/sites/sample.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: "Sample", // optional, falls back to object key
3 | description: "The default sites that get tested",
4 | options: {
5 | runs: 1,
6 | frequency: 1, // (in minutes)
7 | },
8 | urls: [
9 | "https://www.speedlify.dev/"
10 | ]
11 | };
--------------------------------------------------------------------------------
/_data/sites/ssg.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: "SSG", // optional, falls back to object key
3 | description: "Site Generator web sites",
4 | // skip if localhost
5 | // skip if this is a new fork of the speedlify (not Zach’s)
6 | skip: !process.env.CONTEXT || process.env.SITE_NAME !== "speedlify",
7 | options: {
8 | frequency: 60 * 23, // 24 hours
9 | // Use "run" if the sites don’t share assets on the same origin
10 | // and we can reset chrome with each run instead of
11 | // each site in every run (it’s faster)
12 | // Use "site" if sites are all on the same origin and share assets.
13 | },
14 | urls: [
15 | "https://www.11ty.dev/",
16 | "https://www.gatsbyjs.com/",
17 | "https://gohugo.io/",
18 | "https://nextjs.org/",
19 | "https://nuxt.com/",
20 | "https://gridsome.org/",
21 | "https://vuepress.vuejs.org/",
22 | "https://docusaurus.io/",
23 | "https://astro.build/",
24 | "https://jekyllrb.com/",
25 | "https://hexo.io/",
26 | "https://svelte.dev/",
27 | "https://remix.run/",
28 | "https://record-collector.net/",
29 | "https://www.solidjs.com/",
30 | "https://lume.land/",
31 | ],
32 | skipUrls: [
33 | "https://hexo.io/zh-cn/",
34 | ]
35 | };
--------------------------------------------------------------------------------
/_data/sites/test-runners.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | description: "Front-end Testing Tools",
3 | // skip if localhost
4 | // skip if this is a new fork of the speedlify (not Zach’s)
5 | skip: !process.env.CONTEXT || process.env.SITE_NAME !== "speedlify",
6 | options: {
7 | frequency: 60 * 11 + 30, // 11h, 30m
8 | // Use "run" if the sites don’t share assets on the same origin
9 | // and we can reset chrome with each run instead of
10 | // each site in every run (it’s faster)
11 | // Use "site" if sites are all on the same origin and share assets.
12 | freshChrome: "site",
13 | },
14 | urls: [
15 | "https://eslint.org/",
16 | "https://qunitjs.com/",
17 | "https://karma-runner.github.io/latest/index.html",
18 | "https://gulpjs.com/",
19 | "https://webhint.io/",
20 | "https://gruntjs.com/",
21 | "https://theintern.io/",
22 | "https://istanbul.js.org/",
23 | "https://webdriver.io/",
24 | "https://mochajs.org/"
25 | ]
26 | };
--------------------------------------------------------------------------------
/_data/sites/zachleat.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | name: "zachleat.com", // optional, falls back to object key
3 | description: "Zach’s Personal web site",
4 | // skip if localhost
5 | // skip if this is a new fork of the speedlify (not Zach’s)
6 | skip: !process.env.CONTEXT || process.env.SITE_NAME !== "speedlify",
7 | options: {
8 | frequency: 60 * 23, // 23 hours
9 | // Use "run" if the sites don’t share assets on the same origin
10 | // and we can reset chrome with each run instead of
11 | // each site in every run (it’s faster)
12 | // Use "site" if sites are all on the same origin and share assets.
13 | freshChrome: "site"
14 | },
15 | urls: [
16 | "https://www.zachleat.com/",
17 | "https://www.zachleat.com/about/",
18 | "https://www.zachleat.com/web/",
19 | "https://www.zachleat.com/web/fonts/",
20 | "https://www.zachleat.com/web/eleventy/",
21 | "https://www.zachleat.com/resume/",
22 | "https://www.zachleat.com/twitter/",
23 | // Popular Posts
24 | "https://www.zachleat.com/web/lighthouse-in-footer/",
25 | "https://www.zachleat.com/web/speedlify/",
26 | "https://www.zachleat.com/web/comprehensive-webfonts/",
27 | "https://www.zachleat.com/web/google-fonts-display/",
28 | ]
29 | };
--------------------------------------------------------------------------------
/_data/urlsForApi.js:
--------------------------------------------------------------------------------
1 | const fastglob = require("fast-glob");
2 |
3 | async function getCategoryToUrlMap() {
4 | let categories = await fastglob("./_data/sites/*.js", {
5 | caseSensitiveMatch: false
6 | });
7 |
8 | let map = {};
9 | for(let file of categories) {
10 | let categoryName = file.split("/").pop().replace(/\.js$/, "");
11 | map[categoryName] = [];
12 |
13 | let categoryData = require(`./sites/${categoryName}.js`);
14 | if(typeof categoryData === "function") {
15 | categoryData = await categoryData();
16 | }
17 | // TODO lowercase just the origin?
18 | map[categoryName] = categoryData.urls.map(url => url.toLowerCase());
19 | }
20 |
21 | return map;
22 | }
23 |
24 | function getCategoryList(map, url) {
25 | let categories = new Set();
26 | for(let categoryName in map) {
27 | if(map[categoryName].indexOf(url.toLowerCase())) {
28 | categories.add(categoryName);
29 | }
30 | }
31 | return Array.from(categories);
32 | }
33 |
34 | module.exports = async function() {
35 | let categoryMap = await getCategoryToUrlMap();
36 |
37 | let resultFiles = await fastglob("./_data/results/**/*.json", {
38 | caseSensitiveMatch: false
39 | });
40 |
41 | let sites = {};
42 |
43 | // TODO api JSON ranks should be dependent on category
44 | for(let resultFile of resultFiles) {
45 | let split = resultFile.split("/");
46 | let filename = split.pop();
47 | let hash = split.pop();
48 |
49 | let resultData = require(`.${resultFile}`);
50 |
51 | let categories = [];
52 | if(resultData.requestedUrl) {
53 | categories = getCategoryList(categoryMap, resultData.requestedUrl);
54 | }
55 |
56 | sites[hash] = {
57 | requestedUrl: resultData.requestedUrl,
58 | url: resultData.url,
59 | // based on final URL not requested URL
60 | hash: hash,
61 | categories: categories,
62 | // Deprecated
63 | vertical: categories[0],
64 | };
65 | }
66 | return Object.values(sites);
67 | };
--------------------------------------------------------------------------------
/_includes/layout.njk:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | Speedlify
7 |
8 |
9 |
10 |
13 |
14 |
15 |
16 |
speedlify{% if vertical %}:{{ vertical }}{% endif %}
17 |
18 | Benchmark {{ sites[vertical].description or "web pages" }} over time.
19 | {%- if sites[vertical].unordered %}
20 | Results for this category are unordered.
21 | {%- endif %}
22 | {%- if maxResults %}
23 | Updates at most once every {{ sites[vertical].options.frequency or 60 }} minutes. Only the last {{ maxResults }} run{% if maxResults != 1 %}s{% endif %} (and the oldest run) are shown.
24 | {%- endif %}
25 | Created by @zachleat.
26 |