From 499191becc64bf23814c2ea5804db96be84dbc81 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Sat, 9 May 2026 16:31:24 +0100 Subject: [PATCH] feat: Removed carbon dep, remove partytown, open links in new tab --- api/carbon.js | 16 ++++- astro.config.mjs | 3 +- package.json | 1 - src/client/components/Form/Row.tsx | 2 +- .../components/Results/CarbonFootprint.tsx | 65 ++++++++++--------- src/client/components/Results/Cookies.tsx | 6 +- .../components/misc/AdditionalResources.tsx | 5 +- src/client/components/misc/SelfScanMsg.tsx | 5 +- src/client/styles/globals.tsx | 6 -- src/client/views/Results.tsx | 2 +- src/layouts/Base.astro | 21 ++++-- src/pages/check/[...target].astro | 2 +- src/pages/web-check-api/index.astro | 2 +- yarn.lock | 17 +---- 14 files changed, 80 insertions(+), 73 deletions(-) diff --git a/api/carbon.js b/api/carbon.js index 1e8e016..0ca5b63 100644 --- a/api/carbon.js +++ b/api/carbon.js @@ -16,6 +16,18 @@ const GRID_INTENSITY = 442; const RENEWABLE_INTENSITY = 50; const LITRES_PER_GRAM = 0.5562; +// Reference median grams CO2 per visit, drawn from websitecarbon's published average. +// Used to estimate a percentile rank since we lack their measured-sites dataset +const REFERENCE_MEDIAN_GRAMS = 0.8; + +// Approximate percentile via log2 distance from the reference median. +// 1 doubling above median drops 25 points; clamp to [1, 99] +const estimateCleanerThan = (grams) => { + if (!grams || grams <= 0) return 0; + const pct = 50 - 25 * Math.log2(grams / REFERENCE_MEDIAN_GRAMS); + return Math.max(1, Math.min(99, Math.round(pct))); +}; + // Stream the response, cap at MAX_BYTES so huge pages can't blow memory or time const fetchByteCount = async (url) => { const r = await fetch(url, { @@ -66,11 +78,13 @@ const carbonHandler = async (url) => { } if (!bytes) return { skipped: 'Site returned no content, cannot calculate carbon' }; log.debug(`measured ${bytes} bytes for ${url}`); + const statistics = computeCarbon(bytes); return { url, bytes, green: false, - statistics: computeCarbon(bytes), + statistics, + cleanerThan: estimateCleanerThan(statistics.co2.grid.grams), scanUrl: url, }; }; diff --git a/astro.config.mjs b/astro.config.mjs index 1697207..0bbad11 100644 --- a/astro.config.mjs +++ b/astro.config.mjs @@ -4,7 +4,6 @@ import { loadEnv } from 'vite'; // Integrations import svelte from '@astrojs/svelte'; import react from '@astrojs/react'; -import partytown from '@astrojs/partytown'; import sitemap from '@astrojs/sitemap'; // Adapters @@ -36,7 +35,7 @@ const base = unwrapEnvVar('BASE_URL', '/'); const isBossServer = unwrapEnvVar('BOSS_SERVER', false); // Initialize Astro integrations -const integrations = [svelte(), react(), partytown(), sitemap()]; +const integrations = [svelte(), react(), sitemap()]; // Set the appropriate adapter, based on the deploy target function getAdapter(target) { diff --git a/package.json b/package.json index 82d5b68..1998220 100644 --- a/package.json +++ b/package.json @@ -73,7 +73,6 @@ "@astrojs/cloudflare": "^13.3.1", "@astrojs/netlify": "^7.0.8", "@astrojs/node": "^10.0.6", - "@astrojs/partytown": "^2.1.7", "@astrojs/sitemap": "^3.7.2", "@astrojs/svelte": "^8.1.0", "@astrojs/ts-plugin": "^1.10.7", diff --git a/src/client/components/Form/Row.tsx b/src/client/components/Form/Row.tsx index 3bb276a..6b030e2 100644 --- a/src/client/components/Form/Row.tsx +++ b/src/client/components/Form/Row.tsx @@ -219,7 +219,7 @@ const Row = (props: RowProps) => { {lbl} )} - copyToClipboard(val)}> + copyToClipboard(val)}> {formatValue(val)} {plaintext && {plaintext}</PlainText>} diff --git a/src/client/components/Results/CarbonFootprint.tsx b/src/client/components/Results/CarbonFootprint.tsx index 7220c16..545b26c 100644 --- a/src/client/components/Results/CarbonFootprint.tsx +++ b/src/client/components/Results/CarbonFootprint.tsx @@ -1,4 +1,3 @@ -import { useEffect, useState } from 'react'; import styled from '@emotion/styled'; import { Card } from 'client/components/Form/Card'; import Row from 'client/components/Form/Row'; @@ -13,47 +12,51 @@ const LearnMoreInfo = styled.p` } `; +const formatBytes = (n: number): string => { + if (n >= 1048576) return `${(n / 1048576).toFixed(2)} MB`; + if (n >= 1024) return `${(n / 1024).toFixed(2)} KB`; + return `${Math.round(n)} bytes`; +}; + +const formatGrams = (g: number): string => { + if (g >= 1000) return `${(g / 1000).toFixed(2)} kg`; + if (g >= 1) return `${g.toFixed(2)} g`; + return `${(g * 1000).toFixed(2)} mg`; +}; + +const formatKwh = (kwh: number): string => { + if (kwh >= 1) return `${kwh.toFixed(3)} kWh`; + if (kwh >= 0.001) return `${(kwh * 1000).toFixed(3)} Wh`; + return `${(kwh * 1_000_000).toFixed(2)} mWh`; +}; + const CarbonCard = (props: { data: any; title: string; actionButtons: any }): JSX.Element => { const carbons = props.data.statistics; - const initialUrl = props.data.scanUrl; - - const [carbonData, setCarbonData] = useState<{ c?: number; p?: number }>({}); - - useEffect(() => { - const fetchCarbonData = async () => { - try { - const response = await fetch( - `https://api.websitecarbon.com/b?url=${encodeURIComponent(initialUrl)}`, - ); - const data = await response.json(); - setCarbonData(data); - } catch (error) { - console.error('Error fetching carbon data:', error); - } - }; - fetchCarbonData(); - }, [initialUrl]); + const cleanerThan = props.data.cleanerThan; return ( <Card heading={props.title} actionButtons={props.actionButtons}> - {!carbons?.adjustedBytes && !carbonData.c && ( - <p>Unable to calculate carbon footprint for host</p> - )} + {!carbons?.adjustedBytes && <p>Unable to calculate carbon footprint for host</p>} {carbons?.adjustedBytes > 0 && ( <> - <Row lbl="HTML Initial Size" val={`${carbons.adjustedBytes} bytes`} /> - <Row - lbl="CO2 for Initial Load" - val={`${(carbons.co2.grid.grams * 1000).toPrecision(4)} grams`} - /> - <Row lbl="Energy Usage for Load" val={`${(carbons.energy * 1000).toPrecision(4)} KWg`} /> + <Row lbl="HTML Initial Size" val={formatBytes(carbons.adjustedBytes)} /> + <Row lbl="CO2 for Initial Load" val={formatGrams(carbons.co2.grid.grams)} /> + <Row lbl="Energy Usage for Load" val={formatKwh(carbons.energy)} /> + {cleanerThan > 0 && ( + <Row lbl="Cleaner than average page (est.)" val={`${cleanerThan}%`} /> + )} </> )} - {carbonData.c && <Row lbl="CO2 Emitted" val={`${carbonData.c} grams`} />} - {carbonData.p && <Row lbl="Better than average site by" val={`${carbonData.p}%`} />} <br /> <LearnMoreInfo> - Learn more at <a href="https://www.websitecarbon.com/">websitecarbon.com</a> + Calculated using the{' '} + <a + href="https://sustainablewebdesign.org/estimating-digital-emissions" + target="_blank" + rel="noreferrer" + > + Sustainable Web Model v4 + </a> </LearnMoreInfo> </Card> ); diff --git a/src/client/components/Results/Cookies.tsx b/src/client/components/Results/Cookies.tsx index 5891cd6..6d269c4 100644 --- a/src/client/components/Results/Cookies.tsx +++ b/src/client/components/Results/Cookies.tsx @@ -34,20 +34,20 @@ const CookiesCard = (props: { data: any; title: string; actionButtons: any }): J }); return ( <ExpandableRow - key={`cookie-${index}`} + key={`header-cookie-${index}-${cookie.name}`} lbl={cookie.name} val={cookie.value} rowList={attributes} /> ); })} - {clientCookies.map((cookie: any) => { + {clientCookies.map((cookie: any, index: number) => { const nameValPairs = Object.keys(cookie).map((key: string) => { return { lbl: key, val: cookie[key] }; }); return ( <ExpandableRow - key={`cookie-${cookie.name}`} + key={`client-cookie-${index}-${cookie.name}`} lbl={cookie.name} val="" rowList={nameValPairs} diff --git a/src/client/components/misc/AdditionalResources.tsx b/src/client/components/misc/AdditionalResources.tsx index 4272f99..87ce697 100644 --- a/src/client/components/misc/AdditionalResources.tsx +++ b/src/client/components/misc/AdditionalResources.tsx @@ -316,7 +316,10 @@ const AdditionalResources = (props: { url?: string }): JSX.Element => { <br /> At the time of listing, all of the above were available and free to use - if this changes, please report it via GitHub ( - <a href="https://github.com/lissy93/web-check">lissy93/web-check</a>). + <a target="_blank" rel="noreferrer" href="https://github.com/lissy93/web-check"> + lissy93/web-check + </a> + ). </Note> </Card> ); diff --git a/src/client/components/misc/SelfScanMsg.tsx b/src/client/components/misc/SelfScanMsg.tsx index 6ae238f..5f24637 100644 --- a/src/client/components/misc/SelfScanMsg.tsx +++ b/src/client/components/misc/SelfScanMsg.tsx @@ -44,7 +44,10 @@ const SelfScanMsg = () => { <br /> <span> But if you want to see how this site is built, why not check out the{' '} - <a href="https://github.com/lissy93/web-check">source code</a>? + <a target="_blank" rel="noreferrer" href="https://github.com/lissy93/web-check"> + source code + </a> + ? </span> <br /> <i>Do me a favour, and drop the repo a Star while you're there</i> 😉 diff --git a/src/client/styles/globals.tsx b/src/client/styles/globals.tsx index 83aff8d..5cba8ba 100644 --- a/src/client/styles/globals.tsx +++ b/src/client/styles/globals.tsx @@ -3,12 +3,6 @@ import { Global, css } from '@emotion/react'; const GlobalStyles = () => ( <Global styles={css` - @font-face { - font-family: PTMono; - font-style: normal; - font-weight: 400; - src: url('/fonts/PTMono.ttf') format('ttf'); - } body, div, a, diff --git a/src/client/views/Results.tsx b/src/client/views/Results.tsx index 40d8040..fae9b51 100644 --- a/src/client/views/Results.tsx +++ b/src/client/views/Results.tsx @@ -223,7 +223,6 @@ const Results = (props: { address?: string }): JSX.Element => { }))} /> <AdditionalResources url={address} /> - <Footer /> <Modal isOpen={modalOpen} closeModal={() => setModalOpen(false)}> {modalContent} </Modal> @@ -234,6 +233,7 @@ const Results = (props: { address?: string }): JSX.Element => { theme="dark" position="bottom-right" /> + <Footer /> </ResultsOuter> ); }; diff --git a/src/layouts/Base.astro b/src/layouts/Base.astro index 5d1a8cc..1b3fbed 100644 --- a/src/layouts/Base.astro +++ b/src/layouts/Base.astro @@ -12,11 +12,14 @@ interface Props { description?: string; keywords?: string; customSchemaJson?: any; + preloadHeadingFont?: boolean; breadcrumbs?: Array<{ name: string; item: string; }>; } + +const { preloadHeadingFont = true } = Astro.props; --- <!doctype html> @@ -25,13 +28,17 @@ interface Props { <ClientRouter /> <MetaTags {...Astro.props} /> <slot name="head" /> - <link - href="/fonts/Hubot-Sans/Hubot-Sans.woff2" - as="font" - rel="preload" - type="font/woff2" - crossorigin="anonymous" - /> + { + preloadHeadingFont && ( + <link + href="/fonts/Hubot-Sans/WOFF2/HubotSans-Regular.woff2" + as="font" + rel="preload" + type="font/woff2" + crossorigin="anonymous" + /> + ) + } </head> <body> <slot /> diff --git a/src/pages/check/[...target].astro b/src/pages/check/[...target].astro index 592f622..fb8c97e 100644 --- a/src/pages/check/[...target].astro +++ b/src/pages/check/[...target].astro @@ -14,7 +14,7 @@ if (searchUrl) { } --- -<BaseLayout> +<BaseLayout preloadHeadingFont={false}> <Main client:only="react" /> </BaseLayout> diff --git a/src/pages/web-check-api/index.astro b/src/pages/web-check-api/index.astro index ba76e2e..7c0b332 100644 --- a/src/pages/web-check-api/index.astro +++ b/src/pages/web-check-api/index.astro @@ -40,7 +40,7 @@ import Footer from '@components/scafold/Footer.astro'; <h3>API Source</h3> <p>View, edit, download or deploy the API from source.</p> <div class="buttons"> - <a href="https://github.com/xray-web/web-check-api"> + <a href="https://github.com/xray-web/web-check-api" target="_blank" rel="noreferrer"> <img src="/assets/images/github.svg" /> GitHub </a> diff --git a/yarn.lock b/yarn.lock index 8d57936..c8de7bd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -116,14 +116,6 @@ send "^1.2.1" server-destroy "^1.0.1" -"@astrojs/partytown@^2.1.7": - version "2.1.7" - resolved "https://registry.npmjs.org/@astrojs/partytown/-/partytown-2.1.7.tgz" - integrity sha512-dbffmNmJ+sAJ0/aXSaLX4aI04EZS/2C6Mm/+fmd4ikqWO7hV6nIi0sug8Z3c+yqedJNi1swFvpwluWmGjLHNzw== - dependencies: - "@qwik.dev/partytown" "^0.13.2" - mrmime "^2.0.1" - "@astrojs/prism@4.0.1": version "4.0.1" resolved "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.1.tgz" @@ -1861,13 +1853,6 @@ tar-fs "^3.1.1" yargs "^17.7.2" -"@qwik.dev/partytown@^0.13.2": - version "0.13.2" - resolved "https://registry.npmjs.org/@qwik.dev/partytown/-/partytown-0.13.2.tgz" - integrity sha512-Umls4bSkuzqLVcGvf8OgwIn/OldproSAbaQ/iYGe8VPYBpl2CaOSxabWwkeC72LDFqxVL0b0q8XlI8MuChDyzg== - dependencies: - dotenv "^16.4.7" - "@reduxjs/toolkit@^1.9.0 || 2.x.x": version "2.11.2" resolved "https://registry.npmjs.org/@reduxjs/toolkit/-/toolkit-2.11.2.tgz" @@ -4251,7 +4236,7 @@ dot-prop@9.0.0, dot-prop@^9.0.0: dependencies: type-fest "^4.18.2" -dotenv@^16.3.1, dotenv@^16.4.7: +dotenv@^16.3.1: version "16.6.1" resolved "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz" integrity sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==