Skip to content

Instantly share code, notes, and snippets.

@JuanVqz
Last active April 13, 2026 19:19
Show Gist options
  • Select an option

  • Save JuanVqz/60f470c538fe16177e4e2d46ab47ea44 to your computer and use it in GitHub Desktop.

Select an option

Save JuanVqz/60f470c538fe16177e4e2d46ab47ea44 to your computer and use it in GitHub Desktop.
PWA en Rails - Demo 4: Multi-Store | RubySur 2026

PWA en Rails - Demo 4: Multi-Store

Charla "PWA en Rails" - RubySur - 13 de abril de 2026 Video: https://www.youtube.com/watch?v=ppxalpIKpGg

Cada subdominio (store) se instala como su propia PWA con nombre, logo y cache independiente. Incluye el fix para que current_store funcione en vistas del Rails::PwaController.

Archivos

  • manifest.json.erb -> app/views/pwa/manifest.json.erb (personalizado por store)
  • service-worker.js.erb -> app/views/pwa/service-worker.js.erb (cache nombrado por store)
  • application_helper.rb -> fix en app/helpers/application_helper.rb

Repo

# PWA en Rails - Demo 4 | RubySur - 13/04/2026
# app/helpers/application_helper.rb
#
# Disponible en todas las vistas, incluyendo las de Rails::PwaController
# que NO hereda de nuestro ApplicationController.
module ApplicationHelper
def current_store
Current.store || Store.find_by(subdomain: request.subdomain)
end
end
<%# app/views/pwa/manifest.json.erb -- personalizado por store %>
{
"name": "<%= current_store&.name || 'May Store' %>",
"short_name": "<%= current_store&.name&.truncate(12) || 'MayStore' %>",
"icons": [
{
"src": "<%= current_store&.logo_url || '/icon.png' %>",
"type": "image/png",
"sizes": "512x512"
},
{
"src": "<%= current_store&.logo_url || '/icon.png' %>",
"type": "image/png",
"sizes": "512x512",
"purpose": "maskable"
}
],
"start_url": "/",
"display": "standalone",
"scope": "/",
"description": "MayStore.",
"theme_color": "red",
"background_color": "red"
}
// PWA en Rails - Demo 4 | RubySur - 13/04/2026
// app/views/pwa/service-worker.js.erb
// Cache nombrado por store para facilitar debugging
const CACHE_NAME = "may-store-<%= current_store&.subdomain || 'public' %>-v1"
// 1. Install: pre-cachear la pagina offline
self.addEventListener("install", (event) => {
event.waitUntil(
caches.open(CACHE_NAME)
.then((cache) => cache.addAll(["/offline.html"]))
)
self.skipWaiting()
})
// 2. Activate: limpiar caches viejos
self.addEventListener("activate", (event) => {
event.waitUntil(
caches.keys().then((names) =>
Promise.all(
names.filter((n) => n !== CACHE_NAME)
.map((n) => caches.delete(n))
)
)
)
self.clients.claim()
})
// 3. Fetch: estrategia mixta
self.addEventListener("fetch", (event) => {
const url = new URL(event.request.url)
// Assets estaticos: Cache First (instantaneo)
if (url.pathname.match(/\.(css|js|png|jpg|svg|woff2)$/)) {
event.respondWith(
caches.match(event.request).then((cached) => {
if (cached) return cached
return fetch(event.request).then((response) => {
const clone = response.clone()
caches.open(CACHE_NAME)
.then((cache) => cache.put(event.request, clone))
return response
})
})
)
return
}
// Navegacion HTML: Network First con fallback offline
if (event.request.mode === "navigate") {
event.respondWith(
fetch(event.request)
.catch(() => caches.match("/offline.html"))
)
return
}
})
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment