Merge pull request #305 from Lissy93/feat/small-improvments
Small improvments
@@ -7,25 +7,23 @@ const log = createLogger('carbon');
|
||||
const TIMEOUT = 8000;
|
||||
const MAX_BYTES = 10 * 1024 * 1024;
|
||||
|
||||
// Sustainable Web Design model v3 constants, matches websitecarbon.com formula
|
||||
const KWH_PER_GB = 0.81;
|
||||
// Sustainable Web Design model v4 constants, matches websitecarbon.com formula
|
||||
const KWH_PER_GB = 0.3;
|
||||
const FIRST_VISIT = 0.25;
|
||||
const RETURN_VISIT = 0.75;
|
||||
const RETURN_DATA_PCT = 0.02;
|
||||
const GRID_INTENSITY = 442;
|
||||
const GRID_INTENSITY = 494;
|
||||
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;
|
||||
// Median CO2 for an HTML-only fetch at HTTP Archive's ~30 KB median
|
||||
const REFERENCE_MEDIAN_GRAMS = 0.001;
|
||||
|
||||
// Approximate percentile via log2 distance from the reference median.
|
||||
// 1 doubling above median drops 25 points; clamp to [1, 99]
|
||||
// Percentile rank via log2 distance from the median, clamped to [1, 95]
|
||||
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)));
|
||||
const pct = 50 - 15 * Math.log2(grams / REFERENCE_MEDIAN_GRAMS);
|
||||
return Math.max(1, Math.min(95, Math.round(pct)));
|
||||
};
|
||||
|
||||
// Stream the response, cap at MAX_BYTES so huge pages can't blow memory or time
|
||||
|
||||
19
api/rank.js
@@ -4,19 +4,24 @@ import { parseTarget } from './_common/parse-target.js';
|
||||
import { upstreamError } from './_common/upstream.js';
|
||||
|
||||
const rankHandler = async (url) => {
|
||||
const { hostname: domain } = parseTarget(url);
|
||||
const { hostname } = parseTarget(url);
|
||||
const { TRANCO_USERNAME, TRANCO_API_KEY } = process.env;
|
||||
const auth = TRANCO_API_KEY
|
||||
? { auth: { username: TRANCO_USERNAME, password: TRANCO_API_KEY } }
|
||||
: {};
|
||||
const fallback = hostname.startsWith('www.') ? hostname.slice(4) : `www.${hostname}`;
|
||||
// Tranco indexes only one variant per site, so try as-is, then toggle www
|
||||
const lookup = (domain) => httpGet(`https://tranco-list.eu/api/ranks/domain/${domain}`, auth);
|
||||
try {
|
||||
const response = await httpGet(`https://tranco-list.eu/api/ranks/domain/${domain}`, auth);
|
||||
if (!response.data?.ranks?.length) {
|
||||
return {
|
||||
skipped: `${domain} isn't ranked in the top 1 million sites yet`,
|
||||
};
|
||||
const first = await lookup(hostname);
|
||||
if (first.data?.ranks?.length) return first.data;
|
||||
try {
|
||||
const second = await lookup(fallback);
|
||||
if (second.data?.ranks?.length) return second.data;
|
||||
} catch {
|
||||
// Ignore fallback failures (e.g. rate limit) and accept the empty first result
|
||||
}
|
||||
return response.data;
|
||||
return { skipped: `${hostname} isn't ranked in the top 1 million sites yet` };
|
||||
} catch (error) {
|
||||
return upstreamError(error, 'Tranco rank lookup');
|
||||
}
|
||||
|
||||
@@ -45,10 +45,15 @@ const subdomainsHandler = async (url) => {
|
||||
params: { q: `%.${domain}`, output: 'json' },
|
||||
headers: { Accept: 'application/json' },
|
||||
});
|
||||
const rows = Array.isArray(res.data) ? res.data : [];
|
||||
const all = collectSubdomains(rows, domain);
|
||||
if (!Array.isArray(res.data)) {
|
||||
return { error: 'Certificate Transparency lookup returned unexpected data, please retry' };
|
||||
}
|
||||
const all = collectSubdomains(res.data, domain);
|
||||
if (!all.length) {
|
||||
return { skipped: `No subdomains found for ${domain} in Certificate Transparency logs` };
|
||||
return {
|
||||
skipped: `No subdomains found for ${domain} in Certificate Transparency logs`,
|
||||
retryable: true,
|
||||
};
|
||||
}
|
||||
return {
|
||||
domain,
|
||||
|
||||
@@ -17,7 +17,8 @@
|
||||
"typecheck": "astro check",
|
||||
"lint": "eslint --config .config/eslint.config.js .",
|
||||
"format:check": "prettier --check --ignore-unknown '!yarn.lock' '!**/*.md' .",
|
||||
"format:fix": "prettier --write --ignore-unknown '!yarn.lock' '!**/*.md' ."
|
||||
"format:fix": "prettier --write --ignore-unknown '!yarn.lock' '!**/*.md' .",
|
||||
"hold-my-beer": "yarn format:fix && yarn lint && yarn typecheck"
|
||||
},
|
||||
"dependencies": {
|
||||
"@astrojs/check": "^0.9.9",
|
||||
|
||||
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 3.3 KiB |
|
Before Width: | Height: | Size: 26 KiB After Width: | Height: | Size: 10 KiB |
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 3.0 KiB |
|
Before Width: | Height: | Size: 921 B After Width: | Height: | Size: 352 B |
|
Before Width: | Height: | Size: 2.3 KiB After Width: | Height: | Size: 605 B |
@@ -1,20 +1,45 @@
|
||||
{
|
||||
"name": "Web Check",
|
||||
"short_name": "Web Check",
|
||||
"name": "Lissy93/Web-Check",
|
||||
"description": "Web Check is the all-in-one OSINT and security tool, for revealing the inner workings of any website",
|
||||
"id": "/",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"theme_color": "#d6fb41",
|
||||
"background_color": "#111211",
|
||||
"icons": [
|
||||
{
|
||||
"src": "favicon.ico",
|
||||
"sizes": "64x64 32x32 24x24 16x16",
|
||||
"type": "image/x-icon"
|
||||
"src": "/favicon.svg",
|
||||
"type": "image/svg+xml",
|
||||
"sizes": "any"
|
||||
},
|
||||
{
|
||||
"src": "apple-touch-icon.png",
|
||||
"src": "/favicon-16x16.png",
|
||||
"type": "image/png",
|
||||
"sizes": "16x16"
|
||||
},
|
||||
{
|
||||
"src": "/favicon-32x32.png",
|
||||
"type": "image/png",
|
||||
"sizes": "32x32"
|
||||
},
|
||||
{
|
||||
"src": "/apple-touch-icon.png",
|
||||
"type": "image/png",
|
||||
"sizes": "180x180"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-192x192.png",
|
||||
"type": "image/png",
|
||||
"sizes": "192x192",
|
||||
"purpose": "any"
|
||||
},
|
||||
{
|
||||
"src": "/android-chrome-512x512.png",
|
||||
"type": "image/png",
|
||||
"sizes": "512x512",
|
||||
"purpose": "any"
|
||||
}
|
||||
],
|
||||
"start_url": ".",
|
||||
"display": "standalone",
|
||||
"theme_color": "#9fef00",
|
||||
"background_color": "#141d2b"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ const Nav = (props: { children?: ReactNode }) => {
|
||||
return (
|
||||
<Header as="header">
|
||||
<Heading color={colors.primary} size="large">
|
||||
<img width="64" src="/web-check.png" alt="Web Check Icon" />
|
||||
<img width="64" src="/favicon.svg" alt="Web Check Icon" />
|
||||
<a href="/" target="_self">
|
||||
Web Check
|
||||
</a>
|
||||
|
||||
@@ -315,12 +315,16 @@ const FancyBackground = (): JSX.Element => {
|
||||
useEffect(() => {
|
||||
App.setup();
|
||||
App.draw();
|
||||
|
||||
var frame = function () {
|
||||
App.evolve();
|
||||
const frameInterval = 25;
|
||||
let lastTime = 0;
|
||||
const frame = (now: number) => {
|
||||
if (now - lastTime >= frameInterval) {
|
||||
App.evolve();
|
||||
lastTime = now;
|
||||
}
|
||||
requestAnimationFrame(frame);
|
||||
};
|
||||
frame();
|
||||
requestAnimationFrame(frame);
|
||||
}, [App]);
|
||||
|
||||
return (
|
||||
|
||||
@@ -51,7 +51,7 @@ const Footer = (props: { isFixed?: boolean }): JSX.Element => {
|
||||
</span>
|
||||
<span>
|
||||
<Link to="/about">Web-Check</Link> is licensed under <ALink href={licenseUrl}>MIT</ALink> -
|
||||
© <ALink href={authorUrl}>Alicia Sykes</ALink> 2023
|
||||
© <ALink href={authorUrl}>Alicia Sykes</ALink> 2026
|
||||
</span>
|
||||
</StyledFooter>
|
||||
);
|
||||
|
||||
@@ -8,14 +8,15 @@ const LoaderContainer = styled(StyledCard)`
|
||||
margin: 0 auto;
|
||||
width: 95vw;
|
||||
position: relative;
|
||||
transition: all 0.2s ease-in-out;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
flex-direction: column;
|
||||
gap: 2rem;
|
||||
height: 50vh;
|
||||
transition: all 0.3s ease-in-out;
|
||||
transition:
|
||||
height 0.3s ease-in-out,
|
||||
opacity 0.3s ease-in-out;
|
||||
p.loadTimeInfo {
|
||||
text-align: center;
|
||||
margin: 0;
|
||||
@@ -29,14 +30,9 @@ const LoaderContainer = styled(StyledCard)`
|
||||
height: 0;
|
||||
overflow: hidden;
|
||||
opacity: 0;
|
||||
margin: -1rem 0 0 0;
|
||||
padding: 0;
|
||||
svg {
|
||||
width: 0;
|
||||
}
|
||||
h4 {
|
||||
font-size: 0;
|
||||
}
|
||||
margin-top: -1rem;
|
||||
padding-top: 0;
|
||||
padding-bottom: 0;
|
||||
}
|
||||
&.hide {
|
||||
display: none;
|
||||
|
||||
@@ -102,11 +102,11 @@ const fetchAndPoll = (path: string) =>
|
||||
}),
|
||||
);
|
||||
|
||||
// Re-run on transient errors, returning the last error if all attempts fail
|
||||
// Re-run on transient errors or when the server hints `retryable: true`
|
||||
const fetchAndRetry = (path: string) =>
|
||||
retrying(
|
||||
path,
|
||||
(r) => !!r?.error,
|
||||
(r) => !!r?.error || !!r?.retryable,
|
||||
3,
|
||||
2000,
|
||||
(last) => last,
|
||||
@@ -207,7 +207,6 @@ export const jobs: JobSpec[] = [
|
||||
{
|
||||
id: 'tls-labs',
|
||||
expectedAddressTypes: [...URL_ONLY],
|
||||
noClientTimeout: true,
|
||||
cards: [
|
||||
card('tls-security-audit', 'TLS Security Audit', ['security'], TlsSecurityAuditCard),
|
||||
card('tls-client-compat', 'TLS Client Compatibility', ['security'], TlsClientCompatCard),
|
||||
|
||||
@@ -121,12 +121,11 @@ const SponsorshipContainer = styled.div`
|
||||
}
|
||||
`;
|
||||
|
||||
const makeAnchor = (title: string): string => {
|
||||
return title
|
||||
const makeAnchor = (title: string): string =>
|
||||
title
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s]|_/g, '')
|
||||
.replace(/\s+/g, '-');
|
||||
};
|
||||
|
||||
const About = (): JSX.Element => {
|
||||
const location = useLocation();
|
||||
|
||||
@@ -43,42 +43,15 @@ const SponsorCard = styled.div`
|
||||
box-shadow: 4px 4px 0px ${colors.bgShadowColor};
|
||||
border-radius: 8px;
|
||||
padding: 1rem;
|
||||
z-index: 5;
|
||||
margin: 1rem;
|
||||
width: calc(100% - 2rem);
|
||||
max-width: 60rem;
|
||||
z-index: 2;
|
||||
.inner {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
flex-wrap: wrap;
|
||||
gap: 1rem;
|
||||
p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
p {
|
||||
margin: 0.25rem 0;
|
||||
}
|
||||
a {
|
||||
color: ${colors.textColor};
|
||||
}
|
||||
img {
|
||||
border-radius: 0.25rem;
|
||||
box-shadow: 2px 2px 0px ${colors.fgShadowColor};
|
||||
transition: box-shadow 0.2s;
|
||||
margin: 0 auto;
|
||||
display: block;
|
||||
width: 200px;
|
||||
&:hover {
|
||||
box-shadow: 4px 4px 0px ${colors.fgShadowColor};
|
||||
}
|
||||
&:active {
|
||||
box-shadow: -2px -2px 0px ${colors.fgShadowColor};
|
||||
}
|
||||
}
|
||||
.cta {
|
||||
font-size: 0.78rem;
|
||||
a {
|
||||
color: ${colors.primary};
|
||||
}
|
||||
color: ${colors.primary};
|
||||
}
|
||||
`;
|
||||
|
||||
@@ -124,14 +97,23 @@ const SiteFeaturesWrapper = styled(StyledCard)`
|
||||
font-size: 0.9rem;
|
||||
color: ${colors.textColor};
|
||||
li {
|
||||
position: relative;
|
||||
margin: 0.1rem 0;
|
||||
text-indent: -1.2rem;
|
||||
padding-left: 1.2rem;
|
||||
break-inside: avoid-column;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
li:before {
|
||||
content: '✓';
|
||||
color: ${colors.primary};
|
||||
margin-right: 0.5rem;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
}
|
||||
li:not(:last-child) a {
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
}
|
||||
a {
|
||||
@@ -139,6 +121,13 @@ const SiteFeaturesWrapper = styled(StyledCard)`
|
||||
}
|
||||
`;
|
||||
|
||||
// Build a URL-safe anchor id from a section title (e.g. "IP Info" -> "ip-info")
|
||||
const makeAnchor = (title: string): string =>
|
||||
title
|
||||
.toLowerCase()
|
||||
.replace(/[^\w\s]|_/g, '')
|
||||
.replace(/\s+/g, '-');
|
||||
|
||||
const Home = (): JSX.Element => {
|
||||
const defaultPlaceholder = 'e.g. duck.com';
|
||||
const [userInput, setUserInput] = useState('');
|
||||
@@ -191,23 +180,6 @@ const Home = (): JSX.Element => {
|
||||
submit();
|
||||
};
|
||||
|
||||
// const findIpAddress = () => {
|
||||
// setUserInput('');
|
||||
// setPlaceholder('Looking up your IP...');
|
||||
// setInputDisabled(true);
|
||||
// fetch('https://ipapi.co/json/')
|
||||
// .then(function(response) {
|
||||
// response.json().then(jsonData => {
|
||||
// setUserInput(jsonData.ip);
|
||||
// setPlaceholder(defaultPlaceholder);
|
||||
// setInputDisabled(true);
|
||||
// });
|
||||
// })
|
||||
// .catch(function(error) {
|
||||
// console.log('Failed to get IP address :\'(', error)
|
||||
// });
|
||||
// };
|
||||
|
||||
return (
|
||||
<HomeContainer>
|
||||
<FancyBackground />
|
||||
@@ -238,43 +210,16 @@ const Home = (): JSX.Element => {
|
||||
</UserInputMain>
|
||||
<SponsorCard>
|
||||
<Heading as="h2" size="small" color={colors.primary}>
|
||||
Sponsored by
|
||||
Enjoying Web Check?
|
||||
</Heading>
|
||||
<div className="inner">
|
||||
<p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=web-check&utm_source=wcgh"
|
||||
>
|
||||
Terminal Trove
|
||||
</a>{' '}
|
||||
- The $HOME of all things in the terminal.
|
||||
<br />
|
||||
<span className="cta">
|
||||
Get updates on the latest CLI/TUI tools via the{' '}
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
className="cta"
|
||||
href="https://terminaltrove.com/newsletter?utm_campaign=github&utm_medium=referral&utm_content=web-check&utm_source=wcgh"
|
||||
>
|
||||
Terminal Trove newsletter
|
||||
</a>
|
||||
</span>
|
||||
</p>
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noreferrer"
|
||||
href="https://terminaltrove.com/?utm_campaign=github&utm_medium=referral&utm_content=web-check&utm_source=wcgh"
|
||||
>
|
||||
<img
|
||||
width="120"
|
||||
alt="Terminal Trove"
|
||||
src="https://i.ibb.co/NKtYjJ1/terminal-trove-web-check.png"
|
||||
/>
|
||||
<p>
|
||||
It's free, open source, and funded by the community. If it's been useful, you can keep it
|
||||
going (and ad-free) by{' '}
|
||||
<a target="_blank" rel="noreferrer" href="https://github.com/sponsors/Lissy93">
|
||||
sponsoring me on GitHub
|
||||
</a>
|
||||
</div>
|
||||
. Every bit genuinely helps, thank you
|
||||
</p>
|
||||
</SponsorCard>
|
||||
<SiteFeaturesWrapper>
|
||||
<div className="features">
|
||||
@@ -283,7 +228,11 @@ const Home = (): JSX.Element => {
|
||||
</Heading>
|
||||
<ul>
|
||||
{docs.map((doc, index) => (
|
||||
<li key={index}>{doc.title}</li>
|
||||
<li key={index}>
|
||||
<Link to={`/check/about#${makeAnchor(doc.title)}`} title={doc.title}>
|
||||
{doc.title}
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
<li>
|
||||
<Link to="/check/about">+ more!</Link>
|
||||
|
||||
@@ -16,7 +16,10 @@ const siteInfo = {
|
||||
titleLong: 'Web Check - X-Ray Vision for any Website',
|
||||
description:
|
||||
'Web Check is the all-in-one OSINT and security tool, for revealing the inner workings of any website',
|
||||
keywords: '',
|
||||
keywords:
|
||||
'web check, OSINT, website security, security audit, DNS lookup, SSL check, ' +
|
||||
'tech stack detection, website analyzer, web tools, open source',
|
||||
themeColor: '#c1fb41',
|
||||
author: 'Alicia Sykes',
|
||||
twitter: '@Lissy_Sykes',
|
||||
site: import.meta.env.SITE_URL || 'https://web-check.xyz',
|
||||
@@ -27,17 +30,16 @@ const siteInfo = {
|
||||
},
|
||||
};
|
||||
|
||||
// Set values for the meta tags, from props or defaults
|
||||
const {
|
||||
title = siteInfo.title,
|
||||
description = siteInfo.description,
|
||||
keywords = siteInfo.keywords,
|
||||
breadcrumbs,
|
||||
customSchemaJson,
|
||||
} = Astro.props;
|
||||
// Resolve per-page values, falling back to siteInfo defaults
|
||||
const { title, description, keywords, breadcrumbs, customSchemaJson } = Astro.props;
|
||||
const pageTitle = title ?? siteInfo.title;
|
||||
const ogTitle = title ?? siteInfo.titleLong;
|
||||
const pageDescription = description ?? siteInfo.description;
|
||||
const pageKeywords = keywords ?? siteInfo.keywords;
|
||||
|
||||
// Set non-customizable values for meta tags, from the siteInfo
|
||||
const { site, author, twitter, analytics, titleLong } = siteInfo;
|
||||
const { site, author, twitter, analytics, themeColor } = siteInfo;
|
||||
const canonical = new URL(Astro.url.pathname, site).href;
|
||||
const ogImage = `${site}/banner.png`;
|
||||
|
||||
// Given a map of breadcrumbs, return the JSON-LD for the BreadcrumbList schema
|
||||
const makeBreadcrumbs = () => {
|
||||
@@ -56,10 +58,12 @@ const makeBreadcrumbs = () => {
|
||||
---
|
||||
|
||||
<!-- Core info -->
|
||||
<title>{title}</title>
|
||||
<meta name="description" content={description} />
|
||||
<meta name="keywords" content={keywords} />
|
||||
<title>{pageTitle}</title>
|
||||
<meta name="description" content={pageDescription} />
|
||||
{pageKeywords && <meta name="keywords" content={pageKeywords} />}
|
||||
<meta name="author" content={author} />
|
||||
<meta name="theme-color" content={themeColor} />
|
||||
<link rel="canonical" href={canonical} />
|
||||
|
||||
<!-- Page info, viewport, Astro credit -->
|
||||
<meta charset="UTF-8" />
|
||||
@@ -69,23 +73,23 @@ const makeBreadcrumbs = () => {
|
||||
|
||||
<!-- Icons and colors -->
|
||||
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
|
||||
<link rel="icon" type="image/png" sizes="512x512" href="/web-check.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="manifest" href="/manifest.json" />
|
||||
|
||||
<!-- Social media meta tags (Open Graphh + Twitter) -->
|
||||
<meta property="og:site_name" content={title} />
|
||||
<meta property="og:site_name" content={siteInfo.title} />
|
||||
<meta property="og:type" content="website" />
|
||||
<meta property="og:url" content={site} />
|
||||
<meta property="og:title" content={titleLong} />
|
||||
<meta property="og:description" content={description} />
|
||||
<meta property="og:image" content={`${site}/banner.png`} />
|
||||
<meta name="twitter:card" content="summary" />
|
||||
<meta property="og:url" content={canonical} />
|
||||
<meta property="og:title" content={ogTitle} />
|
||||
<meta property="og:description" content={pageDescription} />
|
||||
<meta property="og:image" content={ogImage} />
|
||||
<meta name="twitter:card" content="summary_large_image" />
|
||||
<meta name="twitter:url" content={site} />
|
||||
<meta name="twitter:title" content={titleLong} />
|
||||
<meta name="twitter:description" content={description} />
|
||||
<meta name="twitter:image" content={`${site}/banner.png`} />
|
||||
<link rel="twitter:image" sizes="180x180" href={`${site}/apple-touch-icon.png`} />
|
||||
<meta name="twitter:url" content={canonical} />
|
||||
<meta name="twitter:title" content={ogTitle} />
|
||||
<meta name="twitter:description" content={pageDescription} />
|
||||
<meta name="twitter:image" content={ogImage} />
|
||||
<meta name="twitter:site" content={twitter} />
|
||||
<meta name="twitter:creator" content={twitter} />
|
||||
|
||||
|
||||
@@ -14,9 +14,12 @@ if (searchUrl) {
|
||||
const target = normalizeAddress(searchUrl);
|
||||
if (target) Astro.redirect(`/check/${target}`);
|
||||
}
|
||||
|
||||
const { target } = Astro.params;
|
||||
const pageTitle = target ? `${target} | Web Check` : 'Web Check';
|
||||
---
|
||||
|
||||
<BaseLayout preloadBodyFont={false}>
|
||||
<BaseLayout preloadBodyFont={false} title={pageTitle}>
|
||||
<link
|
||||
slot="head"
|
||||
href="/fonts/PTMono-Regular.woff2"
|
||||
|
||||