Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 30 additions & 0 deletions SEO_DEPLOYMENT.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
# SEO deployment checklist

## Canonical host

1. In Cloudflare, create a permanent redirect from `www.buffer.lol/*` to `https://buffer.lol/${1}` while preserving the path and query string.
2. Keep the existing HTTP-to-HTTPS redirect.
3. Confirm `curl -I https://www.buffer.lol/tools/dns-lookup` returns a permanent redirect to the apex URL.

The application also contains a host-based permanent redirect as a fallback. The Cloudflare rule avoids serving the duplicate hostname at the edge.

## Cloudflare Web Analytics

1. Open **Cloudflare → Web Analytics → Add a site** and select `buffer.lol`.
2. Use the automatic setup for the proxied hostname.
3. After deployment, confirm `beacon.min.js` loads and a request reaches `cloudflareinsights.com` without a Content Security Policy error.
4. Confirm page views appear without enabling cookies or advertising integrations.

## Google Search Console

1. Add a Domain property for `buffer.lol`.
2. Add the provided DNS TXT verification record in Cloudflare DNS and leave it in place.
3. Submit `https://buffer.lol/sitemap.xml` in the Sitemaps report.
4. Use URL Inspection to request initial indexing for:
- `https://buffer.lol/`
- `https://buffer.lol/tools/dns-lookup`
- `https://buffer.lol/tools/json-formatter`
- `https://buffer.lol/tools/my-ip`
- `https://buffer.lol/tools/ssl-checker`
- `https://buffer.lol/tools/http-headers`
5. Review Page Indexing and Search Performance weekly for the first month, then monthly. Record impressions, clicks, click-through rate, average position, indexed tool count, and queries by landing page.
22 changes: 22 additions & 0 deletions app/globals.css
Original file line number Diff line number Diff line change
Expand Up @@ -347,6 +347,21 @@ main,
.tool-command span { color: var(--purple); }
.tool-command i { display: inline-block; width: 5px; height: 11px; margin-left: 4px; vertical-align: -2px; background: var(--purple); animation: blink 1s steps(2) infinite; }
.tool-workspace { display: grid; grid-template-columns: minmax(300px, 0.8fr) minmax(0, 1.2fr); gap: 0.8rem; align-items: stretch; }
.tool-guide { max-width: 960px; margin: 5rem auto 3rem; padding-top: 4rem; border-top: 1px solid var(--line); }
.tool-guide > header { max-width: 780px; }
.tool-guide h2 { font-size: clamp(2rem, 4vw, 3.2rem); line-height: 1.1; letter-spacing: -0.05em; }
.tool-guide > header > p { margin-top: 1.1rem; color: #c5c1ce; font-size: 1rem; line-height: 1.8; }
.tool-guide-sections { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 1rem; margin-top: 2.5rem; }
.tool-guide-sections section { padding: 1.25rem; border: 1px solid var(--line); border-radius: 12px; background: rgba(255, 255, 255, 0.02); }
.tool-guide h3 { color: #e4e0e8; font-size: 0.92rem; }
.tool-guide-sections p, .tool-guide-sections li { margin-top: 0.8rem; color: var(--muted); font-size: 0.82rem; line-height: 1.72; }
.tool-guide-sections ul { padding-left: 1.1rem; }
.tool-faq { margin-top: 4rem; }
.tool-faq h2 { font-size: clamp(1.8rem, 3vw, 2.5rem); }
.tool-faq dl { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 0.8rem; margin-top: 1.5rem; }
.tool-faq dl > div { padding: 1.2rem; border: 1px solid var(--line); border-radius: 10px; background: rgba(155, 135, 245, 0.025); }
.tool-faq dt { color: #ded9e5; font-size: 0.88rem; font-weight: 650; }
.tool-faq dd { margin-top: 0.65rem; color: var(--muted); font-size: 0.8rem; line-height: 1.7; }
.related-tools { display: grid; grid-template-columns: auto minmax(0, 1fr); align-items: center; gap: 1rem; margin-top: 0.8rem; padding: 0.85rem 1rem; border: 1px solid var(--line); border-radius: 10px; background: rgba(255, 255, 255, 0.018); }
.related-tools > span { color: var(--faint); font-family: var(--font-mono); font-size: 0.58rem; letter-spacing: 0.06em; text-transform: uppercase; }
.related-tools > div { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 0.45rem; }
Expand Down Expand Up @@ -477,6 +492,7 @@ main,
.tools-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
.tool-workspace { grid-template-columns: 1fr; }
.tool-controls, .result-panel { min-height: 340px; }
.tool-guide-sections { grid-template-columns: 1fr; }
.launch-strip { grid-template-columns: 1fr auto; }
.launch-strip p { display: none; }
}
Expand Down Expand Up @@ -597,6 +613,12 @@ main,
.tool-page-header { align-items: start; flex-direction: column; padding: 3rem 0 2rem; }
.tool-page-header h1 { font-size: clamp(2.6rem, 13vw, 4rem); }
.tool-command { display: none; }
.tool-guide { margin-top: 3.5rem; padding-top: 3rem; }
.tool-guide > header > p { font-size: 0.91rem; line-height: 1.7; }
.tool-guide-sections { gap: 0.65rem; margin-top: 2rem; }
.tool-guide-sections section { padding: 1rem; }
.tool-faq { margin-top: 3rem; }
.tool-faq dl { grid-template-columns: 1fr; }
.tool-controls { padding: 1rem; }
.tool-controls input, .tool-controls textarea, .tool-controls select { font-size: 16px; }
.result-list div { grid-template-columns: 1fr; gap: 0.35rem; }
Expand Down
19 changes: 11 additions & 8 deletions app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -4,23 +4,26 @@ import "./globals.css";
export const metadata: Metadata = {
metadataBase: new URL("https://buffer.lol"),
title: {
default: "buffer.lol | Networking & developer tools",
default: "Free Network & Developer Tools | buffer.lol",
template: "%s | buffer.lol"
},
description:
"Fast, simple browser-based tools for network diagnostics, web checks, and developer utilities.",
"Free browser-based tools for network diagnostics, web checks, IP addresses, and developer utilities.",
openGraph: {
title: "buffer.lol",
title: "Free Network & Developer Tools | buffer.lol",
description:
"Fast, simple browser-based networking and developer tools.",
"Fast browser-based networking, IP, web, and developer utilities with no sign-up.",
type: "website",
url: "https://buffer.lol/"
url: "https://buffer.lol/",
siteName: "buffer.lol",
images: [{ url: "/opengraph-image", width: 1200, height: 630, alt: "buffer.lol network and developer tools" }]
},
twitter: {
card: "summary",
title: "buffer.lol",
card: "summary_large_image",
title: "Free Network & Developer Tools | buffer.lol",
description:
"Fast, simple browser-based networking and developer tools."
"Fast browser-based networking, IP, web, and developer utilities with no sign-up.",
images: ["/opengraph-image"]
},
icons: {
icon: [
Expand Down
6 changes: 6 additions & 0 deletions app/not-found.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
import Link from "next/link";
import type { Metadata } from "next";
import { SiteChrome } from "@/components/landing/SiteChrome";

export const metadata: Metadata = {
title: "Page Not Found",
robots: { index: false, follow: false }
};

export default function NotFoundPage() {
return (
<SiteChrome navHomePrefix="/">
Expand Down
21 changes: 21 additions & 0 deletions app/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
import { ImageResponse } from "next/og";

export const alt = "buffer.lol network and developer tools";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default function OpenGraphImage() {
return new ImageResponse(
<div style={{ width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "space-between", padding: "72px", color: "#f5f3ff", background: "radial-gradient(circle at 80% 20%, #3f2a79 0, #08080d 42%)", fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }}>
<div style={{ display: "flex", alignItems: "center", fontSize: 34, fontWeight: 700 }}>
<span style={{ display: "flex", padding: "10px 14px", marginRight: 18, border: "2px solid #9b87f5", borderRadius: 12, color: "#b9a9ff" }}>&gt;_</span>
buffer<span style={{ color: "#b9a9ff" }}>.lol</span>
</div>
<div style={{ display: "flex", flexDirection: "column" }}>
<div style={{ display: "flex", maxWidth: 950, fontSize: 74, lineHeight: 1.05, letterSpacing: "-4px", fontWeight: 700 }}>Free network &amp; developer tools.</div>
<div style={{ display: "flex", marginTop: 26, color: "#aaa5b6", fontSize: 28 }}>Fast diagnostics and utilities. No sign-up.</div>
</div>
</div>,
size
);
}
38 changes: 26 additions & 12 deletions app/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,23 +5,36 @@ import { HeroTerminal } from "@/components/landing/HeroTerminal";
import { SiteChrome } from "@/components/landing/SiteChrome";
import { ToolCard } from "@/components/tools/ToolCard";
import { QuickAccess } from "@/components/tools/ToolDiscovery";
import { categoryMeta, getToolsByCategory, type ToolCategory } from "@/data/tools";
import { categoryMeta, getToolsByCategory, tools, type ToolCategory } from "@/data/tools";
import { StructuredData } from "@/components/StructuredData";
import { homeStructuredData } from "@/lib/seo";

const categories: ToolCategory[] = ["networking", "ip", "developer"];

export const metadata: Metadata = {
alternates: { canonical: "/" }
title: { absolute: "Free Network & Developer Tools | buffer.lol" },
description: "Free browser-based tools for DNS, HTTP, SSL, IP addresses, network diagnostics, JSON, Base64, UUIDs, timestamps, and more.",
alternates: { canonical: "/" },
openGraph: {
title: "Free Network & Developer Tools | buffer.lol",
description: "Fast browser-based networking, IP, web, and developer utilities with no sign-up.",
url: "/",
type: "website",
siteName: "buffer.lol"
}
};

export default function HomePage() {
return (
<SiteChrome>
<main id="main-content">
<section className="home-hero" aria-labelledby="hero-title">
<>
<StructuredData data={homeStructuredData(tools)} />
<SiteChrome>
<main id="main-content">
<section className="home-hero" aria-labelledby="hero-title">
<div className="hero-copy">
<div className="eyebrow"><span className="live-dot" /> Browser-based network utilities</div>
<h1 id="hero-title">buffer<span>.lol</span></h1>
<p className="hero-subtitle">Fast, simple networking tools.</p>
<h1 id="hero-title">Network <span>&amp; developer</span> tools.</h1>
<p className="hero-subtitle">Fast, focused, and free to use.</p>
<p className="hero-description">A focused toolbox for network diagnostics, web checks, and developer utilities—built to be quick, clear, and useful without getting in your way.</p>
<div className="hero-actions">
<Link className="primary-button" href="#networking">Explore tools <span>↓</span></Link>
Expand All @@ -31,11 +44,11 @@ export default function HomePage() {
</div>

<HeroTerminal />
</section>
</section>

<QuickAccess />
<QuickAccess />

<IPLensPromo />
<IPLensPromo />

<section className="tool-intro" aria-labelledby="toolbox-heading">
<div><span className="section-kicker">The toolbox</span><h2 id="toolbox-heading">Everything you need.<br />Nothing you don&apos;t.</h2></div>
Expand Down Expand Up @@ -74,7 +87,8 @@ export default function HomePage() {
<p>Small tools. Clear results. No accounts, dashboards, or mystery.</p>
<Link href="/tools/uuid-generator">Generate a UUID <span>→</span></Link>
</section>
</main>
</SiteChrome>
</main>
</SiteChrome>
</>
);
}
3 changes: 2 additions & 1 deletion app/privacy/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,11 @@ export default function PrivacyPage() {
<li>Server-backed network and IP tools process the target you submit, the request metadata needed to complete the check, and short-lived diagnostic results.</li>
<li>Your email address and message content if you contact us.</li>
<li>Basic technical metadata collected by the hosting platform, such as request time, IP address, user agent, and abuse-prevention signals.</li>
<li>Aggregate page-view, referrer, device, country, and performance measurements produced by Cloudflare Web Analytics without site-set analytics cookies or advertising identifiers.</li>
</ul>

<h2>How We Use It</h2>
<p>We use submitted data to provide requested tools, respond to messages, prevent abuse, and improve reliability. We do not sell contact emails.</p>
<p>We use submitted data to provide requested tools, respond to messages, prevent abuse, understand aggregate site usage, and improve reliability and navigation. We do not sell contact emails or analytics data.</p>

<h2>Tool Data</h2>
<p>JSON, Base64, hashing, UUID, timestamp, URL parsing, JWT decoding, regex testing, CIDR calculations, and user-agent utilities run locally in your browser. Ping and packet-loss/stability tests send repeated HTTPS requests from your browser to buffer.lol. Server-backed DNS, HTTP, TLS, uptime, port, RDAP, redirect, robots/sitemap, public IP, geolocation, ASN, and traceroute tools send the target to buffer.lol only to produce the requested result. Traceroute checks are handled by a restricted diagnostics worker.</p>
Expand Down
28 changes: 15 additions & 13 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,26 @@
import type { MetadataRoute } from "next";
import { tools } from "@/data/tools";
import { tools } from "../data/tools";
import { siteUrl } from "../lib/seo";

const baseUrl = "https://buffer.lol";
const staticRoutes = [
{ path: "", updatedAt: "2026-07-19" },
{ path: "/privacy", updatedAt: "2026-07-19" },
{ path: "/terms", updatedAt: "2026-06-22" },
{ path: "/ip-lens", updatedAt: "2026-07-19" },
{ path: "/ip-lens/privacy", updatedAt: "2026-07-19" },
{ path: "/ip-lens/terms", updatedAt: "2026-07-19" },
{ path: "/ip-lens/support", updatedAt: "2026-07-19" }
] as const;

export default function sitemap(): MetadataRoute.Sitemap {
const lastModified = new Date();
const staticRoutes = ["", "/privacy", "/terms", "/ip-lens", "/ip-lens/privacy", "/ip-lens/terms", "/ip-lens/support"];

return [
...staticRoutes.map((route) => ({
url: `${baseUrl}${route}`,
lastModified,
changeFrequency: route === "/ip-lens" ? "monthly" as const : route ? "yearly" as const : "weekly" as const,
priority: route === "/ip-lens" ? 0.8 : route ? 0.3 : 1
url: `${siteUrl}${route.path}`,
lastModified: route.updatedAt
})),
...tools.map((tool) => ({
url: `${baseUrl}/tools/${tool.slug}`,
lastModified,
changeFrequency: "monthly" as const,
priority: 0.7
url: `${siteUrl}/tools/${tool.slug}`,
lastModified: tool.seo.updatedAt
}))
];
}
26 changes: 26 additions & 0 deletions app/tools/[slug]/opengraph-image.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { ImageResponse } from "next/og";
import { getTool } from "@/data/tools";

export const alt = "buffer.lol browser tool";
export const size = { width: 1200, height: 630 };
export const contentType = "image/png";

export default async function ToolOpenGraphImage({ params }: { params: Promise<{ slug: string }> }) {
const tool = getTool((await params).slug);
const title = tool?.seo.title ?? "Browser tool";
const category = tool?.category ?? "utility";

return new ImageResponse(
<div style={{ width: "100%", height: "100%", display: "flex", flexDirection: "column", justifyContent: "space-between", padding: "72px", color: "#f5f3ff", background: "radial-gradient(circle at 80% 20%, #3f2a79 0, #08080d 42%)", fontFamily: "ui-monospace, SFMono-Regular, Menlo, monospace" }}>
<div style={{ display: "flex", justifyContent: "space-between", alignItems: "center", fontSize: 30, fontWeight: 700 }}>
<div style={{ display: "flex", alignItems: "center" }}><span style={{ color: "#b9a9ff" }}>&gt;_&nbsp;</span>buffer<span style={{ color: "#b9a9ff" }}>.lol</span></div>
<div style={{ display: "flex", padding: "10px 16px", border: "1px solid #514866", borderRadius: 9, color: "#aaa5b6", fontSize: 20, textTransform: "uppercase" }}>{category}</div>
</div>
<div style={{ display: "flex", flexDirection: "column" }}>
<div style={{ display: "flex", maxWidth: 980, fontSize: 76, lineHeight: 1.04, letterSpacing: "-4px", fontWeight: 700 }}>{title}</div>
<div style={{ display: "flex", marginTop: 26, color: "#aaa5b6", fontSize: 27 }}>Fast, focused, and free to use.</div>
</div>
</div>,
size
);
}
35 changes: 17 additions & 18 deletions app/tools/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
import type { Metadata } from "next";
import { Suspense } from "react";
import { notFound } from "next/navigation";
import { SiteChrome } from "@/components/landing/SiteChrome";
import { ToolExperience } from "@/components/tools/ToolExperience";
import { ToolLayout } from "@/components/tools/ToolLayout";
import { TargetAwareToolExperience } from "@/components/tools/TargetAwareTools";
import { getTool, tools } from "@/data/tools";
import { safeTargetPrefill } from "@/lib/tool-discovery";
import { buildToolMetadata, toolStructuredData } from "@/lib/seo";
import { StructuredData } from "@/components/StructuredData";

type ToolPageProps = { params: Promise<{ slug: string }>; searchParams: Promise<{ target?: string | string[] }> };
type ToolPageProps = { params: Promise<{ slug: string }> };

export function generateStaticParams() {
return tools.map((tool) => ({ slug: tool.slug }));
Expand All @@ -15,27 +18,23 @@ export function generateStaticParams() {
export async function generateMetadata({ params }: ToolPageProps): Promise<Metadata> {
const tool = getTool((await params).slug);
if (!tool) return {};
return {
title: tool.name,
description: tool.description,
alternates: { canonical: `/tools/${tool.slug}` },
openGraph: {
title: `${tool.name} | buffer.lol`,
description: tool.description,
url: `/tools/${tool.slug}`,
type: "website"
}
};
return buildToolMetadata(tool);
}

export default async function ToolPage({ params, searchParams }: ToolPageProps) {
export default async function ToolPage({ params }: ToolPageProps) {
const tool = getTool((await params).slug);
if (!tool) notFound();
const target = tool.supportsTargetPrefill ? safeTargetPrefill((await searchParams).target) : "";

return (
<SiteChrome navHomePrefix="/">
<ToolLayout tool={tool} target={target}><ToolExperience tool={tool} initialTarget={target} /></ToolLayout>
</SiteChrome>
<>
<StructuredData data={toolStructuredData(tool)} />
<SiteChrome navHomePrefix="/">
<ToolLayout tool={tool}>
<Suspense fallback={<ToolExperience tool={tool} />}>
<TargetAwareToolExperience tool={tool} />
</Suspense>
</ToolLayout>
</SiteChrome>
</>
);
}
5 changes: 5 additions & 0 deletions components/StructuredData.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { serializeJsonLd } from "@/lib/seo";

export function StructuredData({ data }: { data: unknown }) {
return <script type="application/ld+json" dangerouslySetInnerHTML={{ __html: serializeJsonLd(data) }} />;
}
19 changes: 19 additions & 0 deletions components/tools/TargetAwareTools.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
"use client";

import { useSearchParams } from "next/navigation";
import type { Tool } from "@/data/tools";
import { safeTargetPrefill } from "@/lib/tool-discovery";
import { RelatedTools } from "./ToolDiscovery";
import { ToolExperience } from "./ToolExperience";

export function TargetAwareToolExperience({ tool }: { tool: Tool }) {
const searchParams = useSearchParams();
const target = tool.supportsTargetPrefill ? safeTargetPrefill(searchParams.get("target") ?? undefined) : "";
return <ToolExperience tool={tool} initialTarget={target} />;
}

export function TargetAwareRelatedTools({ related, allowTarget }: { related: Tool[]; allowTarget: boolean }) {
const searchParams = useSearchParams();
const target = allowTarget ? safeTargetPrefill(searchParams.get("target") ?? undefined) : "";
return <RelatedTools related={related} target={target} />;
}
Loading
Loading