diff --git a/SEO_DEPLOYMENT.md b/SEO_DEPLOYMENT.md new file mode 100644 index 0000000..7295e9e --- /dev/null +++ b/SEO_DEPLOYMENT.md @@ -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. diff --git a/app/globals.css b/app/globals.css index 61abe73..b042d8b 100644 --- a/app/globals.css +++ b/app/globals.css @@ -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; } @@ -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; } } @@ -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; } diff --git a/app/layout.tsx b/app/layout.tsx index c9ff388..b3c1822 100644 --- a/app/layout.tsx +++ b/app/layout.tsx @@ -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: [ diff --git a/app/not-found.tsx b/app/not-found.tsx index f4f01b3..8c17236 100644 --- a/app/not-found.tsx +++ b/app/not-found.tsx @@ -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 ( diff --git a/app/opengraph-image.tsx b/app/opengraph-image.tsx new file mode 100644 index 0000000..7e0f92b --- /dev/null +++ b/app/opengraph-image.tsx @@ -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( +
+
+ >_ + buffer.lol +
+
+
Free network & developer tools.
+
Fast diagnostics and utilities. No sign-up.
+
+
, + size + ); +} diff --git a/app/page.tsx b/app/page.tsx index 68cab88..53a2803 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -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 ( - -
-
+ <> + + +
+
Browser-based network utilities
-

buffer.lol

-

Fast, simple networking tools.

+

Network & developer tools.

+

Fast, focused, and free to use.

A focused toolbox for network diagnostics, web checks, and developer utilities—built to be quick, clear, and useful without getting in your way.

Explore tools @@ -31,11 +44,11 @@ export default function HomePage() {
-
+
- + - +
The toolbox

Everything you need.
Nothing you don't.

@@ -74,7 +87,8 @@ export default function HomePage() {

Small tools. Clear results. No accounts, dashboards, or mystery.

Generate a UUID
-
-
+ +
+ ); } diff --git a/app/privacy/page.tsx b/app/privacy/page.tsx index b81b8e5..e873a74 100644 --- a/app/privacy/page.tsx +++ b/app/privacy/page.tsx @@ -23,10 +23,11 @@ export default function PrivacyPage() {
  • Server-backed network and IP tools process the target you submit, the request metadata needed to complete the check, and short-lived diagnostic results.
  • Your email address and message content if you contact us.
  • Basic technical metadata collected by the hosting platform, such as request time, IP address, user agent, and abuse-prevention signals.
  • +
  • Aggregate page-view, referrer, device, country, and performance measurements produced by Cloudflare Web Analytics without site-set analytics cookies or advertising identifiers.
  • How We Use It

    -

    We use submitted data to provide requested tools, respond to messages, prevent abuse, and improve reliability. We do not sell contact emails.

    +

    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.

    Tool Data

    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.

    diff --git a/app/sitemap.ts b/app/sitemap.ts index c0f70ec..8f3621b 100644 --- a/app/sitemap.ts +++ b/app/sitemap.ts @@ -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 })) ]; } diff --git a/app/tools/[slug]/opengraph-image.tsx b/app/tools/[slug]/opengraph-image.tsx new file mode 100644 index 0000000..173246c --- /dev/null +++ b/app/tools/[slug]/opengraph-image.tsx @@ -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( +
    +
    +
    >_ buffer.lol
    +
    {category}
    +
    +
    +
    {title}
    +
    Fast, focused, and free to use.
    +
    +
    , + size + ); +} diff --git a/app/tools/[slug]/page.tsx b/app/tools/[slug]/page.tsx index c724b89..0c35319 100644 --- a/app/tools/[slug]/page.tsx +++ b/app/tools/[slug]/page.tsx @@ -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 })); @@ -15,27 +18,23 @@ export function generateStaticParams() { export async function generateMetadata({ params }: ToolPageProps): Promise { 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 ( - - - + <> + + + + }> + + + + + ); } diff --git a/components/StructuredData.tsx b/components/StructuredData.tsx new file mode 100644 index 0000000..c12b43e --- /dev/null +++ b/components/StructuredData.tsx @@ -0,0 +1,5 @@ +import { serializeJsonLd } from "@/lib/seo"; + +export function StructuredData({ data }: { data: unknown }) { + return "); + }); +}); + +describe("sitemap", () => { + it("contains one canonical URL per indexable page with stable date strings", () => { + const entries = sitemap(); + expect(entries).toHaveLength(34); + expect(new Set(entries.map((entry) => entry.url)).size).toBe(entries.length); + expect(entries.filter((entry) => entry.url.startsWith(`${siteUrl}/tools/`))).toHaveLength(27); + for (const entry of entries) { + expect(entry.url).not.toContain("www."); + expect(entry.lastModified).toMatch(/^\d{4}-\d{2}-\d{2}$/); + expect(entry).not.toHaveProperty("priority"); + expect(entry).not.toHaveProperty("changeFrequency"); + } + }); +}); diff --git a/tests/tool-discovery.test.ts b/tests/tool-discovery.test.ts index f99fc05..697f64a 100644 --- a/tests/tool-discovery.test.ts +++ b/tests/tool-discovery.test.ts @@ -12,6 +12,14 @@ function tool(name: string, overrides: Partial = {}): Tool { command: "check", status: "available", keywords: [], + seo: { + title: `${name} online`, + metaDescription: "A complete description of this useful network diagnostic tool for testing.", + updatedAt: "2026-07-19", + intro: "A useful introduction.", + sections: [], + faq: [] + }, ...overrides }; }