Kami menggunakan cookies untuk meningkatkan pengalaman Anda di website ini. Dengan melanjutkan, Anda menyetujui penggunaan cookies sesuai Kebijakan Privasi kami.
PWA memungkinkan website Anda diinstall di HP pengguna seperti aplikasi biasa, tanpa perlu Google Play atau App Store. Offline support, push notification, dan performa native — semua dari website.
Muhamad Putra Aulia Hidayat
Bayangkan website toko online Anda bisa di-install di HP pelanggan, muncul di home screen mereka, dan bisa diakses bahkan saat offline. Itu yang bisa dilakukan PWA.
1. Web App Manifest
// public/manifest.json
{
"name": "Toko Digital Uptime",
"short_name": "DigiUpti",
"description": "Platform layanan digital terpercaya",
"start_url": "/",
"display": "standalone",
"background_color": "#0a0a0a",
"theme_color": "#2563eb",
"orientation": "portrait",
"icons": [
{
"src": "/icons/icon-192x192.png",
"sizes": "192x192",
"type": "image/png",
"purpose": "maskable any"
},
{
"src": "/icons/icon-512x512.png",
"sizes": "512x512",
"type": "image/png",
"purpose": "maskable any"
}
],
"screenshots": [
{
"src": "/screenshots/home.png",
"sizes": "1280x720",
"type": "image/png",
"form_factor": "wide"
}
]
}
2. Service Worker
// public/sw.js
const CACHE_NAME = "digiupti-v1"
const STATIC_ASSETS = [
"/",
"/offline",
"/icons/icon-192x192.png"
]
// Install: cache static assets
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME).then(cache => cache.addAll(STATIC_ASSETS))
)
self.skipWaiting()
})
// Activate: hapus cache lama
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then(keys =>
Promise.all(
keys.filter(k => k !== CACHE_NAME).map(k => caches.delete(k))
)
)
)
self.clients.claim()
})
// Fetch: Network first, fallback to cache
self.addEventListener("fetch", (event) => {
if (event.request.method !== "GET") return
event.respondWith(
fetch(event.request)
.then(response => {
// Cache successful responses
const cloned = response.clone()
caches.open(CACHE_NAME).then(cache => {
cache.put(event.request, cloned)
})
return response
})
.catch(() => {
// Offline fallback
return caches.match(event.request)
.then(cached => cached || caches.match("/offline"))
})
)
})
// Push notifications
self.addEventListener("push", (event) => {
const data = event.data?.json()
event.waitUntil(
self.registration.showNotification(data.title, {
body: data.body,
icon: "/icons/icon-192x192.png",
badge: "/icons/badge-72x72.png",
data: { url: data.url },
actions: [
{ action: "open", title: "Lihat" },
{ action: "dismiss", title: "Tutup" }
]
})
)
})
3. Register Service Worker di Next.js
// app/layout.tsx
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html>
<head>
<link rel="manifest" href="/manifest.json" />
<meta name="theme-color" content="#2563eb" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<link rel="apple-touch-icon" href="/icons/icon-192x192.png" />
</head>
<body>
{children}
<ServiceWorkerRegistration />
</body>
</html>
)
}
// components/ServiceWorkerRegistration.tsx
"use client"
import { useEffect } from "react"
export function ServiceWorkerRegistration() {
useEffect(() => {
if ("serviceWorker" in navigator) {
navigator.serviceWorker.register("/sw.js")
.then(reg => console.log("SW registered:", reg.scope))
.catch(err => console.log("SW error:", err))
}
}, [])
return null
}
// lib/push-notifications.ts
export async function subscribeToPush() {
const registration = await navigator.serviceWorker.ready
const subscription = await registration.pushManager.subscribe({
userVisibleOnly: true,
applicationServerKey: process.env.NEXT_PUBLIC_VAPID_PUBLIC_KEY
})
// Kirim subscription ke server
await fetch("/api/push/subscribe", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(subscription)
})
return subscription
}
// app/api/push/send/route.ts
import webpush from "web-push"
webpush.setVapidDetails(
"mailto:hello@digiupti.com",
process.env.VAPID_PUBLIC_KEY!,
process.env.VAPID_PRIVATE_KEY!
)
export async function POST(req: Request) {
const { userId, title, body, url } = await req.json()
const subscription = await getSubscription(userId)
await webpush.sendNotification(
subscription,
JSON.stringify({ title, body, url })
)
return Response.json({ sent: true })
}
"use client"
import { useState, useEffect } from "react"
export function InstallPrompt() {
const [deferredPrompt, setDeferredPrompt] = useState<any>(null)
const [showBanner, setShowBanner] = useState(false)
useEffect(() => {
window.addEventListener("beforeinstallprompt", (e) => {
e.preventDefault()
setDeferredPrompt(e)
setShowBanner(true)
})
}, [])
const handleInstall = async () => {
if (!deferredPrompt) return
deferredPrompt.prompt()
const { outcome } = await deferredPrompt.userChoice
if (outcome === "accepted") setShowBanner(false)
}
if (!showBanner) return null
return (
<div className="fixed bottom-4 left-4 right-4 bg-primary text-primary-foreground p-4 rounded-xl shadow-lg">
<p className="font-semibold">Install aplikasi kami</p>
<p className="text-sm opacity-80">Akses lebih cepat langsung dari home screen</p>
<div className="flex gap-2 mt-3">
<button onClick={handleInstall} className="px-4 py-2 bg-white text-primary rounded-lg text-sm font-medium">
Install
</button>
<button onClick={() => setShowBanner(false)} className="px-4 py-2 opacity-70 text-sm">
Nanti saja
</button>
</div>
</div>
)
}
PWA adalah sweet spot antara web dan native app — terutama untuk pasar Indonesia di mana pengguna sensitif terhadap storage HP dan kuota internet.
Tips teknologi & bisnis mingguan
Bergabung dengan 2,500+ subscriber yang mendapatkan insight teknologi, tutorial development, dan tips bisnis digital langsung ke inbox mereka setiap minggu.
Dapatkan tips & insight teknologi terbaru langsung ke inbox Anda.
© 2026 PT Digital Uptime Teknologi Informasi. Hak cipta dilindungi.