{"id":4442,"date":"2025-11-11T08:17:48","date_gmt":"2025-11-11T01:17:48","guid":{"rendered":"https:\/\/www.jagowebdesign.com\/website\/?p=4442"},"modified":"2025-11-11T08:17:48","modified_gmt":"2025-11-11T01:17:48","slug":"progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern","status":"publish","type":"post","link":"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/","title":{"rendered":"Progressive Web App (PWA) Complete Guide: Membangun Aplikasi Web Modern"},"content":{"rendered":"<p>Progressive Web Apps (PWA) telah merevolusi cara kita membangun aplikasi web, menggabungkan keunggulan web dan native applications. Di tahun 2025, PWA menjadi standard untuk aplikasi web modern yang memberikan pengalaman seperti native app namun tetap accessible melalui browser. Artikel ini akan memberikan panduan lengkap untuk membangun PWA yang powerful dan user-friendly.<\/p>\n<p>Apa itu Progressive Web App?<\/p>\n<p>PWA adalah web applications yang menggunakan modern web capabilities untuk menyediakan user experience seperti native mobile app:<\/p>\n<p>1. Core PWA Characteristics<br \/>\n\u2022 Progressive: Works untuk semua users, regardless browser choice<br \/>\n\u2022 Responsive: Works di semua devices dan form factors<br \/>\n\u2022 Connectivity Independent: Works offline atau dengan poor network<br \/>\n\u2022 App-like: Feels like native app dengan navigation dan interactions<br \/>\n\u2022 Fresh: Always up-to-date dengan background sync<br \/>\n\u2022 Safe: Served melalui HTTPS untuk prevent tampering<br \/>\n\u2022 Discoverable: Searchable melalui search engines<br \/>\n\u2022 Re-engageable: Can send push notifications<br \/>\n\u2022 Installable: Can be added to home screen<\/p>\n<p>2. PWA Technology Stack<br \/>\n\u2022 Service Workers: Background processes untuk offline capability<br \/>\n\u2022 Web App Manifest: JSON file untuk app metadata<br \/>\n\u2022 HTTPS: Secure connection requirement<br \/>\n\u2022 Responsive Design: Multi-device compatibility<br \/>\n\u2022 Application Shell Architecture: Instant loading experience<\/p>\n<p>Service Workers Deep Dive<\/p>\n<p>1. Service Worker Fundamentals<br \/>\nService worker adalah JavaScript file yang berjalan di background, separate dari web page:<\/p>\n<p>&#8220;`javascript<br \/>\n\/\/ sw.js &#8211; Service Worker Implementation<br \/>\nconst CACHE_NAME = &#8216;pwa-app-v1.0.0&#8217;;<br \/>\nconst STATIC_CACHE = &#8216;static-v1&#8217;;<br \/>\nconst DYNAMIC_CACHE = &#8216;dynamic-v1&#8217;;<\/p>\n<p>\/\/ Files to cache immediately<br \/>\nconst STATIC_ASSETS = [<br \/>\n  &#8216;\/&#8217;,<br \/>\n  &#8216;\/index.html&#8217;,<br \/>\n  &#8216;\/css\/main.css&#8217;,<br \/>\n  &#8216;\/js\/main.js&#8217;,<br \/>\n  &#8216;\/images\/logo.png&#8217;,<br \/>\n  &#8216;\/images\/icons\/icon-192&#215;192.png&#8217;,<br \/>\n  &#8216;\/manifest.json&#8217;,<br \/>\n  &#8216;https:\/\/fonts.googleapis.com\/css2?family=Inter:wght@400;500;600;700&amp;display=swap&#8217;<br \/>\n];<\/p>\n<p>\/\/ Install event &#8211; cache static assets<br \/>\nself.addEventListener(&#8216;install&#8217;, (event) =&gt; {<br \/>\n  console.log(&#8216;Service Worker: Installing&#8230;&#8217;);<\/p>\n<p>  event.waitUntil(<br \/>\n    caches.open(STATIC_CACHE)<br \/>\n      .then((cache) =&gt; {<br \/>\n        console.log(&#8216;Service Worker: Caching static assets&#8217;);<br \/>\n        return cache.addAll(STATIC_ASSETS);<br \/>\n      })<br \/>\n      .then(() =&gt; self.skipWaiting())<br \/>\n  );<br \/>\n});<\/p>\n<p>\/\/ Activate event &#8211; clean up old caches<br \/>\nself.addEventListener(&#8216;activate&#8217;, (event) =&gt; {<br \/>\n  console.log(&#8216;Service Worker: Activating&#8230;&#8217;);<\/p>\n<p>  event.waitUntil(<br \/>\n    caches.keys()<br \/>\n      .then((cacheNames) =&gt; {<br \/>\n        return Promise.all(<br \/>\n          cacheNames.map((cacheName) =&gt; {<br \/>\n            if (cacheName !== STATIC_CACHE &amp;&amp; cacheName !== DYNAMIC_CACHE) {<br \/>\n              console.log(&#8216;Service Worker: Deleting old cache:&#8217;, cacheName);<br \/>\n              return caches.delete(cacheName);<br \/>\n            }<br \/>\n          })<br \/>\n        );<br \/>\n      })<br \/>\n      .then(() =&gt; self.clients.claim())<br \/>\n  );<br \/>\n});<\/p>\n<p>\/\/ Fetch event &#8211; serve from cache with network fallback<br \/>\nself.addEventListener(&#8216;fetch&#8217;, (event) =&gt; {<br \/>\n  const { request } = event;<\/p>\n<p>  \/\/ Skip non-GET requests<br \/>\n  if (request.method !== &#8216;GET&#8217;) {<br \/>\n    return;<br \/>\n  }<\/p>\n<p>  \/\/ Strategy 1: Cache First for static assets<br \/>\n  if (request.url.includes(&#8216;\/css\/&#8217;) ||<br \/>\n      request.url.includes(&#8216;\/js\/&#8217;) ||<br \/>\n      request.url.includes(&#8216;\/images\/&#8217;) ||<br \/>\n      request.url.includes(&#8216;\/fonts\/&#8217;)) {<\/p>\n<p>    event.respondWith(<br \/>\n      caches.match(request)<br \/>\n        .then((response) =&gt; {<br \/>\n          return response || fetch(request);<br \/>\n        })<br \/>\n    );<br \/>\n    return;<br \/>\n  }<\/p>\n<p>  \/\/ Strategy 2: Network First for API calls<br \/>\n  if (request.url.includes(&#8216;\/api\/&#8217;)) {<br \/>\n    event.respondWith(<br \/>\n      fetch(request)<br \/>\n        .then((response) =&gt; {<br \/>\n          \/\/ Cache successful API responses<br \/>\n          if (response.status === 200) {<br \/>\n            const responseClone = response.clone();<br \/>\n            caches.open(DYNAMIC_CACHE)<br \/>\n              .then((cache) =&gt; cache.put(request, responseClone));<br \/>\n          }<br \/>\n          return response;<br \/>\n        })<br \/>\n        .catch(() =&gt; {<br \/>\n          \/\/ Try to serve from cache if network fails<br \/>\n          return caches.match(request);<br \/>\n        })<br \/>\n    );<br \/>\n    return;<br \/>\n  }<\/p>\n<p>  \/\/ Strategy 3: Stale While Revalidate for HTML pages<br \/>\n  if (request.headers.get(&#8216;accept&#8217;).includes(&#8216;text\/html&#8217;)) {<br \/>\n    event.respondWith(<br \/>\n      caches.match(request)<br \/>\n        .then((response) =&gt; {<br \/>\n          const networkFetch = fetch(request)<br \/>\n            .then((newResponse) =&gt; {<br \/>\n              caches.open(DYNAMIC_CACHE)<br \/>\n                .then((cache) =&gt; cache.put(request, newResponse.clone()));<br \/>\n              return newResponse;<br \/>\n            });<\/p>\n<p>          return response || networkFetch;<br \/>\n        })<br \/>\n    );<br \/>\n    return;<br \/>\n  }<\/p>\n<p>  \/\/ Default: Network request<br \/>\n  event.respondWith(fetch(request));<br \/>\n});<br \/>\n&#8220;`<\/p>\n<p>2. Advanced Caching Strategies<br \/>\n&#8220;`javascript<br \/>\n\/\/ Advanced service worker with multiple strategies<br \/>\nclass CacheManager {<br \/>\n  constructor() {<br \/>\n    this.strategies = {<br \/>\n      cacheFirst: this.cacheFirst.bind(this),<br \/>\n      networkFirst: this.networkFirst.bind(this),<br \/>\n      staleWhileRevalidate: this.staleWhileRevalidate.bind(this),<br \/>\n      networkOnly: this.networkOnly.bind(this),<br \/>\n      cacheOnly: this.cacheOnly.bind(this)<br \/>\n    };<br \/>\n  }<\/p>\n<p>  async cacheFirst(request) {<br \/>\n    const cachedResponse = await caches.match(request);<br \/>\n    return cachedResponse || fetch(request);<br \/>\n  }<\/p>\n<p>  async networkFirst(request, cacheName = DYNAMIC_CACHE) {<br \/>\n    try {<br \/>\n      const networkResponse = await fetch(request);<\/p>\n<p>      if (networkResponse.ok) {<br \/>\n        const cache = await caches.open(cacheName);<br \/>\n        cache.put(request, networkResponse.clone());<br \/>\n      }<\/p>\n<p>      return networkResponse;<br \/>\n    } catch (error) {<br \/>\n      return caches.match(request);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  async staleWhileRevalidate(request, cacheName = DYNAMIC_CACHE) {<br \/>\n    const cache = await caches.open(cacheName);<br \/>\n    const cachedResponse = await cache.match(request);<\/p>\n<p>    const networkFetchPromise = fetch(request)<br \/>\n      .then((networkResponse) =&gt; {<br \/>\n        cache.put(request, networkResponse.clone());<br \/>\n        return networkResponse;<br \/>\n      })<br \/>\n      .catch(() =&gt; cachedResponse);<\/p>\n<p>    return cachedResponse || networkFetchPromise;<br \/>\n  }<\/p>\n<p>  async networkOnly(request) {<br \/>\n    return fetch(request);<br \/>\n  }<\/p>\n<p>  async cacheOnly(request) {<br \/>\n    return caches.match(request);<br \/>\n  }<\/p>\n<p>  \/\/ Dynamic routing based on request type<br \/>\n  async handleRequest(request) {<br \/>\n    const url = new URL(request.url);<\/p>\n<p>    \/\/ API calls &#8211; Network First<br \/>\n    if (url.pathname.startsWith(&#8216;\/api\/&#8217;)) {<br \/>\n      return this.networkFirst(request);<br \/>\n    }<\/p>\n<p>    \/\/ Static assets &#8211; Cache First<br \/>\n    if (url.pathname.match(\/\\.(css|js|png|jpg|jpeg|svg|woff|woff2)$\/)) {<br \/>\n      return this.cacheFirst(request);<br \/>\n    }<\/p>\n<p>    \/\/ HTML pages &#8211; Stale While Revalidate<br \/>\n    if (request.headers.get(&#8216;accept&#8217;).includes(&#8216;text\/html&#8217;)) {<br \/>\n      return this.staleWhileRevalidate(request);<br \/>\n    }<\/p>\n<p>    \/\/ Default &#8211; Network<br \/>\n    return this.networkOnly(request);<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ Initialize cache manager<br \/>\nconst cacheManager = new CacheManager();<\/p>\n<p>self.addEventListener(&#8216;fetch&#8217;, (event) =&gt; {<br \/>\n  if (event.request.method !== &#8216;GET&#8217;) return;<\/p>\n<p>  event.respondWith(<br \/>\n    cacheManager.handleRequest(event.request)<br \/>\n  );<br \/>\n});<br \/>\n&#8220;`<\/p>\n<p>Web App Manifest Configuration<\/p>\n<p>1. Complete Manifest File<br \/>\n&#8220;`json<br \/>\n\/\/ manifest.json<br \/>\n{<br \/>\n  &#8220;name&#8221;: &#8220;My Progressive Web App&#8221;,<br \/>\n  &#8220;short_name&#8221;: &#8220;PWA App&#8221;,<br \/>\n  &#8220;description&#8221;: &#8220;A modern Progressive Web Application built with best practices&#8221;,<br \/>\n  &#8220;start_url&#8221;: &#8220;\/&#8221;,<br \/>\n  &#8220;scope&#8221;: &#8220;\/&#8221;,<br \/>\n  &#8220;display&#8221;: &#8220;standalone&#8221;,<br \/>\n  &#8220;orientation&#8221;: &#8220;portrait-primary&#8221;,<br \/>\n  &#8220;theme_color&#8221;: &#8220;#2563eb&#8221;,<br \/>\n  &#8220;background_color&#8221;: &#8220;#ffffff&#8221;,<br \/>\n  &#8220;lang&#8221;: &#8220;en-US&#8221;,<br \/>\n  &#8220;dir&#8221;: &#8220;ltr&#8221;,<br \/>\n  &#8220;categories&#8221;: [&#8220;productivity&#8221;, &#8220;business&#8221;, &#8220;utilities&#8221;],<\/p>\n<p>  &#8220;icons&#8221;: [<br \/>\n    {<br \/>\n      &#8220;src&#8221;: &#8220;images\/icons\/icon-72&#215;72.png&#8221;,<br \/>\n      &#8220;sizes&#8221;: &#8220;72&#215;72&#8221;,<br \/>\n      &#8220;type&#8221;: &#8220;image\/png&#8221;,<br \/>\n      &#8220;purpose&#8221;: &#8220;maskable any&#8221;<br \/>\n    },<br \/>\n    {<br \/>\n      &#8220;src&#8221;: &#8220;images\/icons\/icon-96&#215;96.png&#8221;,<br \/>\n      &#8220;sizes&#8221;: &#8220;96&#215;96&#8221;,<br \/>\n      &#8220;type&#8221;: &#8220;image\/png&#8221;,<br \/>\n      &#8220;purpose&#8221;: &#8220;maskable any&#8221;<br \/>\n    },<br \/>\n    {<br \/>\n      &#8220;src&#8221;: &#8220;images\/icons\/icon-128&#215;128.png&#8221;,<br \/>\n      &#8220;sizes&#8221;: &#8220;128&#215;128&#8221;,<br \/>\n      &#8220;type&#8221;: &#8220;image\/png&#8221;,<br \/>\n      &#8220;purpose&#8221;: &#8220;maskable any&#8221;<br \/>\n    },<br \/>\n    {<br \/>\n      &#8220;src&#8221;: &#8220;images\/icons\/icon-144&#215;144.png&#8221;,<br \/>\n      &#8220;sizes&#8221;: &#8220;144&#215;144&#8221;,<br \/>\n      &#8220;type&#8221;: &#8220;image\/png&#8221;,<br \/>\n      &#8220;purpose&#8221;: &#8220;maskable any&#8221;<br \/>\n    },<br \/>\n    {<br \/>\n      &#8220;src&#8221;: &#8220;images\/icons\/icon-152&#215;152.png&#8221;,<br \/>\n      &#8220;sizes&#8221;: &#8220;152&#215;152&#8221;,<br \/>\n      &#8220;type&#8221;: &#8220;image\/png&#8221;,<br \/>\n      &#8220;purpose&#8221;: &#8220;maskable any&#8221;<br \/>\n    },<br \/>\n    {<br \/>\n      &#8220;src&#8221;: &#8220;images\/icons\/icon-192&#215;192.png&#8221;,<br \/>\n      &#8220;sizes&#8221;: &#8220;192&#215;192&#8221;,<br \/>\n      &#8220;type&#8221;: &#8220;image\/png&#8221;,<br \/>\n      &#8220;purpose&#8221;: &#8220;maskable any&#8221;<br \/>\n    },<br \/>\n    {<br \/>\n      &#8220;src&#8221;: &#8220;images\/icons\/icon-384&#215;384.png&#8221;,<br \/>\n      &#8220;sizes&#8221;: &#8220;384&#215;384&#8221;,<br \/>\n      &#8220;type&#8221;: &#8220;image\/png&#8221;,<br \/>\n      &#8220;purpose&#8221;: &#8220;maskable any&#8221;<br \/>\n    },<br \/>\n    {<br \/>\n      &#8220;src&#8221;: &#8220;images\/icons\/icon-512&#215;512.png&#8221;,<br \/>\n      &#8220;sizes&#8221;: &#8220;512&#215;512&#8221;,<br \/>\n      &#8220;type&#8221;: &#8220;image\/png&#8221;,<br \/>\n      &#8220;purpose&#8221;: &#8220;maskable any&#8221;<br \/>\n    }<br \/>\n  ],<\/p>\n<p>  &#8220;screenshots&#8221;: [<br \/>\n    {<br \/>\n      &#8220;src&#8221;: &#8220;images\/screenshots\/desktop-home.png&#8221;,<br \/>\n      &#8220;sizes&#8221;: &#8220;1280&#215;720&#8221;,<br \/>\n      &#8220;type&#8221;: &#8220;image\/png&#8221;,<br \/>\n      &#8220;form_factor&#8221;: &#8220;wide&#8221;,<br \/>\n      &#8220;label&#8221;: &#8220;Home screen on desktop&#8221;<br \/>\n    },<br \/>\n    {<br \/>\n      &#8220;src&#8221;: &#8220;images\/screenshots\/mobile-home.png&#8221;,<br \/>\n      &#8220;sizes&#8221;: &#8220;390&#215;844&#8221;,<br \/>\n      &#8220;type&#8221;: &#8220;image\/png&#8221;,<br \/>\n      &#8220;form_factor&#8221;: &#8220;narrow&#8221;,<br \/>\n      &#8220;label&#8221;: &#8220;Home screen on mobile&#8221;<br \/>\n    }<br \/>\n  ],<\/p>\n<p>  &#8220;shortcuts&#8221;: [<br \/>\n    {<br \/>\n      &#8220;name&#8221;: &#8220;New Task&#8221;,<br \/>\n      &#8220;short_name&#8221;: &#8220;Add&#8221;,<br \/>\n      &#8220;description&#8221;: &#8220;Create a new task quickly&#8221;,<br \/>\n      &#8220;url&#8221;: &#8220;\/new-task&#8221;,<br \/>\n      &#8220;icons&#8221;: [<br \/>\n        {<br \/>\n          &#8220;src&#8221;: &#8220;images\/icons\/add-96&#215;96.png&#8221;,<br \/>\n          &#8220;sizes&#8221;: &#8220;96&#215;96&#8221;<br \/>\n        }<br \/>\n      ]<br \/>\n    },<br \/>\n    {<br \/>\n      &#8220;name&#8221;: &#8220;Search&#8221;,<br \/>\n      &#8220;short_name&#8221;: &#8220;Search&#8221;,<br \/>\n      &#8220;description&#8221;: &#8220;Search tasks and content&#8221;,<br \/>\n      &#8220;url&#8221;: &#8220;\/search&#8221;,<br \/>\n      &#8220;icons&#8221;: [<br \/>\n        {<br \/>\n          &#8220;src&#8221;: &#8220;images\/icons\/search-96&#215;96.png&#8221;,<br \/>\n          &#8220;sizes&#8221;: &#8220;96&#215;96&#8221;<br \/>\n        }<br \/>\n      ]<br \/>\n    }<br \/>\n  ],<\/p>\n<p>  &#8220;share_target&#8221;: {<br \/>\n    &#8220;action&#8221;: &#8220;\/share&#8221;,<br \/>\n    &#8220;method&#8221;: &#8220;POST&#8221;,<br \/>\n    &#8220;enctype&#8221;: &#8220;multipart\/form-data&#8221;,<br \/>\n    &#8220;params&#8221;: {<br \/>\n      &#8220;title&#8221;: &#8220;title&#8221;,<br \/>\n      &#8220;text&#8221;: &#8220;text&#8221;,<br \/>\n      &#8220;url&#8221;: &#8220;url&#8221;,<br \/>\n      &#8220;files&#8221;: [<br \/>\n        {<br \/>\n          &#8220;name&#8221;: &#8220;files&#8221;,<br \/>\n          &#8220;accept&#8221;: [&#8220;image\/*&#8221;, &#8220;audio\/*&#8221;, &#8220;video\/*&#8221;]<br \/>\n        }<br \/>\n      ]<br \/>\n    }<br \/>\n  },<\/p>\n<p>  &#8220;protocol_handlers&#8221;: [<br \/>\n    {<br \/>\n      &#8220;protocol&#8221;: &#8220;web+pwa&#8221;,<br \/>\n      &#8220;url&#8221;: &#8220;\/handle-protocol?url=%s&#8221;<br \/>\n    }<br \/>\n  ]<br \/>\n}<br \/>\n&#8220;`<\/p>\n<p>2. Dynamic Manifest Generation<br \/>\n&#8220;`javascript<br \/>\n\/\/ Dynamic manifest generation based on user preferences<br \/>\nclass ManifestManager {<br \/>\n  constructor() {<br \/>\n    this.defaultManifest = {<br \/>\n      name: &#8220;My PWA App&#8221;,<br \/>\n      short_name: &#8220;PWA&#8221;,<br \/>\n      display: &#8220;standalone&#8221;,<br \/>\n      theme_color: &#8220;#2563eb&#8221;,<br \/>\n      background_color: &#8220;#ffffff&#8221;<br \/>\n    };<br \/>\n  }<\/p>\n<p>  generateManifest(userPreferences = {}) {<br \/>\n    const manifest = {<br \/>\n      &#8230;this.defaultManifest,<br \/>\n      &#8230;userPreferences,<br \/>\n      icons: this.generateIcons(userPreferences.theme),<br \/>\n      shortcuts: this.generateShortcuts(userPreferences.features)<br \/>\n    };<\/p>\n<p>    return manifest;<br \/>\n  }<\/p>\n<p>  generateIcons(theme = &#8216;default&#8217;) {<br \/>\n    const iconSets = {<br \/>\n      default: [<br \/>\n        { src: &#8220;icons\/icon-192&#215;192.png&#8221;, sizes: &#8220;192&#215;192&#8221;, type: &#8220;image\/png&#8221; },<br \/>\n        { src: &#8220;icons\/icon-512&#215;512.png&#8221;, sizes: &#8220;512&#215;512&#8221;, type: &#8220;image\/png&#8221; }<br \/>\n      ],<br \/>\n      dark: [<br \/>\n        { src: &#8220;icons\/dark\/icon-192&#215;192.png&#8221;, sizes: &#8220;192&#215;192&#8221;, type: &#8220;image\/png&#8221; },<br \/>\n        { src: &#8220;icons\/dark\/icon-512&#215;512.png&#8221;, sizes: &#8220;512&#215;512&#8221;, type: &#8220;image\/png&#8221; }<br \/>\n      ]<br \/>\n    };<\/p>\n<p>    return iconSets[theme] || iconSets.default;<br \/>\n  }<\/p>\n<p>  generateShortcuts(features = []) {<br \/>\n    const allShortcuts = {<br \/>\n      tasks: {<br \/>\n        name: &#8220;Tasks&#8221;,<br \/>\n        url: &#8220;\/tasks&#8221;,<br \/>\n        icons: [{ src: &#8220;icons\/tasks.png&#8221;, sizes: &#8220;96&#215;96&#8221; }]<br \/>\n      },<br \/>\n      calendar: {<br \/>\n        name: &#8220;Calendar&#8221;,<br \/>\n        url: &#8220;\/calendar&#8221;,<br \/>\n        icons: [{ src: &#8220;icons\/calendar.png&#8221;, sizes: &#8220;96&#215;96&#8221; }]<br \/>\n      },<br \/>\n      messages: {<br \/>\n        name: &#8220;Messages&#8221;,<br \/>\n        url: &#8220;\/messages&#8221;,<br \/>\n        icons: [{ src: &#8220;icons\/messages.png&#8221;, sizes: &#8220;96&#215;96&#8243; }]<br \/>\n      }<br \/>\n    };<\/p>\n<p>    return features.map(feature =&gt; allShortcuts[feature]).filter(Boolean);<br \/>\n  }<\/p>\n<p>  updateManifest(manifest) {<br \/>\n    const manifestBlob = new Blob([JSON.stringify(manifest, null, 2)], {<br \/>\n      type: &#8216;application\/json&#8217;<br \/>\n    });<\/p>\n<p>    const manifestURL = URL.createObjectURL(manifestBlob);<\/p>\n<p>    \/\/ Update manifest link in document<br \/>\n    const manifestLink = document.querySelector(&#8216;link[rel=&#8221;manifest&#8221;]&#8217;);<br \/>\n    if (manifestLink) {<br \/>\n      manifestLink.href = manifestURL;<br \/>\n    }<\/p>\n<p>    return manifestURL;<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ Usage<br \/>\nconst manifestManager = new ManifestManager();<br \/>\nconst dynamicManifest = manifestManager.generateManifest({<br \/>\n  theme: &#8216;dark&#8217;,<br \/>\n  features: [&#8216;tasks&#8217;, &#8216;calendar&#8217;],<br \/>\n  name: &#8220;My Custom PWA&#8221;<br \/>\n});<\/p>\n<p>manifestManager.updateManifest(dynamicManifest);<br \/>\n&#8220;`<\/p>\n<p>Background Sync dan Push Notifications<\/p>\n<p>1. Background Sync Implementation<br \/>\n&#8220;`javascript<br \/>\n\/\/ Background sync for offline actions<br \/>\nclass BackgroundSyncManager {<br \/>\n  constructor() {<br \/>\n    this.syncRegistry = new Map();<br \/>\n    this.setupEventListeners();<br \/>\n  }<\/p>\n<p>  setupEventListeners() {<br \/>\n    if (&#8216;serviceWorker&#8217; in navigator) {<br \/>\n      navigator.serviceWorker.ready.then(registration =&gt; {<br \/>\n        registration.addEventListener(&#8216;sync&#8217;, (event) =&gt; {<br \/>\n          if (event.tag === &#8216;background-sync&#8217;) {<br \/>\n            event.waitUntil(this.syncData());<br \/>\n          }<br \/>\n        });<br \/>\n      });<br \/>\n    }<br \/>\n  }<\/p>\n<p>  \/\/ Register sync event<br \/>\n  async registerSync(data) {<br \/>\n    if (&#8216;serviceWorker&#8217; in navigator &amp;&amp; &#8216;sync&#8217; in window.ServiceWorkerRegistration.prototype) {<br \/>\n      try {<br \/>\n        const registration = await navigator.serviceWorker.ready;<\/p>\n<p>        \/\/ Store data untuk sync<br \/>\n        this.syncRegistry.set(&#8216;pending-data&#8217;, data);<\/p>\n<p>        \/\/ Register sync<br \/>\n        await registration.sync.register(&#8216;background-sync&#8217;);<\/p>\n<p>        console.log(&#8216;Background sync registered&#8217;);<br \/>\n        return true;<br \/>\n      } catch (error) {<br \/>\n        console.error(&#8216;Background sync registration failed:&#8217;, error);<br \/>\n        return false;<br \/>\n      }<br \/>\n    } else {<br \/>\n      \/\/ Fallback: immediately sync<br \/>\n      return this.syncData();<br \/>\n    }<br \/>\n  }<\/p>\n<p>  async syncData() {<br \/>\n    const pendingData = this.syncRegistry.get(&#8216;pending-data&#8217;);<\/p>\n<p>    if (!pendingData) {<br \/>\n      console.log(&#8216;No data to sync&#8217;);<br \/>\n      return;<br \/>\n    }<\/p>\n<p>    try {<br \/>\n      \/\/ Sync data ke server<br \/>\n      const response = await fetch(&#8216;\/api\/sync&#8217;, {<br \/>\n        method: &#8216;POST&#8217;,<br \/>\n        headers: {<br \/>\n          &#8216;Content-Type&#8217;: &#8216;application\/json&#8217;,<br \/>\n        },<br \/>\n        body: JSON.stringify(pendingData)<br \/>\n      });<\/p>\n<p>      if (response.ok) {<br \/>\n        console.log(&#8216;Data synced successfully&#8217;);<br \/>\n        this.syncRegistry.delete(&#8216;pending-data&#8217;);<\/p>\n<p>        \/\/ Notify user<br \/>\n        this.showSyncNotification(&#8216;Data synced successfully!&#8217;);<br \/>\n      } else {<br \/>\n        throw new Error(&#8216;Sync failed&#8217;);<br \/>\n      }<br \/>\n    } catch (error) {<br \/>\n      console.error(&#8216;Sync failed:&#8217;, error);<br \/>\n      this.showSyncNotification(&#8216;Sync failed. Will retry later.&#8217;, &#8216;error&#8217;);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  showSyncNotification(message, type = &#8216;success&#8217;) {<br \/>\n    if (&#8216;Notification&#8217; in window &amp;&amp; Notification.permission === &#8216;granted&#8217;) {<br \/>\n      new Notification(&#8216;PWA Sync&#8217;, {<br \/>\n        body: message,<br \/>\n        icon: type === &#8216;success&#8217; ? &#8216;\/icons\/success.png&#8217; : &#8216;\/icons\/error.png&#8217;,<br \/>\n        badge: &#8216;\/icons\/badge.png&#8217;<br \/>\n      });<br \/>\n    }<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ Service worker sync handling<br \/>\nself.addEventListener(&#8216;sync&#8217;, (event) =&gt; {<br \/>\n  if (event.tag === &#8216;background-sync&#8217;) {<br \/>\n    event.waitUntil(<br \/>\n      fetch(&#8216;\/api\/sync&#8217;, {<br \/>\n        method: &#8216;POST&#8217;,<br \/>\n        headers: { &#8216;Content-Type&#8217;: &#8216;application\/json&#8217; },<br \/>\n        body: JSON.stringify({ data: &#8216;pending-sync-data&#8217; })<br \/>\n      })<br \/>\n      .then(response =&gt; {<br \/>\n        if (!response.ok) throw new Error(&#8216;Sync failed&#8217;);<br \/>\n        console.log(&#8216;Background sync successful&#8217;);<br \/>\n      })<br \/>\n      .catch(error =&gt; {<br \/>\n        console.error(&#8216;Background sync failed:&#8217;, error);<br \/>\n        \/\/ Sync will be retried automatically<br \/>\n      })<br \/>\n    );<br \/>\n  }<br \/>\n});<br \/>\n&#8220;`<\/p>\n<p>2. Push Notifications Setup<br \/>\n&#8220;`javascript<br \/>\n\/\/ Push notification manager<br \/>\nclass PushNotificationManager {<br \/>\n  constructor() {<br \/>\n    this.subscription = null;<br \/>\n    this.publicKey = &#8216;BKqQy6nJz8QzNnQz8QzNnQz8QzNnQz8QzNnQz8QzNnQz8QzNnQz8QzNnQz8QzNnQzNn&#8217;;<br \/>\n  }<\/p>\n<p>  async initialize() {<br \/>\n    \/\/ Request notification permission<br \/>\n    const permission = await this.requestPermission();<\/p>\n<p>    if (permission === &#8216;granted&#8217;) {<br \/>\n      await this.subscribeToPush();<br \/>\n      this.setupPushEventListeners();<br \/>\n    }<\/p>\n<p>    return permission;<br \/>\n  }<\/p>\n<p>  async requestPermission() {<br \/>\n    if (!(&#8216;Notification&#8217; in window)) {<br \/>\n      console.warn(&#8216;This browser does not support notifications&#8217;);<br \/>\n      return &#8216;denied&#8217;;<br \/>\n    }<\/p>\n<p>    if (Notification.permission === &#8216;granted&#8217;) {<br \/>\n      return &#8216;granted&#8217;;<br \/>\n    }<\/p>\n<p>    if (Notification.permission !== &#8216;denied&#8217;) {<br \/>\n      const permission = await Notification.requestPermission();<br \/>\n      return permission;<br \/>\n    }<\/p>\n<p>    return &#8216;denied&#8217;;<br \/>\n  }<\/p>\n<p>  async subscribeToPush() {<br \/>\n    try {<br \/>\n      const registration = await navigator.serviceWorker.ready;<\/p>\n<p>      \/\/ Check existing subscription<br \/>\n      const existingSubscription = await registration.pushManager.getSubscription();<\/p>\n<p>      if (existingSubscription) {<br \/>\n        this.subscription = existingSubscription;<br \/>\n        return existingSubscription;<br \/>\n      }<\/p>\n<p>      \/\/ Subscribe to push<br \/>\n      const subscription = await registration.pushManager.subscribe({<br \/>\n        userVisibleOnly: true,<br \/>\n        applicationServerKey: this.urlB64ToUint8Array(this.publicKey)<br \/>\n      });<\/p>\n<p>      this.subscription = subscription;<\/p>\n<p>      \/\/ Send subscription ke server<br \/>\n      await this.sendSubscriptionToServer(subscription);<\/p>\n<p>      console.log(&#8216;Push subscription successful&#8217;);<br \/>\n      return subscription;<\/p>\n<p>    } catch (error) {<br \/>\n      console.error(&#8216;Push subscription failed:&#8217;, error);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  async sendSubscriptionToServer(subscription) {<br \/>\n    try {<br \/>\n      await fetch(&#8216;\/api\/subscribe&#8217;, {<br \/>\n        method: &#8216;POST&#8217;,<br \/>\n        headers: {<br \/>\n          &#8216;Content-Type&#8217;: &#8216;application\/json&#8217;,<br \/>\n        },<br \/>\n        body: JSON.stringify(subscription)<br \/>\n      });<br \/>\n    } catch (error) {<br \/>\n      console.error(&#8216;Failed to send subscription to server:&#8217;, error);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  setupPushEventListeners() {<br \/>\n    navigator.serviceWorker.addEventListener(&#8216;message&#8217;, (event) =&gt; {<br \/>\n      if (event.data &amp;&amp; event.data.type === &#8216;PUSH_RECEIVED&#8217;) {<br \/>\n        this.handlePushReceived(event.data.payload);<br \/>\n      }<br \/>\n    });<br \/>\n  }<\/p>\n<p>  handlePushReceived(payload) {<br \/>\n    \/\/ Handle incoming push notification<br \/>\n    console.log(&#8216;Push received:&#8217;, payload);<\/p>\n<p>    \/\/ Update UI, show badge, dll<br \/>\n    this.updateUIWithNewData(payload);<br \/>\n  }<\/p>\n<p>  updateUIWithNewData(data) {<br \/>\n    \/\/ Update application state<br \/>\n    const event = new CustomEvent(&#8216;pwa-push-received&#8217;, { detail: data });<br \/>\n    window.dispatchEvent(event);<br \/>\n  }<\/p>\n<p>  \/\/ Utility function<br \/>\n  urlB64ToUint8Array(base64String) {<br \/>\n    const padding = &#8216;=&#8217;.repeat((4 &#8211; base64String.length % 4) % 4);<br \/>\n    const base64 = (base64String + padding)<br \/>\n      .replace(\/\\-\/g, &#8216;+&#8217;)<br \/>\n      .replace(\/_\/g, &#8216;\/&#8217;);<\/p>\n<p>    const rawData = window.atob(base64);<br \/>\n    const outputArray = new Uint8Array(rawData.length);<\/p>\n<p>    for (let i = 0; i  {<br \/>\n  const options = {<br \/>\n    body: event.data ? event.data.text() : &#8216;New message from PWA&#8217;,<br \/>\n    icon: &#8216;\/icons\/icon-192&#215;192.png&#8217;,<br \/>\n    badge: &#8216;\/icons\/badge.png&#8217;,<br \/>\n    vibrate: [100, 50, 100],<br \/>\n    data: {<br \/>\n      dateOfArrival: Date.now(),<br \/>\n      primaryKey: &#8216;2&#8217;<br \/>\n    },<br \/>\n    actions: [<br \/>\n      {<br \/>\n        action: &#8216;explore&#8217;,<br \/>\n        title: &#8216;Explore&#8217;,<br \/>\n        icon: &#8216;\/icons\/checkmark.png&#8217;<br \/>\n      },<br \/>\n      {<br \/>\n        action: &#8216;close&#8217;,<br \/>\n        title: &#8216;Close&#8217;,<br \/>\n        icon: &#8216;\/icons\/xmark.png&#8217;<br \/>\n      }<br \/>\n    ]<br \/>\n  };<\/p>\n<p>  event.waitUntil(<br \/>\n    self.registration.showNotification(&#8216;PWA Notification&#8217;, options)<br \/>\n  );<br \/>\n});<\/p>\n<p>self.addEventListener(&#8216;notificationclick&#8217;, (event) =&gt; {<br \/>\n  console.log(&#8216;Notification click received.&#8217;);<\/p>\n<p>  event.notification.close();<\/p>\n<p>  if (event.action === &#8216;explore&#8217;) {<br \/>\n    \/\/ Open app to specific page<br \/>\n    event.waitUntil(<br \/>\n      clients.openWindow(&#8216;\/explore&#8217;)<br \/>\n    );<br \/>\n  } else if (event.action === &#8216;close&#8217;) {<br \/>\n    \/\/ Just close notification<br \/>\n    return;<br \/>\n  } else {<br \/>\n    \/\/ Open app to homepage<br \/>\n    event.waitUntil(<br \/>\n      clients.openWindow(&#8216;\/&#8217;)<br \/>\n    );<br \/>\n  }<br \/>\n});<br \/>\n&#8220;`<\/p>\n<p>PWA Installation dan User Experience<\/p>\n<p>1. Install Prompt Management<br \/>\n&#8220;`javascript<br \/>\n\/\/ PWA Install prompt manager<br \/>\nclass PWAInstallManager {<br \/>\n  constructor() {<br \/>\n    this.deferredPrompt = null;<br \/>\n    this.installButton = null;<br \/>\n    this.setupInstallPrompt();<br \/>\n  }<\/p>\n<p>  setupInstallPrompt() {<br \/>\n    window.addEventListener(&#8216;beforeinstallprompt&#8217;, (event) =&gt; {<br \/>\n      \/\/ Prevent default install prompt<br \/>\n      event.preventDefault();<\/p>\n<p>      \/\/ Store event untuk later use<br \/>\n      this.deferredPrompt = event;<\/p>\n<p>      \/\/ Show install button<br \/>\n      this.showInstallButton();<br \/>\n    });<\/p>\n<p>    \/\/ Handle successful install<br \/>\n    window.addEventListener(&#8216;appinstalled&#8217;, (event) =&gt; {<br \/>\n      console.log(&#8216;PWA was installed&#8217;);<br \/>\n      this.hideInstallButton();<\/p>\n<p>      \/\/ Track installation analytics<br \/>\n      this.trackInstallation();<br \/>\n    });<br \/>\n  }<\/p>\n<p>  showInstallButton() {<br \/>\n    \/\/ Create or show install button<br \/>\n    if (!this.installButton) {<br \/>\n      this.installButton = document.createElement(&#8216;button&#8217;);<br \/>\n      this.installButton.innerHTML = `<\/p>\n<p>        Install App<br \/>\n      `;<br \/>\n      this.installButton.className = &#8216;pwa-install-button&#8217;;<br \/>\n      this.installButton.addEventListener(&#8216;click&#8217;, () =&gt; this.promptInstall());<\/p>\n<p>      document.body.appendChild(this.installButton);<br \/>\n    }<\/p>\n<p>    \/\/ Animate button appearance<br \/>\n    setTimeout(() =&gt; {<br \/>\n      this.installButton.classList.add(&#8216;show&#8217;);<br \/>\n    }, 1000);<br \/>\n  }<\/p>\n<p>  hideInstallButton() {<br \/>\n    if (this.installButton) {<br \/>\n      this.installButton.classList.remove(&#8216;show&#8217;);<br \/>\n      setTimeout(() =&gt; {<br \/>\n        if (this.installButton.parentNode) {<br \/>\n          this.installButton.parentNode.removeChild(this.installButton);<br \/>\n        }<br \/>\n      }, 300);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  async promptInstall() {<br \/>\n    if (!this.deferredPrompt) {<br \/>\n      console.log(&#8216;Install prompt not available&#8217;);<br \/>\n      return;<br \/>\n    }<\/p>\n<p>    try {<br \/>\n      \/\/ Show install prompt<br \/>\n      this.deferredPrompt.prompt();<\/p>\n<p>      \/\/ Wait for user response<br \/>\n      const { outcome } = await this.deferredPrompt.userChoice;<\/p>\n<p>      console.log(`User ${outcome} the install prompt`);<\/p>\n<p>      \/\/ Clear deferred prompt<br \/>\n      this.deferredPrompt = null;<\/p>\n<p>      if (outcome === &#8216;accepted&#8217;) {<br \/>\n        this.hideInstallButton();<br \/>\n      }<\/p>\n<p>    } catch (error) {<br \/>\n      console.error(&#8216;Error showing install prompt:&#8217;, error);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  trackInstallation() {<br \/>\n    \/\/ Send installation data ke analytics<br \/>\n    if (typeof gtag !== &#8216;undefined&#8217;) {<br \/>\n      gtag(&#8216;event&#8217;, &#8216;pwa_installed&#8217;, {<br \/>\n        &#8216;event_category&#8217;: &#8216;PWA&#8217;,<br \/>\n        &#8216;event_label&#8217;: &#8216;Install Success&#8217;<br \/>\n      });<br \/>\n    }<br \/>\n  }<\/p>\n<p>  \/\/ Check if PWA is already installed<br \/>\n  isInstalled() {<br \/>\n    \/\/ For iOS<br \/>\n    if (&#8216;standalone&#8217; in window.navigator &amp;&amp; window.navigator.standalone) {<br \/>\n      return true;<br \/>\n    }<\/p>\n<p>    \/\/ For Android<br \/>\n    if (window.matchMedia(&#8216;(display-mode: standalone)&#8217;).matches) {<br \/>\n      return true;<br \/>\n    }<\/p>\n<p>    return false;<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ Usage<br \/>\nconst installManager = new PWAInstallManager();<\/p>\n<p>\/\/ CSS untuk install button<br \/>\nconst pwaInstallStyles = `<br \/>\n  .pwa-install-button {<br \/>\n    position: fixed;<br \/>\n    bottom: 20px;<br \/>\n    right: 20px;<br \/>\n    background: #2563eb;<br \/>\n    color: white;<br \/>\n    border: none;<br \/>\n    padding: 12px 20px;<br \/>\n    border-radius: 50px;<br \/>\n    cursor: pointer;<br \/>\n    display: flex;<br \/>\n    align-items: center;<br \/>\n    gap: 8px;<br \/>\n    font-family: -apple-system, BlinkMacSystemFont, &#8216;Segoe UI&#8217;, Roboto, sans-serif;<br \/>\n    font-size: 16px;<br \/>\n    font-weight: 500;<br \/>\n    box-shadow: 0 4px 12px rgba(37, 99, 235, 0.3);<br \/>\n    transform: translateY(100px);<br \/>\n    opacity: 0;<br \/>\n    transition: all 0.3s ease;<br \/>\n    z-index: 1000;<br \/>\n  }<\/p>\n<p>  .pwa-install-button.show {<br \/>\n    transform: translateY(0);<br \/>\n    opacity: 1;<br \/>\n  }<\/p>\n<p>  .pwa-install-button:hover {<br \/>\n    background: #1d4ed8;<br \/>\n    transform: translateY(-2px);<br \/>\n    box-shadow: 0 6px 16px rgba(37, 99, 235, 0.4);<br \/>\n  }<\/p>\n<p>  .pwa-install-button:active {<br \/>\n    transform: translateY(0);<br \/>\n  }<\/p>\n<p>  @media (max-width: 768px) {<br \/>\n    .pwa-install-button {<br \/>\n      bottom: 16px;<br \/>\n      right: 16px;<br \/>\n      padding: 10px 16px;<br \/>\n      font-size: 14px;<br \/>\n    }<br \/>\n  }<br \/>\n`;<\/p>\n<p>\/\/ Inject styles<br \/>\nconst styleSheet = document.createElement(&#8216;style&#8217;);<br \/>\nstyleSheet.textContent = pwaInstallStyles;<br \/>\ndocument.head.appendChild(styleSheet);<br \/>\n&#8220;`<\/p>\n<p>Offline-First Architecture<\/p>\n<p>1. Application Shell Pattern<br \/>\n&#8220;`javascript<br \/>\n\/\/ Application Shell Manager<br \/>\nclass AppShellManager {<br \/>\n  constructor() {<br \/>\n    this.shellCache = &#8216;shell-v1&#8217;;<br \/>\n    this.contentCache = &#8216;content-v1&#8217;;<br \/>\n    this.setupShell();<br \/>\n  }<\/p>\n<p>  setupShell() {<br \/>\n    \/\/ Define shell components<br \/>\n    this.shellComponents = [<br \/>\n      &#8216;\/&#8217;,<br \/>\n      &#8216;\/index.html&#8217;,<br \/>\n      &#8216;\/css\/shell.css&#8217;,<br \/>\n      &#8216;\/js\/shell.js&#8217;,<br \/>\n      &#8216;\/js\/navigation.js&#8217;,<br \/>\n      &#8216;\/images\/logo.svg&#8217;,<br \/>\n      &#8216;\/manifest.json&#8217;<br \/>\n    ];<\/p>\n<p>    \/\/ Cache shell components<br \/>\n    this.cacheShell();<br \/>\n  }<\/p>\n<p>  async cacheShell() {<br \/>\n    if (&#8216;caches&#8217; in window) {<br \/>\n      const cache = await caches.open(this.shellCache);<br \/>\n      await cache.addAll(this.shellComponents);<br \/>\n    }<br \/>\n  }<\/p>\n<p>  \/\/ Load shell immediately<br \/>\n  async loadShell() {<br \/>\n    try {<br \/>\n      const cache = await caches.open(this.shellCache);<br \/>\n      const shellResponse = await cache.match(&#8216;\/js\/shell.js&#8217;);<\/p>\n<p>      if (shellResponse) {<br \/>\n        const shellCode = await shellResponse.text();<br \/>\n        eval(shellCode); \/\/ Execute shell code<br \/>\n        return true;<br \/>\n      }<br \/>\n    } catch (error) {<br \/>\n      console.error(&#8216;Failed to load shell:&#8217;, error);<br \/>\n    }<\/p>\n<p>    return false;<br \/>\n  }<\/p>\n<p>  \/\/ Dynamic content loading<br \/>\n  async loadContent(url) {<br \/>\n    try {<br \/>\n      \/\/ Try network first<br \/>\n      const response = await fetch(url);<\/p>\n<p>      if (response.ok) {<br \/>\n        const content = await response.text();<\/p>\n<p>        \/\/ Cache content for offline<br \/>\n        const cache = await caches.open(this.contentCache);<br \/>\n        await cache.put(url, new Response(content));<\/p>\n<p>        return content;<br \/>\n      }<br \/>\n    } catch (error) {<br \/>\n      \/\/ Fallback to cache<br \/>\n      const cache = await caches.open(this.contentCache);<br \/>\n      const cachedResponse = await cache.match(url);<\/p>\n<p>      if (cachedResponse) {<br \/>\n        return cachedResponse.text();<br \/>\n      }<br \/>\n    }<\/p>\n<p>    return null;<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ Shell implementation<br \/>\nconst appShell = `<\/p>\n<p>  <title>My PWA App<\/title><\/p>\n<p>  <!-- App Shell Structure --><\/p>\n<header class=\"app-header\">\n<nav class=\"app-nav\">\n<div class=\"nav-brand\">\n        <img decoding=\"async\" src=\"\/images\/logo.svg\" alt=\"PWA App\" class=\"brand-logo\">\n      <\/div>\n<p>      <button class=\"menu-toggle\" aria-label=\"Toggle menu\"><br \/>\n        <span><\/span><br \/>\n        <span><\/span><br \/>\n        <span><\/span><br \/>\n      <\/button><br \/>\n    <\/nav>\n<\/header>\n<p>  <main class=\"app-main\"><br \/>\n    <!-- Content akan dimuat di sini --><\/p>\n<div id=\"content\">\n<div class=\"loading-skeleton\">\n<div class=\"skeleton-header\"><\/div>\n<div class=\"skeleton-line\"><\/div>\n<div class=\"skeleton-line short\"><\/div>\n<div class=\"skeleton-line\"><\/div>\n<\/p><\/div>\n<\/p><\/div>\n<p>  <\/main><\/p>\n<footer class=\"app-footer\">\n<nav class=\"bottom-nav\">\n      <a href=\"\/\" class=\"nav-item active\" data-page=\"home\"><\/p>\n<p>        <span>Home<\/span><br \/>\n      <\/a><br \/>\n      <a href=\"\/tasks\" class=\"nav-item\" data-page=\"tasks\"><\/p>\n<p>        <span>Tasks<\/span><br \/>\n      <\/a><br \/>\n      <a href=\"\/profile\" class=\"nav-item\" data-page=\"profile\"><\/p>\n<p>        <span>Profile<\/span><br \/>\n      <\/a><br \/>\n    <\/nav>\n<\/footer>\n<p>`;<br \/>\n&#8220;`<\/p>\n<p>Performance Optimization<\/p>\n<p>1. Resource Optimization<br \/>\n&#8220;`javascript<br \/>\n\/\/ Performance optimization manager<br \/>\nclass PerformanceManager {<br \/>\n  constructor() {<br \/>\n    this.setupPerformanceMonitoring();<br \/>\n    this.optimizeResources();<br \/>\n  }<\/p>\n<p>  setupPerformanceMonitoring() {<br \/>\n    \/\/ Monitor Core Web Vitals<br \/>\n    this.measureCoreWebVitals();<\/p>\n<p>    \/\/ Monitor custom metrics<br \/>\n    this.setupCustomMetrics();<br \/>\n  }<\/p>\n<p>  measureCoreWebVitals() {<br \/>\n    \/\/ Largest Contentful Paint (LCP)<br \/>\n    new PerformanceObserver((entryList) =&gt; {<br \/>\n      const entries = entryList.getEntries();<br \/>\n      const lastEntry = entries[entries.length &#8211; 1];<br \/>\n      console.log(&#8216;LCP:&#8217;, lastEntry.startTime);<\/p>\n<p>      \/\/ Send ke analytics<br \/>\n      this.sendMetric(&#8216;LCP&#8217;, lastEntry.startTime);<br \/>\n    }).observe({ entryTypes: [&#8216;largest-contentful-paint&#8217;] });<\/p>\n<p>    \/\/ First Input Delay (FID)<br \/>\n    new PerformanceObserver((entryList) =&gt; {<br \/>\n      const entries = entryList.getEntries();<br \/>\n      entries.forEach((entry) =&gt; {<br \/>\n        console.log(&#8216;FID:&#8217;, entry.processingStart &#8211; entry.startTime);<br \/>\n        this.sendMetric(&#8216;FID&#8217;, entry.processingStart &#8211; entry.startTime);<br \/>\n      });<br \/>\n    }).observe({ entryTypes: [&#8216;first-input&#8217;] });<\/p>\n<p>    \/\/ Cumulative Layout Shift (CLS)<br \/>\n    let clsValue = 0;<br \/>\n    new PerformanceObserver((entryList) =&gt; {<br \/>\n      for (const entry of entryList.getEntries()) {<br \/>\n        if (!entry.hadRecentInput) {<br \/>\n          clsValue += entry.value;<br \/>\n        }<br \/>\n      }<br \/>\n      console.log(&#8216;CLS:&#8217;, clsValue);<br \/>\n      this.sendMetric(&#8216;CLS&#8217;, clsValue);<br \/>\n    }).observe({ entryTypes: [&#8216;layout-shift&#8217;] });<br \/>\n  }<\/p>\n<p>  setupCustomMetrics() {<br \/>\n    \/\/ Time to Interactive<br \/>\n    const measureTTI = () =&gt; {<br \/>\n      const tti = performance.now() &#8211; navigationTiming.loadEventEnd;<br \/>\n      console.log(&#8216;TTI:&#8217;, tti);<br \/>\n      this.sendMetric(&#8216;TTI&#8217;, tti);<br \/>\n    };<\/p>\n<p>    if (document.readyState === &#8216;complete&#8217;) {<br \/>\n      setTimeout(measureTTI, 0);<br \/>\n    } else {<br \/>\n      window.addEventListener(&#8216;load&#8217;, () =&gt; {<br \/>\n        setTimeout(measureTTI, 0);<br \/>\n      });<br \/>\n    }<br \/>\n  }<\/p>\n<p>  optimizeResources() {<br \/>\n    \/\/ Lazy loading images<br \/>\n    this.setupLazyLoading();<\/p>\n<p>    \/\/ Preload critical resources<br \/>\n    this.preloadCriticalResources();<\/p>\n<p>    \/\/ Optimize fonts<br \/>\n    this.optimizeFonts();<br \/>\n  }<\/p>\n<p>  setupLazyLoading() {<br \/>\n    const images = document.querySelectorAll(&#8216;img[data-src]&#8217;);<\/p>\n<p>    const imageObserver = new IntersectionObserver((entries, observer) =&gt; {<br \/>\n      entries.forEach(entry =&gt; {<br \/>\n        if (entry.isIntersecting) {<br \/>\n          const img = entry.target;<br \/>\n          img.src = img.dataset.src;<br \/>\n          img.classList.remove(&#8216;lazy&#8217;);<br \/>\n          imageObserver.unobserve(img);<br \/>\n        }<br \/>\n      });<br \/>\n    });<\/p>\n<p>    images.forEach(img =&gt; imageObserver.observe(img));<br \/>\n  }<\/p>\n<p>  preloadCriticalResources() {<br \/>\n    const criticalResources = [<br \/>\n      { href: &#8216;\/css\/critical.css&#8217;, as: &#8216;style&#8217; },<br \/>\n      { href: &#8216;\/js\/critical.js&#8217;, as: &#8216;script&#8217; },<br \/>\n      { href: &#8216;\/fonts\/inter.woff2&#8217;, as: &#8216;font&#8217;, type: &#8216;font\/woff2&#8217;, crossorigin: &#8216;true&#8217; }<br \/>\n    ];<\/p>\n<p>    criticalResources.forEach(resource =&gt; {<br \/>\n      const link = document.createElement(&#8216;link&#8217;);<br \/>\n      link.rel = &#8216;preload&#8217;;<br \/>\n      link.href = resource.href;<br \/>\n      link.as = resource.as;<\/p>\n<p>      if (resource.type) link.type = resource.type;<br \/>\n      if (resource.crossorigin) link.crossOrigin = resource.crossorigin;<\/p>\n<p>      document.head.appendChild(link);<br \/>\n    });<br \/>\n  }<\/p>\n<p>  optimizeFonts() {<br \/>\n    \/\/ Font display strategy<br \/>\n    const fontDisplay = `<br \/>\n      @font-face {<br \/>\n        font-family: &#8216;Inter&#8217;;<br \/>\n        font-style: normal;<br \/>\n        font-weight: 400;<br \/>\n        font-display: swap;<br \/>\n        src: url(&#8216;\/fonts\/inter-regular.woff2&#8217;) format(&#8216;woff2&#8217;);<br \/>\n      }<br \/>\n    `;<\/p>\n<p>    const styleSheet = document.createElement(&#8216;style&#8217;);<br \/>\n    styleSheet.textContent = fontDisplay;<br \/>\n    document.head.appendChild(styleSheet);<br \/>\n  }<\/p>\n<p>  sendMetric(name, value) {<br \/>\n    \/\/ Send ke analytics service<br \/>\n    if (typeof gtag !== &#8216;undefined&#8217;) {<br \/>\n      gtag(&#8216;event&#8217;, &#8216;web_vital&#8217;, {<br \/>\n        &#8216;event_category&#8217;: &#8216;Performance&#8217;,<br \/>\n        &#8216;event_label&#8217;: name,<br \/>\n        &#8216;value&#8217;: Math.round(value)<br \/>\n      });<br \/>\n    }<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ Initialize performance manager<br \/>\nconst performanceManager = new PerformanceManager();<br \/>\n&#8220;`<\/p>\n<p>Testing PWA<\/p>\n<p>1. PWA Testing Framework<br \/>\n&#8220;`javascript<br \/>\n\/\/ PWA Testing Utilities<br \/>\nclass PWATester {<br \/>\n  constructor() {<br \/>\n    this.tests = [];<br \/>\n    this.results = [];<br \/>\n  }<\/p>\n<p>  \/\/ Add test case<br \/>\n  addTest(name, testFunction, category = &#8216;general&#8217;) {<br \/>\n    this.tests.push({ name, testFunction, category });<br \/>\n  }<\/p>\n<p>  \/\/ Run all tests<br \/>\n  async runTests() {<br \/>\n    this.results = [];<\/p>\n<p>    for (const test of this.tests) {<br \/>\n      try {<br \/>\n        const result = await test.testFunction();<br \/>\n        this.results.push({<br \/>\n          name: test.name,<br \/>\n          category: test.category,<br \/>\n          passed: result.passed,<br \/>\n          message: result.message,<br \/>\n          details: result.details<br \/>\n        });<br \/>\n      } catch (error) {<br \/>\n        this.results.push({<br \/>\n          name: test.name,<br \/>\n          category: test.category,<br \/>\n          passed: false,<br \/>\n          message: `Test failed: ${error.message}`,<br \/>\n          details: { error: error.stack }<br \/>\n        });<br \/>\n      }<br \/>\n    }<\/p>\n<p>    return this.results;<br \/>\n  }<\/p>\n<p>  \/\/ Get test results summary<br \/>\n  getSummary() {<br \/>\n    const passed = this.results.filter(r =&gt; r.passed).length;<br \/>\n    const total = this.results.length;<\/p>\n<p>    return {<br \/>\n      passed,<br \/>\n      failed: total &#8211; passed,<br \/>\n      total,<br \/>\n      passRate: (passed \/ total) * 100<br \/>\n    };<br \/>\n  }<\/p>\n<p>  \/\/ Generate HTML report<br \/>\n  generateReport() {<br \/>\n    const summary = this.getSummary();<\/p>\n<p>    let html = `<\/p>\n<div class=\"pwa-test-report\">\n<h2>PWA Test Results<\/h2>\n<div class=\"summary\">\n          <span class=\"passed\">Passed: ${summary.passed}<\/span><br \/>\n          <span class=\"failed\">Failed: ${summary.failed}<\/span><br \/>\n          <span class=\"total\">Total: ${summary.total}<\/span><br \/>\n          <span class=\"pass-rate\">Pass Rate: ${summary.passRate.toFixed(1)}%<\/span>\n        <\/div>\n<div class=\"results\">\n    `;<\/p>\n<p>    const categories = [&#8230;new Set(this.tests.map(t =&gt; t.category))];<\/p>\n<p>    categories.forEach(category =&gt; {<br \/>\n      html += `<\/p>\n<h3>${category}<\/h3>\n<p>`;<\/p>\n<p>      const categoryResults = this.results.filter(r =&gt; {<br \/>\n        const test = this.tests.find(t =&gt; t.name === r.name);<br \/>\n        return test &amp;&amp; test.category === category;<br \/>\n      });<\/p>\n<p>      categoryResults.forEach(result =&gt; {<br \/>\n        html += `<\/p>\n<div class=\"test-result ${result.passed ? 'passed' : 'failed'}\">\n            <span class=\"test-name\">${result.name}<\/span><br \/>\n            <span class=\"test-status\">${result.passed ? &#8216;\u2713&#8217; : &#8216;\u2717&#8217;}<\/span><\/p>\n<div class=\"test-message\">${result.message}<\/div>\n<p>            ${result.details ? `<\/p>\n<pre>${JSON.stringify(result.details, null, 2)}<\/pre>\n<p>` : &#8221;}\n          <\/p><\/div>\n<p>        `;<br \/>\n      });<br \/>\n    });<\/p>\n<p>    html += `\n        <\/p><\/div>\n<\/p><\/div>\n<p>    `;<\/p>\n<p>    return html;<br \/>\n  }<br \/>\n}<\/p>\n<p>\/\/ Common PWA tests<br \/>\nconst pwaTests = new PWATester();<\/p>\n<p>\/\/ Service Worker test<br \/>\npwaTests.addTest(&#8216;Service Worker Registration&#8217;, async () =&gt; {<br \/>\n  if (!(&#8216;serviceWorker&#8217; in navigator)) {<br \/>\n    return { passed: false, message: &#8216;Service Worker not supported&#8217; };<br \/>\n  }<\/p>\n<p>  const registration = await navigator.serviceWorker.ready;<br \/>\n  if (registration.active) {<br \/>\n    return { passed: true, message: &#8216;Service Worker is active&#8217; };<br \/>\n  } else {<br \/>\n    return { passed: false, message: &#8216;Service Worker not active&#8217; };<br \/>\n  }<br \/>\n}, &#8216;core&#8217;);<\/p>\n<p>\/\/ Manifest test<br \/>\npwaTests.addTest(&#8216;Web App Manifest&#8217;, async () =&gt; {<br \/>\n  try {<br \/>\n    const response = await fetch(&#8216;\/manifest.json&#8217;);<br \/>\n    if (response.ok) {<br \/>\n      const manifest = await response.json();<\/p>\n<p>      const requiredFields = [&#8216;name&#8217;, &#8216;short_name&#8217;, &#8216;start_url&#8217;, &#8216;display&#8217;];<br \/>\n      const missingFields = requiredFields.filter(field =&gt; !manifest[field]);<\/p>\n<p>      if (missingFields.length === 0) {<br \/>\n        return { passed: true, message: &#8216;Manifest has all required fields&#8217;, details: manifest };<br \/>\n      } else {<br \/>\n        return { passed: false, message: `Missing fields: ${missingFields.join(&#8216;, &#8216;)}` };<br \/>\n      }<br \/>\n    } else {<br \/>\n      return { passed: false, message: &#8216;Manifest not found&#8217; };<br \/>\n    }<br \/>\n  } catch (error) {<br \/>\n    return { passed: false, message: &#8216;Error loading manifest&#8217;, details: error.message };<br \/>\n  }<br \/>\n}, &#8216;core&#8217;);<\/p>\n<p>\/\/ HTTPS test<br \/>\npwaTests.addTest(&#8216;HTTPS Connection&#8217;, async () =&gt; {<br \/>\n  const isHTTPS = location.protocol === &#8216;https:&#8217; || location.hostname === &#8216;localhost&#8217;;<\/p>\n<p>  if (isHTTPS) {<br \/>\n    return { passed: true, message: &#8216;Site is served over HTTPS&#8217; };<br \/>\n  } else {<br \/>\n    return { passed: false, message: &#8216;Site must be served over HTTPS for PWA&#8217; };<br \/>\n  }<br \/>\n}, &#8216;core&#8217;);<\/p>\n<p>\/\/ Responsive design test<br \/>\npwaTests.addTest(&#8216;Responsive Design&#8217;, async () =&gt; {<br \/>\n  const viewportMeta = document.querySelector(&#8216;meta[name=&#8221;viewport&#8221;]&#8217;);<br \/>\n  const hasViewportMeta = viewportMeta &amp;&amp; viewportMeta.content.includes(&#8216;width=device-width&#8217;);<\/p>\n<p>  return {<br \/>\n    passed: hasViewportMeta,<br \/>\n    message: hasViewportMeta ? &#8216;Viewport meta tag present&#8217; : &#8216;Missing viewport meta tag&#8217;<br \/>\n  };<br \/>\n}, &#8216;ux&#8217;);<\/p>\n<p>\/\/ Offline functionality test<br \/>\npwaTests.addTest(&#8216;Offline Functionality&#8217;, async () =&gt; {<br \/>\n  try {<br \/>\n    const response = await fetch(&#8216;\/&#8217;);<br \/>\n    const isFromCache = response.headers.get(&#8216;X-From-Service-Worker&#8217;);<\/p>\n<p>    return {<br \/>\n      passed: true,<br \/>\n      message: &#8216;Page loads successfully&#8217;,<br \/>\n      details: { fromCache: !!isFromCache }<br \/>\n    };<br \/>\n  } catch (error) {<br \/>\n    return { passed: false, message: &#8216;Failed to load page&#8217;, details: error.message };<br \/>\n  }<br \/>\n}, &#8216;performance&#8217;);<\/p>\n<p>\/\/ Run tests dan display results<br \/>\nasync function runPWATests() {<br \/>\n  const results = await pwaTests.runTests();<br \/>\n  const report = pwaTests.generateReport();<\/p>\n<p>  \/\/ Display report<br \/>\n  const reportContainer = document.createElement(&#8216;div&#8217;);<br \/>\n  reportContainer.innerHTML = report;<br \/>\n  document.body.appendChild(reportContainer);<\/p>\n<p>  console.log(&#8216;PWA Test Results:&#8217;, results);<br \/>\n}<\/p>\n<p>\/\/ Usage di development<br \/>\nif (process.env.NODE_ENV === &#8216;development&#8217;) {<br \/>\n  runPWATests();<br \/>\n}<br \/>\n&#8220;`<\/p>\n<p>2. Lighthouse CI Integration<br \/>\n&#8220;`yaml<br \/>\n# .github\/workflows\/lighthouse.yml<br \/>\nname: Lighthouse CI<\/p>\n<p>on:<br \/>\n  push:<br \/>\n    branches: [ main ]<br \/>\n  pull_request:<br \/>\n    branches: [ main ]<\/p>\n<p>jobs:<br \/>\n  lighthouse:<br \/>\n    runs-on: ubuntu-latest<\/p>\n<p>    steps:<br \/>\n    &#8211; uses: actions\/checkout@v3<\/p>\n<p>    &#8211; name: Setup Node.js<br \/>\n      uses: actions\/setup-node@v3<br \/>\n      with:<br \/>\n        node-version: &#8217;18&#8217;<\/p>\n<p>    &#8211; name: Install dependencies<br \/>\n      run: npm install<\/p>\n<p>    &#8211; name: Build application<br \/>\n      run: npm run build<\/p>\n<p>    &#8211; name: Run Lighthouse CI<br \/>\n      run: |<br \/>\n        npm install -g @lhci\/cli@0.12.x<br \/>\n        lhci autorun<br \/>\n      env:<br \/>\n        LHCI_GITHUB_APP_TOKEN: ${{ secrets.LHCI_GITHUB_APP_TOKEN }}<\/p>\n<p>    &#8211; name: Upload Lighthouse results<br \/>\n      uses: actions\/upload-artifact@v3<br \/>\n      with:<br \/>\n        name: lighthouse-results<br \/>\n        path: &#8216;.lighthouseci&#8217;<br \/>\n&#8220;`<\/p>\n<p>PWA Best Practices dan Guidelines<\/p>\n<p>1. Performance Guidelines<br \/>\n\u2022 Time to Interactive: &lt; 5 seconds on 3G<br \/>\n\u2022 First Contentful Paint: &lt; 1.5 seconds<br \/>\n\u2022 Largest Contentful Paint: &lt; 2.5 seconds<br \/>\n\u2022 Cumulative Layout Shift: &lt; 0.1<\/p>\n<p>2. Security Requirements<br \/>\n\u2022 HTTPS Mandatory: All PWA resources must be served over HTTPS<br \/>\n\u2022 CSP Headers: Implement Content Security Policy<br \/>\n\u2022 Service Worker Security: Validate all requests<br \/>\n\u2022 Data Protection: Secure user data storage<\/p>\n<p>3. User Experience Guidelines<br \/>\n\u2022 Offline First: Design for offline functionality first<br \/>\n\u2022 Progressive Enhancement: Works di semua browsers<br \/>\n\u2022 Responsive Design: Optimize untuk semua screen sizes<br \/>\n\u2022 Accessibility: WCAG 2.1 AA compliance<\/p>\n<p>Future of PWA Technology<\/p>\n<p>1. Emerging Features<br \/>\n\u2022 Web Share Target Level 2: Enhanced sharing capabilities<br \/>\n\u2022 Web OTP: One-time password API<br \/>\n\u2022 File System Access: Direct file system access<br \/>\n\u2022 Web NFC: Near field communication<br \/>\n\u2022 Background Fetch: Background data synchronization<\/p>\n<p>2. Platform Integration<br \/>\n\u2022 Desktop PWA: Enhanced desktop integration<br \/>\n\u2022 Windows 11 Widgets: PWA as Windows widgets<br \/>\n\u2022 macOS Integration: Native macOS features<br \/>\n\u2022 Chrome OS: Deep OS integration<\/p>\n<p>3. Performance Improvements<br \/>\n\u2022 WebAssembly Integration: High-performance computing<br \/>\n\u2022 WebGPU: Graphics acceleration<br \/>\n\u2022 WebCodecs: Media processing<br \/>\n\u2022 Portals: Seamless navigation between apps<\/p>\n<p>Kesimpulan<\/p>\n<p>Progressive Web Apps represent evolution dalam web development, menggabungkan keunggulan web reachability dengan native app experience. Dengan proper implementation dari service workers, web app manifest, dan modern web capabilities, PWA dapat deliver exceptional user experience across all platforms.<\/p>\n<p>Key success factors:<br \/>\n\u2022 Performance: Optimize untuk Core Web Vitals<br \/>\n\u2022 Offline Capability: Design offline-first architecture<br \/>\n\u2022 User Experience: Native-like interactions dan animations<br \/>\n\u2022 Security: HTTPS implementation dan secure coding practices<br \/>\n\u2022 Testing: Comprehensive PWA testing strategy<\/p>\n<p>Investasi dalam PWA development akan memberikan benefits:<br \/>\n&#8211; Increased user engagement dan retention<br \/>\n&#8211; Improved performance dan reliability<br \/>\n&#8211; Better conversion rates<br \/>\n&#8211; Reduced development costs<br \/>\n&#8211; Cross-platform compatibility<\/p>\n<p>Start dengan implementing core PWA features, measure performance, dan iterate based on user feedback. PWA adalah future-proof approach untuk modern web applications.<\/p>\n","protected":false},"excerpt":{"rendered":"<p>Progressive Web Apps (PWA) telah merevolusi cara kita membangun aplikasi web, menggabungkan keunggulan web dan native applications. Di tahun 2025, PWA menjadi standard untuk aplikasi web modern yang memberikan pengalaman seperti native app namun tetap accessible melalui browser. Artikel ini akan memberikan panduan lengkap untuk membangun PWA yang powerful dan user-friendly. Apa itu Progressive Web [&hellip;]<\/p>\n","protected":false},"author":6,"featured_media":4422,"comment_status":"closed","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":[],"categories":[1],"tags":[],"yoast_head":"<!-- This site is optimized with the Yoast SEO plugin v20.8 - https:\/\/yoast.com\/wordpress\/plugins\/seo\/ -->\n<title>Progressive Web App (PWA) Complete Guide: Membangun Aplikasi Web Modern - Demo Website<\/title>\n<meta name=\"robots\" content=\"index, follow, max-snippet:-1, max-image-preview:large, max-video-preview:-1\" \/>\n<link rel=\"canonical\" href=\"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/\" \/>\n<meta property=\"og:locale\" content=\"en_US\" \/>\n<meta property=\"og:type\" content=\"article\" \/>\n<meta property=\"og:title\" content=\"Progressive Web App (PWA) Complete Guide: Membangun Aplikasi Web Modern - Demo Website\" \/>\n<meta property=\"og:description\" content=\"Progressive Web Apps (PWA) telah merevolusi cara kita membangun aplikasi web, menggabungkan keunggulan web dan native applications. Di tahun 2025, PWA menjadi standard untuk aplikasi web modern yang memberikan pengalaman seperti native app namun tetap accessible melalui browser. Artikel ini akan memberikan panduan lengkap untuk membangun PWA yang powerful dan user-friendly. Apa itu Progressive Web [&hellip;]\" \/>\n<meta property=\"og:url\" content=\"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/\" \/>\n<meta property=\"og:site_name\" content=\"Demo Website\" \/>\n<meta property=\"article:published_time\" content=\"2025-11-11T01:17:48+00:00\" \/>\n<meta property=\"og:image\" content=\"http:\/\/www.jagowebdesign.com\/website\/wp-content\/uploads\/2025\/11\/website-7.jpg\" \/>\n\t<meta property=\"og:image:width\" content=\"1024\" \/>\n\t<meta property=\"og:image:height\" content=\"1024\" \/>\n\t<meta property=\"og:image:type\" content=\"image\/jpeg\" \/>\n<meta name=\"author\" content=\"redakturjagowebdesign\" \/>\n<meta name=\"twitter:card\" content=\"summary_large_image\" \/>\n<meta name=\"twitter:label1\" content=\"Written by\" \/>\n\t<meta name=\"twitter:data1\" content=\"redakturjagowebdesign\" \/>\n<script type=\"application\/ld+json\" class=\"yoast-schema-graph\">{\"@context\":\"https:\/\/schema.org\",\"@graph\":[{\"@type\":\"WebPage\",\"@id\":\"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/\",\"url\":\"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/\",\"name\":\"Progressive Web App (PWA) Complete Guide: Membangun Aplikasi Web Modern - Demo Website\",\"isPartOf\":{\"@id\":\"https:\/\/www.jagowebdesign.com\/website\/#website\"},\"datePublished\":\"2025-11-11T01:17:48+00:00\",\"dateModified\":\"2025-11-11T01:17:48+00:00\",\"author\":{\"@id\":\"https:\/\/www.jagowebdesign.com\/website\/#\/schema\/person\/9c4f0b34abafcb25285cfc51e9459095\"},\"breadcrumb\":{\"@id\":\"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/#breadcrumb\"},\"inLanguage\":\"en-US\",\"potentialAction\":[{\"@type\":\"ReadAction\",\"target\":[\"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/\"]}]},{\"@type\":\"BreadcrumbList\",\"@id\":\"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Home\",\"item\":\"https:\/\/www.jagowebdesign.com\/website\/\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Progressive Web App (PWA) Complete Guide: Membangun Aplikasi Web Modern\"}]},{\"@type\":\"WebSite\",\"@id\":\"https:\/\/www.jagowebdesign.com\/website\/#website\",\"url\":\"https:\/\/www.jagowebdesign.com\/website\/\",\"name\":\"Demo Website\",\"description\":\"Jagowebdesign.Com\",\"potentialAction\":[{\"@type\":\"SearchAction\",\"target\":{\"@type\":\"EntryPoint\",\"urlTemplate\":\"https:\/\/www.jagowebdesign.com\/website\/?s={search_term_string}\"},\"query-input\":\"required name=search_term_string\"}],\"inLanguage\":\"en-US\"},{\"@type\":\"Person\",\"@id\":\"https:\/\/www.jagowebdesign.com\/website\/#\/schema\/person\/9c4f0b34abafcb25285cfc51e9459095\",\"name\":\"redakturjagowebdesign\",\"image\":{\"@type\":\"ImageObject\",\"inLanguage\":\"en-US\",\"@id\":\"https:\/\/www.jagowebdesign.com\/website\/#\/schema\/person\/image\/\",\"url\":\"https:\/\/secure.gravatar.com\/avatar\/cf55dfe07e97818622d2a46e2c6de4b1?s=96&d=mm&r=g\",\"contentUrl\":\"https:\/\/secure.gravatar.com\/avatar\/cf55dfe07e97818622d2a46e2c6de4b1?s=96&d=mm&r=g\",\"caption\":\"redakturjagowebdesign\"},\"url\":\"https:\/\/www.jagowebdesign.com\/website\/author\/redakturjagowebdesign\/\"}]}<\/script>\n<!-- \/ Yoast SEO plugin. -->","yoast_head_json":{"title":"Progressive Web App (PWA) Complete Guide: Membangun Aplikasi Web Modern - Demo Website","robots":{"index":"index","follow":"follow","max-snippet":"max-snippet:-1","max-image-preview":"max-image-preview:large","max-video-preview":"max-video-preview:-1"},"canonical":"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/","og_locale":"en_US","og_type":"article","og_title":"Progressive Web App (PWA) Complete Guide: Membangun Aplikasi Web Modern - Demo Website","og_description":"Progressive Web Apps (PWA) telah merevolusi cara kita membangun aplikasi web, menggabungkan keunggulan web dan native applications. Di tahun 2025, PWA menjadi standard untuk aplikasi web modern yang memberikan pengalaman seperti native app namun tetap accessible melalui browser. Artikel ini akan memberikan panduan lengkap untuk membangun PWA yang powerful dan user-friendly. Apa itu Progressive Web [&hellip;]","og_url":"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/","og_site_name":"Demo Website","article_published_time":"2025-11-11T01:17:48+00:00","og_image":[{"width":1024,"height":1024,"url":"http:\/\/www.jagowebdesign.com\/website\/wp-content\/uploads\/2025\/11\/website-7.jpg","type":"image\/jpeg"}],"author":"redakturjagowebdesign","twitter_card":"summary_large_image","twitter_misc":{"Written by":"redakturjagowebdesign"},"schema":{"@context":"https:\/\/schema.org","@graph":[{"@type":"WebPage","@id":"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/","url":"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/","name":"Progressive Web App (PWA) Complete Guide: Membangun Aplikasi Web Modern - Demo Website","isPartOf":{"@id":"https:\/\/www.jagowebdesign.com\/website\/#website"},"datePublished":"2025-11-11T01:17:48+00:00","dateModified":"2025-11-11T01:17:48+00:00","author":{"@id":"https:\/\/www.jagowebdesign.com\/website\/#\/schema\/person\/9c4f0b34abafcb25285cfc51e9459095"},"breadcrumb":{"@id":"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/#breadcrumb"},"inLanguage":"en-US","potentialAction":[{"@type":"ReadAction","target":["https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/"]}]},{"@type":"BreadcrumbList","@id":"https:\/\/www.jagowebdesign.com\/website\/progressive-web-app-pwa-complete-guide-membangun-aplikasi-web-modern\/#breadcrumb","itemListElement":[{"@type":"ListItem","position":1,"name":"Home","item":"https:\/\/www.jagowebdesign.com\/website\/"},{"@type":"ListItem","position":2,"name":"Progressive Web App (PWA) Complete Guide: Membangun Aplikasi Web Modern"}]},{"@type":"WebSite","@id":"https:\/\/www.jagowebdesign.com\/website\/#website","url":"https:\/\/www.jagowebdesign.com\/website\/","name":"Demo Website","description":"Jagowebdesign.Com","potentialAction":[{"@type":"SearchAction","target":{"@type":"EntryPoint","urlTemplate":"https:\/\/www.jagowebdesign.com\/website\/?s={search_term_string}"},"query-input":"required name=search_term_string"}],"inLanguage":"en-US"},{"@type":"Person","@id":"https:\/\/www.jagowebdesign.com\/website\/#\/schema\/person\/9c4f0b34abafcb25285cfc51e9459095","name":"redakturjagowebdesign","image":{"@type":"ImageObject","inLanguage":"en-US","@id":"https:\/\/www.jagowebdesign.com\/website\/#\/schema\/person\/image\/","url":"https:\/\/secure.gravatar.com\/avatar\/cf55dfe07e97818622d2a46e2c6de4b1?s=96&d=mm&r=g","contentUrl":"https:\/\/secure.gravatar.com\/avatar\/cf55dfe07e97818622d2a46e2c6de4b1?s=96&d=mm&r=g","caption":"redakturjagowebdesign"},"url":"https:\/\/www.jagowebdesign.com\/website\/author\/redakturjagowebdesign\/"}]}},"_links":{"self":[{"href":"https:\/\/www.jagowebdesign.com\/website\/wp-json\/wp\/v2\/posts\/4442"}],"collection":[{"href":"https:\/\/www.jagowebdesign.com\/website\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.jagowebdesign.com\/website\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.jagowebdesign.com\/website\/wp-json\/wp\/v2\/users\/6"}],"replies":[{"embeddable":true,"href":"https:\/\/www.jagowebdesign.com\/website\/wp-json\/wp\/v2\/comments?post=4442"}],"version-history":[{"count":1,"href":"https:\/\/www.jagowebdesign.com\/website\/wp-json\/wp\/v2\/posts\/4442\/revisions"}],"predecessor-version":[{"id":4443,"href":"https:\/\/www.jagowebdesign.com\/website\/wp-json\/wp\/v2\/posts\/4442\/revisions\/4443"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/www.jagowebdesign.com\/website\/wp-json\/wp\/v2\/media\/4422"}],"wp:attachment":[{"href":"https:\/\/www.jagowebdesign.com\/website\/wp-json\/wp\/v2\/media?parent=4442"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.jagowebdesign.com\/website\/wp-json\/wp\/v2\/categories?post=4442"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.jagowebdesign.com\/website\/wp-json\/wp\/v2\/tags?post=4442"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}