Skip to content

Commit 705c583

Browse files
authored
Merge pull request #102 from NodeByteHosting/development
fix(ui): optimizations and google ads support
2 parents 41865a2 + 519a1af commit 705c583

10 files changed

Lines changed: 137 additions & 10 deletions

File tree

‎CHANGELOG.md‎

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,22 @@ All notable changes to the NodeByte Hosting website will be documented in this f
55
The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),
66
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
77

8+
## [3.5.4] - 2026-05-30
9+
10+
### Added
11+
- **Google Ads Integration** — Google tag (`AW-16740819749`) installed via `next/script` with `strategy="afterInteractive"` in the root layout; fires after hydration without blocking render
12+
- `packages/core/lib/gtag.ts` — central utility exporting `GOOGLE_ADS_ID`, `CONVERSION_IDS` map, and ready-to-call `conversions.*()` helpers for all 10 goals (begin checkout, subscribe, purchase, submit lead form, page view, sign-up, get directions, request quote, outbound click, contact)
13+
- `packages/ui/components/google-ads-pageview.tsx` — client component that re-fires the page-view conversion on every App Router route change via `usePathname`
14+
15+
### Changed
16+
- **`About`, `Features`, `Services` converted to Server Components** — `"use client"` directive removed from all three home page sections; none use client-only APIs, state, or effects, so they now render as HTML on the server, reducing the client JS bundle and improving TTI
17+
- **Currency rates — localStorage TTL cache** — `CurrencyProvider` now checks `localStorage` for a cached rates response before fetching `/api/currency/rates`; cache TTL is 1 hour (matching the API's `s-maxage`), so repeat page loads within the hour skip the network request entirely
18+
19+
### Fixed
20+
- **Canvas globe — per-frame string allocation** — the dot-grid draw loop in `hero-graphic.tsx` was creating a new `` `rgba(150,175,215,${a})` `` string for every dot on every 60 fps frame (~500+ allocations/frame); replaced with a precomputed 32-entry `DOT_ALPHA_TABLE` lookup built once at module load time
21+
22+
---
23+
824
## [3.5.3] - 2026-04-17
925

1026
### Added

‎app/layout.tsx‎

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,11 @@ import type React from "react"
22
import type { Metadata } from "next"
33
import { cookies } from "next/headers"
44
import { Geist, Geist_Mono } from "next/font/google"
5+
import Script from "next/script"
56
import { Analytics } from "@vercel/analytics/next"
67
import { NextIntlClientProvider } from "next-intl"
8+
import { GoogleAdsPageView } from "@/packages/ui/components/google-ads-pageview"
9+
import { GOOGLE_ADS_ID } from "@/packages/core/lib/gtag"
710
import { getMessages, getLocale } from "next-intl/server"
811
import "./globals.css"
912
import { Toaster } from "@/packages/ui/components/ui/toaster"
@@ -154,6 +157,20 @@ export default async function RootLayout({
154157
</NextIntlClientProvider>
155158
<Toaster />
156159
<Analytics />
160+
<GoogleAdsPageView />
161+
{/* Google Ads tag */}
162+
<Script
163+
src={`https://www.googletagmanager.com/gtag/js?id=${GOOGLE_ADS_ID}`}
164+
strategy="afterInteractive"
165+
/>
166+
<Script id="gtag-init" strategy="afterInteractive">
167+
{`
168+
window.dataLayer = window.dataLayer || [];
169+
function gtag(){dataLayer.push(arguments);}
170+
gtag('js', new Date());
171+
gtag('config', '${GOOGLE_ADS_ID}');
172+
`}
173+
</Script>
157174
</body>
158175
</html>
159176
)

‎package.json‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
"name": "@nodebyte/hosting-site",
33
"description": "The official website for NodeByte Hosting.",
44
"license": "AGPL-3.0-only",
5-
"version": "3.3.0",
5+
"version": "3.5.4",
66
"scripts": {
77
"build": "next build",
88
"dev": "next dev",

‎packages/core/hooks/use-currency.tsx‎

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,38 @@ export function CurrencyProvider({ children }: { children: ReactNode }) {
4444
setCurrencyState(getDefaultCurrency())
4545
}
4646

47-
// Fetch live exchange rates — falls back to static if unavailable
47+
// Fetch live exchange rates — falls back to cached or static if unavailable
48+
const RATES_CACHE_KEY = "nb_currency_rates"
49+
const RATES_CACHE_TTL = 3600_000 // 1 hour in ms
50+
51+
try {
52+
const cached = localStorage.getItem(RATES_CACHE_KEY)
53+
if (cached) {
54+
const { rates, ts } = JSON.parse(cached) as { rates: Record<string, number>; ts: number }
55+
if (Date.now() - ts < RATES_CACHE_TTL) {
56+
setLiveRates((prev) => {
57+
const updated = { ...prev }
58+
for (const [code, rate] of Object.entries(rates)) {
59+
if (code in updated) updated[code as CurrencyCode] = rate
60+
}
61+
return updated
62+
})
63+
return // skip network fetch — cache is fresh
64+
}
65+
}
66+
} catch {
67+
// corrupted cache entry — ignore and fall through to fetch
68+
}
69+
4870
fetch("/api/currency/rates")
4971
.then((r) => r.json())
5072
.then((data: { rates?: Record<string, number> }) => {
5173
if (data.rates) {
74+
try {
75+
localStorage.setItem(RATES_CACHE_KEY, JSON.stringify({ rates: data.rates, ts: Date.now() }))
76+
} catch {
77+
// localStorage quota exceeded — ignore
78+
}
5279
setLiveRates((prev) => {
5380
const updated = { ...prev }
5481
for (const [code, rate] of Object.entries(data.rates!)) {

‎packages/core/lib/gtag.ts‎

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,46 @@
1+
export const GOOGLE_ADS_ID = "AW-16740819749"
2+
3+
/** Send-to labels for each conversion goal */
4+
export const CONVERSION_IDS = {
5+
beginCheckout: `${GOOGLE_ADS_ID}/qUthCNieibYcEKXG0q4-`,
6+
subscribe: `${GOOGLE_ADS_ID}/2l-5CNueibYcEKXG0q4-`,
7+
purchase: `${GOOGLE_ADS_ID}/eDjjCN6eibYcEKXG0q4-`,
8+
submitLeadForm: `${GOOGLE_ADS_ID}/CY9iCOGeibYcEKXG0q4-`,
9+
pageView: `${GOOGLE_ADS_ID}/li9bCOSeibYcEKXG0q4-`,
10+
signUp: `${GOOGLE_ADS_ID}/ElPhCOeeibYcEKXG0q4-`,
11+
getDirections: `${GOOGLE_ADS_ID}/E4xrCOqeibYcEKXG0q4-`,
12+
requestQuote: `${GOOGLE_ADS_ID}/WWVMCO2eibYcEKXG0q4-`,
13+
outboundClick: `${GOOGLE_ADS_ID}/Y6xZCPCeibYcEKXG0q4-`,
14+
contact: `${GOOGLE_ADS_ID}/Z1z3CJGcoLYcEKXG0q4-`,
15+
} as const
16+
17+
declare global {
18+
interface Window {
19+
gtag: (...args: unknown[]) => void
20+
dataLayer: unknown[]
21+
}
22+
}
23+
24+
function fireConversion(
25+
sendTo: string,
26+
extra?: Record<string, unknown>,
27+
): void {
28+
if (typeof window === "undefined" || typeof window.gtag !== "function") return
29+
window.gtag("event", "conversion", { send_to: sendTo, ...extra })
30+
}
31+
32+
/** Ready-to-call helpers for each conversion goal */
33+
export const conversions = {
34+
beginCheckout: () => fireConversion(CONVERSION_IDS.beginCheckout),
35+
subscribe: () => fireConversion(CONVERSION_IDS.subscribe),
36+
/** @param transactionId Optional order / invoice ID */
37+
purchase: (transactionId = "") =>
38+
fireConversion(CONVERSION_IDS.purchase, { transaction_id: transactionId }),
39+
submitLeadForm: () => fireConversion(CONVERSION_IDS.submitLeadForm),
40+
pageView: () => fireConversion(CONVERSION_IDS.pageView),
41+
signUp: () => fireConversion(CONVERSION_IDS.signUp),
42+
getDirections: () => fireConversion(CONVERSION_IDS.getDirections),
43+
requestQuote: () => fireConversion(CONVERSION_IDS.requestQuote),
44+
outboundClick: () => fireConversion(CONVERSION_IDS.outboundClick),
45+
contact: () => fireConversion(CONVERSION_IDS.contact),
46+
}

‎packages/ui/components/Layouts/Home/about.tsx‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"use client"
2-
31
import type React from "react"
42
import { Card } from "@/packages/ui/components/ui/card"
53
import { Heart, Code, Gamepad2, Server, Sparkles, ArrowRight, Globe, Shield, Zap } from "lucide-react"

‎packages/ui/components/Layouts/Home/features.tsx‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"use client"
2-
31
import React from "react"
42
import { Shield, Zap, Globe, Lock, Eye, Server, ArrowRight, CheckCircle2 } from "lucide-react"
53
import { Card } from "@/packages/ui/components/ui/card"

‎packages/ui/components/Layouts/Home/hero-graphic.tsx‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,14 @@ const CY = SIZE / 2
1616
const cT = Math.cos(TILT)
1717
const sT = Math.sin(TILT)
1818

19+
// Precomputed alpha lookup — avoids template-string allocation on every dot per frame.
20+
// alpha = 0.025 + 0.135 * (z / R), z ∈ (0, R] → 32 discrete levels.
21+
const DOT_ALPHA_LEVELS = 32
22+
const DOT_ALPHA_TABLE: string[] = Array.from({ length: DOT_ALPHA_LEVELS }, (_, i) => {
23+
const a = 0.025 + 0.135 * ((i + 1) / DOT_ALPHA_LEVELS)
24+
return `rgba(150,175,215,${a.toFixed(3)})`
25+
})
26+
1927
// ─── Locations ────────────────────────────────────────────────────────────────
2028

2129
type Region = "eu" | "am" | "ap"
@@ -159,8 +167,8 @@ export default function HeroGraphic() {
159167
for (let lon = 0; lon < 360; lon += step) {
160168
const p = proj(lat, lon, rot)
161169
if (p.z <= 0) continue
162-
const a = 0.025 + 0.135 * (p.z / R)
163-
ctx.fillStyle = `rgba(150,175,215,${a})`
170+
const idx = Math.min(DOT_ALPHA_LEVELS - 1, Math.floor((p.z / R) * DOT_ALPHA_LEVELS))
171+
ctx.fillStyle = DOT_ALPHA_TABLE[idx]
164172
ctx.fillRect(p.x - DOT_PX, p.y - DOT_PX, DOT_PX * 2, DOT_PX * 2)
165173
}
166174
}

‎packages/ui/components/Layouts/Home/services.tsx‎

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
"use client"
2-
31
import { Button } from "@/packages/ui/components/ui/button"
42
import { Card } from "@/packages/ui/components/ui/card"
53
import { Layers, ArrowRight, Check } from "lucide-react"
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
"use client"
2+
3+
import { useEffect } from "react"
4+
import { usePathname } from "next/navigation"
5+
import { conversions } from "@/packages/core/lib/gtag"
6+
7+
/**
8+
* Fires the Google Ads page-view conversion on every client-side route change.
9+
* Must be rendered inside the root layout (client boundary).
10+
*/
11+
export function GoogleAdsPageView() {
12+
const pathname = usePathname()
13+
14+
useEffect(() => {
15+
conversions.pageView()
16+
}, [pathname])
17+
18+
return null
19+
}

0 commit comments

Comments
 (0)