El teu projecte funciona, està provat, es mesura i una màquina el vigila a cada push. I tanmateix continua vivint al teu portàtil, on no serveix a ningú. Desplegar és el que converteix un repositori en un producte, i és també el moment en què apareixen els problemes que cap entorn de desenvolupament no ensenya: les rutes que funcionaven a localhost i donen 404 al servidor, la memòria cau que serveix la versió antiga durant una setmana, el service worker que es queda enganxat amb codi de fa tres desplegaments, la clau d'API que creies protegida i que qualsevol pot llegir amb dos clics, i les capçaleres de seguretat que ningú no configura perquè ningú no les hi ha explicat. En aquesta lliçó aprendràs a preparar la compilació de producció entenent què fa cada part i per què cap clau secreta no pot viure al client; a triar on allotjar amb una taula comparativa honesta; a configurar el servidor de debò —rutes de SPA, memòria cau coherent amb el hashing i amb el service worker, compressió i capçaleres de seguretat explicades una a una—; a decidir què fer amb el backend sense escriure'l, amb l'advertiment que la validació del client mai no substitueix la del servidor; a muntar el desplegament continu amb vistes prèvies per branca i una estratègia de reversió que funcioni a les tres de la matinada; a monitorar en producció sense convertir-te en un problema de privacitat; i a actualitzar una PWA ja instal·lada sense deixar ningú amb una versió antiga. Acabaràs amb l'aplicació desplegada amb HTTPS, el seu flux de desplegament continu i una llista de comprovació de llançament signada.
Contingut
- Què canvia quan el codi surt de la teva màquina
- La compilació de producció
- Variables d'entorn i per què no hi ha secrets al client
- On allotjar: taula comparativa honesta
- Domini, HTTPS i certificats
- Les rutes d'una SPA i la reserva a
index.html - Memòria cau: la part que més es fa malament
- Compressió
- Capçaleres de seguretat, una a una
- El backend: quines opcions hi ha sense escriure un servidor
- La validació del client mai no substitueix la del servidor
- Desplegament continu amb GitHub Actions
- Entorns de vista prèvia per branca
- Estratègia de reversió
- Monitoratge d'errors en producció
- Mètriques de camp amb
web-vitals - Actualitzar una PWA ja instal·lada
- La llista de comprovació de llançament
- Errors Habituals i Consells
- Exercicis
- Conclusió
- Què canvia quan el codi surt de la teva màquina
En desenvolupament, Vite fa moltíssimes coses per tu que en producció no existeixen. Convé veure la llista completa abans de res, perquè cada fila és una font de sorpreses:
| Aspecte | En desenvolupament (npm run dev) |
En producció |
|---|---|---|
| Mòduls | Se serveixen sense empaquetar, un per fitxer | Empaquetats i minificats |
| Rutes | El servidor retorna index.html per a tot |
Retorna 404 si no ho configures |
| Memòria cau | Desactivada | Agressiva, i difícil de desfer |
| Errors | Consola visible, amb mapes de codi | Ningú no els veu tret que els recullis |
| Xarxa | Local, instantània | Latència real, pèrdues, 3G |
| Variables d'entorn | Del fitxer .env |
Les que injecti el procés de compilació |
| HTTPS | Opcional | Obligatori (i moltes API l'exigeixen) |
| Service worker | Normalment desactivat | Actiu, i cacheja amb persistència |
| Usuaris | Tu, en un navegador recent | Qualsevol, en qualsevol cosa |
La conseqüència pràctica és una regla d'or que estalvia moltíssim temps:
No despleguis mai res que no hagis provat amb
npm run buildinpm run previewa la teva màquina.
preview serveix la compilació real des de dist/, i allà apareixen la meitat dels problemes: rutes mal resoltes, imatges que no es van copiar, import dinàmics que en desenvolupament funcionaven per casualitat, i variables d'entorn que no es van injectar.
I una segona regla, del consell de 11-01 que tant de bo hagis seguit: el primer desplegament es fa amb l'aplicació buida, a la fita H1. Si ho vas fer, aquest apartat és un repàs. Si no, fes-ho ara abans de continuar: descobrir els problemes de configuració amb una pàgina en blanc costa una hora; descobrir-los amb tot el producte en joc costa un cap de setmana.
- La compilació de producció
I el que produeix, comentat:
dist/
├── index.html 2,1 kB ← referencies amb hash
├── manifest.json 0,6 kB
├── sw.js 4,3 kB ← SENSE hash, a proposit
├── assets/
│ ├── index-C4f8a2b1.js 38,2 kB ← el paquet principal
│ ├── informe-a91b3c7d.js 9,4 kB ← tros carregat a demanda
│ ├── calendari-7f2e9d10.js 6,5 kB
│ └── index-B2d91e0f.css 11,8 kB
└── icones/
├── icona-192.png
└── icona-512.pngAquí passen quatre coses i convé entendre cadascuna.
1 · Empaquetat. Els centenars de mòduls ES de src/ es combinen en uns pocs fitxers. En desenvolupament cada import era una petició; en producció són tres o quatre descàrregues. És el que fa possible complir el pressupost de «≤ 4 peticions» de 11-01.
2 · Divisió de codi (09-05). Les pantalles que no es veuen en arrencar surten en trossos a part, carregats quan calen:
// L informe nomes es descarrega si algu l obre
const { crearInformeVista } = await import('./vista/informe-vista.js');3 · Minificació. S'eliminen espais, comentaris i noms llargs, i s'apliquen optimitzacions com el tree shaking: si importes una funció d'un mòdul i no fas servir les altres cinc, les altres cinc no s'inclouen. Això només funciona amb mòduls ES estàtics, que és una de les raons per les quals 05-04 hi insistia.
4 · Hashing del nom. index-C4f8a2b1.js conté un resum del contingut. Si el contingut canvia, el nom canvia. Aquesta és la peça que fa possible l'estratègia de memòria cau de l'apartat 7, i val la pena veure-la amb claredat:
| Situació | Nom | Conseqüència |
|---|---|---|
| Sense hash | index.js |
Cal triar entre memòria cau llarga (usuaris amb versió vella) o curta (descàrrega a cada visita) |
| Amb hash | index-C4f8a2b1.js |
Memòria cau d'un any i actualització immediata: el fitxer nou té un altre nom |
És una solució elegant a un problema que semblava irresoluble, i explica per què index.html no porta hash: algú ha de ser el punt d'entrada estable que apunti als fitxers amb hash.
Configuració de Vite comentada:
// vite.config.js
import { defineConfig } from 'vite';
export default defineConfig({
base: '/orbita/', // ← si serveixes des d un subdirectori (GitHub Pages)
build: {
outDir: 'dist',
sourcemap: true, // ← mapes de codi: veure mes avall
target: 'es2020',
rollupOptions: {
output: {
manualChunks: {
// Separar el que canvia poc del que canvia molt
domini: ['./src/domini/tasca.js', './src/domini/tauler.js', './src/domini/arbre.js']
}
}
},
chunkSizeWarningLimit: 60 // avisa si un tros supera el teu pressupost
}
});base és la causa número u de desplegaments trencats. Si serveixes a https://usuari.github.io/orbita/, les rutes absolutes /assets/... apuntarien a l'arrel del domini, on no hi ha res. Amb base: '/orbita/', Vite genera /orbita/assets/.... A Netlify, Vercel o un domini propi, on l'aplicació és a l'arrel, base ha de ser '/'.
Sobre sourcemap: true, que té un matís que convé decidir conscientment: els mapes de codi permeten depurar en producció veient el teu codi original en lloc del minificat, i són imprescindibles perquè un servei de seguiment d'errors doni traces llegibles. La contrapartida és que exposen el teu codi font. Per a un projecte de portafoli amb repositori públic, no hi ha cap raó per amagar-lo. En un producte comercial, la pràctica habitual és generar-los i pujar-los al servei d'errors sense publicar-los al servidor.
- Variables d'entorn i per què no hi ha secrets al client
Vite injecta al paquet les variables que comencin per VITE_:
# .env.production
VITE_API_URL=https://api.orbita.example/v1
VITE_ENTORN=produccio
VITE_VERSION=1.0.0I aquí ve el punt més important de tota la lliçó, que cal entendre sense ambigüitat:
import.meta.env.VITE_EL_QUE_SIGUIse substitueix literalment pel seu valor durant la compilació. Aquest valor acaba escrit en un fitxer.jsque es descarrega al navegador de qualsevol. No és una variable: és una constant pública.
Comprova-ho tu mateix, i fes-ho ara:
npm run build
grep -r "VITE_" dist/ # es veuen els noms reemplaçats
grep -r "sk_live\|api_key\|secret" dist/ # ← això ha de retornar ZERO resultatsQuè pot anar al client i què no:
| Pot anar-hi | No hi pot anar mai |
|---|---|
| La URL pública de la teva API | Claus d'API secretes |
| El nom de l'entorn | Contrasenyes o cadenes de connexió a base de dades |
| La versió de l'aplicació | Secrets de signatura de tokens |
| Identificadors públics de serveis (Google Analytics, Sentry DSN públic) | Credencials de correu, passarel·les de pagament, serveis al núvol |
| Banderes de funcionalitat no sensibles | Qualsevol cosa que doni accés a dades que no són d'aquell usuari |
Per què la gent s'equivoca aquí, i amb quines conseqüències. El raonament erroni és: «està en una variable d'entorn, i les variables d'entorn són secretes». Ho són al servidor. En una compilació de client, la variable d'entorn només descriu on es va escriure el valor, no on acaba. I el valor acaba en un fitxer públic, minificat però perfectament llegible amb Ctrl+F.
Les conseqüències són reals i cares: claus de serveis al núvol filtrades en repositoris públics es detecten automàticament per robots en qüestió de minuts, i el resultat acostuma a ser una factura per ús aliè.
Què fer si el teu projecte necessita parlar amb un servei que exigeix clau secreta. Només hi ha una resposta correcta: un intermediari al servidor.
flowchart LR
A["Navegador<br/><i>sense secrets</i>"] -->|"peticio publica"| B["La teva funcio al servidor<br/><i>desa la clau</i>"]
B -->|"clau secreta"| C["Servei extern"]
C --> B --> A
style A fill:#dcfce7,stroke:#16a34a
style B fill:#dbeafe,stroke:#2563eb
Netlify Functions, Vercel Functions i Cloudflare Workers permeten fer-ho amb vint línies i sense muntar un servidor complet. És l'apartat 10.
I la regla del repositori: .env a .gitignore, sempre, amb un .env.example versionat que documenti quines variables calen i amb valors d'exemple. Si algun cop confirmes un secret per error, canviar-lo és obligatori: esborrar-lo de l'historial no n'hi ha prou, perquè ja és en qualsevol clon que s'hagi fet mentrestant.
- On allotjar: taula comparativa honesta
Una aplicació com la teva és un lloc estàtic: HTML, CSS, JavaScript i algunes imatges. No hi ha cap procés de servidor executant el teu codi. Això obre moltes opcions i totes són bones; les diferències són als detalls.
| Plataforma | Cost | Desplegament | Vistes prèvies | Funcions de servidor | Capçaleres a mida | Domini propi | Quan triar-la |
|---|---|---|---|---|---|---|---|
| GitHub Pages | Gratis | Acció de GitHub | ❌ | ❌ | Molt limitades | ✅ amb HTTPS | Portafoli pur; el més simple |
| Netlify | Gratis generós | Git o CLI | ✅ per PR | ✅ Functions | ✅ _headers, _redirects |
✅ | La recomanada per a aquest projecte |
| Vercel | Gratis generós | Git o CLI | ✅ per PR | ✅ Functions | ✅ vercel.json |
✅ | Molt semblant; excel·lent si després fas servir Next.js |
| Cloudflare Pages | Gratis molt ampli | Git o CLI | ✅ per branca | ✅ Workers | ✅ _headers |
✅ | Xarxa global excel·lent; límits generosos |
| Servidor propi + Nginx | Cost del servidor | Teu (rsync, CI) | Manual | El que muntis | Control total | ✅ (Let's Encrypt) | Quan necessites control o ja tens servidor |
El que la taula no diu i convé saber:
GitHub Pages és l'opció més simple i té una limitació que afecta directament aquest projecte: no permet configurar capçaleres HTTP. Això significa que no hi pots posar una Content-Security-Policy real (només la versió en <meta>, que és més limitada), ni controlar el Cache-Control, ni configurar la reserva de SPA excepte amb el truc del 404.html. Per a un portafoli és perfectament vàlid; per practicar l'apartat 9 d'aquesta lliçó, no.
Netlify, Vercel i Cloudflare Pages fan essencialment el mateix des del punt de vista d'un lloc estàtic: connectes el repositori, cada push a main desplega, cada PR genera una URL de vista prèvia, i tens un fitxer de configuració per a capçaleres i redireccions. Triar-ne una per a aquest projecte és qüestió de preferència; les tres són excel·lents.
Un servidor propi amb Nginx és l'únic que t'obliga a entendre què està passant, i precisament per això és formatiu. També és l'únic on tu ets responsable de les actualitzacions de seguretat, les còpies de seguretat i el certificat.
Un avís sobre els plans gratuïts, perquè convé l'honestedat: són generosos i suficients per a un projecte personal, però tenen límits (minuts de compilació, amplada de banda, invocacions de funció) i canvien amb el temps. Llegeix els límits actuals abans de comprometre-t'hi, i tingues en compte que un projecte que creix pot acabar necessitant un pla de pagament.
Recomanació per a aquest mòdul: desplega a Netlify o Cloudflare Pages. Tens vistes prèvies per PR, capçaleres configurables i funcions de servidor si en necessites, sense cost i sense administrar res. I si vols l'exercici complet, munta a més una configuració de Nginx equivalent encara que sigui en local: entendre el fitxer de configuració ensenya més que qualsevol tauler de control.
- Domini, HTTPS i certificats
HTTPS no és opcional, i no només per seguretat:
| Sense HTTPS no funcionen | Motiu |
|---|---|
| Service workers | L'especificació ho exigeix |
| Notificacions, geolocalització, càmera, porta-retalls | Contextos segurs obligatoris |
crypto.subtle |
Ídem |
| La instal·lació com a PWA | Requisit del manifest |
| La confiança de l'usuari | El navegador mostra «No és segur» |
A més, les dades viatgen en clar: en una wifi pública, qualsevol pot llegir i modificar el que s'envia.
Com s'aconsegueix. A les plataformes de l'apartat anterior, automàticament i de franc: emeten i renoven el certificat per tu. En un servidor propi, amb Let's Encrypt i certbot:
sudo certbot --nginx -d orbita.example -d www.orbita.example
# Renovacio automatica cada 60 dies mitjancant temporitzador del sistema
sudo certbot renew --dry-runEl domini. Un domini propi costa entre deu i quinze euros a l'any i canvia completament la percepció del projecte. La configuració és un registre DNS:
| Tipus | Nom | Valor | Per a què |
|---|---|---|---|
CNAME |
www |
el-teu-lloc.netlify.app |
El subdomini apunta a la plataforma |
A o ALIAS |
@ |
La IP o l'àlies que indiqui la plataforma | El domini arrel |
I una decisió que cal prendre i no deixar a mitges: tria una forma canònica —amb www o sense www— i redirigeix l'altra amb un 301. Tenir les dues actives duplica el contingut per als cercadors, parteix la memòria cau i confon els usuaris. El mateix amb http → https, que ha de ser una redirecció permanent.
HSTS (Strict-Transport-Security) diu al navegador que mai no intenti connectar-se per HTTP al teu domini:
És una de les capçaleres més eficaces que existeixen. Amb un advertiment important: és difícil de revertir. Un cop un navegador l'ha vista, es nega a fer servir HTTP en aquell domini durant el max-age encara que tu treguis la capçalera. Comença amb un valor petit (max-age=300), comprova que tot funciona, i puja a un any.
- Les rutes d'una SPA i la reserva a
index.html
index.htmlAquest és el problema que sorprèn tothom en el seu primer desplegament.
El teu encaminador (11-01) fa servir la History API: en navegar a la vista d'informe, la URL passa a https://orbita.example/informe. Funciona perfectament… fins que algú recarrega la pàgina o comparteix aquell enllaç.
sequenceDiagram
participant U as Usuari
participant S as Servidor
U->>S: GET /informe
S->>S: Existeix el fitxer /informe?
S-->>U: 404 Not Found ❌
Note over U,S: La teva aplicacio no arriba a carregar-se mai
La causa és simple: el servidor no sap res del teu encaminador. Busca un fitxer anomenat informe i no existeix.
La solució s'anomena reserva (fallback) a index.html: qualsevol ruta que no correspongui a un fitxer real retorna index.html, i llavors el teu JavaScript arrenca, llegeix la URL i pinta la pantalla corresponent.
Amb un advertiment important: la reserva ha de retornar codi 200, no 404, o els cercadors indexaran les teves rutes com a errors.
A Netlify (public/_redirects):
A Vercel (vercel.json):
A Cloudflare Pages (public/_redirects): idèntic a Netlify.
A Nginx:
try_files prova en ordre: el fitxer exacte, el directori, i si no n'existeix cap, index.html. És exactament la lògica que necessites.
A GitHub Pages no hi ha configuració de servidor, així que es fa servir un truc: crear un 404.html idèntic a l'index.html. GitHub el serveix per a rutes inexistents i l'aplicació arrenca. Funciona, però retorna codi 404, cosa que és dolenta per al SEO. És una de les raons per preferir una altra plataforma si t'importa la indexació.
I el detall que trenca la reserva: si apliques la regla a tot, /api/tasques també retornarà index.html, i el teu codi rebrà HTML on esperava JSON — amb un error d'anàlisi desconcertant. Exclou les rutes d'API abans de la regla general:
location /api/ { proxy_pass http://127.0.0.1:3000; } # abans del location /
location / { try_files $uri $uri/ /index.html; }
- Memòria cau: la part que més es fa malament
La memòria cau és on més s'equivoca la gent, i produeix dues errades oposades i igual de dolentes:
| Errada | Causa | Símptoma |
|---|---|---|
| Tot es cacheja massa | Memòria cau llarga a index.html |
Els usuaris continuen amb la versió antiga durant dies |
| No es cacheja res | Sense Cache-Control |
Es descarrega tot a cada visita: lent i car |
La solució correcta aprofita el hashing de l'apartat 2, i es resumeix en una regla de dues línies:
Els fitxers amb hash al nom: memòria cau d'un any, immutable. L'
index.html, el service worker i el manifest: mai a la memòria cau.
La lògica és impecable: si el contingut canvia, el nom canvia, per tant un fitxer amb hash mai no necessita revalidar-se. I index.html, que és qui apunta als noms nous, s'ha de demanar sempre.
La taula completa:
| Recurs | Cache-Control |
Per què |
|---|---|---|
assets/*-[hash].js i .css |
public, max-age=31536000, immutable |
El nom canvia si canvia el contingut |
| Imatges amb hash | public, max-age=31536000, immutable |
Ídem |
| Fonts | public, max-age=31536000, immutable |
Rarament canvien; si ho fan, se'ls posa hash |
index.html |
no-cache |
S'ha de revalidar sempre per descobrir els noms nous |
sw.js |
no-cache |
Si es cacheja, els usuaris es queden amb el service worker antic |
manifest.json |
no-cache o max-age=3600 |
Canvia poc però ha de poder actualitzar-se |
| Respostes d'API | no-store si són privades |
No cachejar dades d'usuari en intermediaris |
no-cache no significa «no desar». Significa «desa, però revalida amb el servidor abans de fer servir». Amb ETag, la revalidació acostuma a retornar un 304 sense cos: cost mínim i sempre actualitzat. El que no desa res és no-store.
A Netlify o Cloudflare Pages (public/_headers):
/assets/*
Cache-Control: public, max-age=31536000, immutable
/index.html
Cache-Control: no-cache
/sw.js
Cache-Control: no-cache
/manifest.json
Cache-Control: no-cacheA Nginx:
# Fitxers amb hash: memoria cau maxima
location ~* ^/assets/.*\.[0-9a-zA-Z_-]{8}\.(js|css|woff2|png|svg)$ {
add_header Cache-Control "public, max-age=31536000, immutable";
}
# El punt d entrada i el service worker: mai a la memoria cau
location = /index.html { add_header Cache-Control "no-cache"; }
location = /sw.js { add_header Cache-Control "no-cache"; }El cas especial del service worker mereix èmfasi perquè produeix l'errada més desconcertant de totes. Si sw.js se serveix amb memòria cau llarga, el navegador no descarrega el nou, així que el service worker antic continua servint la versió antiga de la teva aplicació indefinidament. L'usuari recarrega, esborra l'historial, i continua veient el mateix. Els navegadors moderns limiten la memòria cau de sw.js a 24 hores per defecte, però no hi confiïs: posa-hi no-cache explícit.
Com comprovar que està ben configurat:
curl -sI https://orbita.example/assets/index-C4f8a2b1.js | grep -i cache-control
# → cache-control: public, max-age=31536000, immutable
curl -sI https://orbita.example/index.html | grep -i cache-control
# → cache-control: no-cache
- Compressió
Comprimir és l'optimització amb millor relació entre esforç i resultat que existeix: una línia de configuració i entre un 60 % i un 80 % menys de bytes en text.
| Algorisme | Reducció típica en JS | Compatibilitat | Cost de CPU |
|---|---|---|---|
| Sense comprimir | — | Tots | 0 |
| gzip | ~70 % | Universal | Baix |
| Brotli | ~75–80 % | Tots els navegadors actuals | Més gran en comprimir |
Les plataformes gestionades ho fan soles i no has de configurar res. A Nginx:
gzip on;
gzip_types text/css application/javascript application/json image/svg+xml;
gzip_min_length 1024; # comprimir coses de 200 bytes no compensa
# Brotli, si el modul esta disponible
brotli on;
brotli_types text/css application/javascript application/json image/svg+xml;Dos matisos importants:
- No comprimeixis el que ja està comprimit. PNG, JPEG, WebP, MP4 i WOFF2 ja ho estan. Tornar-los a comprimir gasta CPU i de vegades augmenta la mida.
- El teu pressupost de 11-01 està en bytes comprimits. Els 60 kB del pressupost es mesuren a la columna «Transferred» del panell Network, no a «Size». Confondre-les fa que et pensis que l'incompleixes quan el compleixes, o a l'inrevés.
- Capçaleres de seguretat, una a una
Aquestes capçaleres són gratis, es configuren una vegada i tanquen classes senceres d'atacs. Van explicades d'una en una perquè copiar-les sense entendre-les produeix llocs trencats que ningú no sap arreglar.
9.1 Content-Security-Policy
És la més potent i la més delicada. Declara d'on pot carregar recursos la teva pàgina; tota la resta la bloqueja el navegador.
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https://api.orbita.example; font-src 'self'; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'Directiva a directiva:
| Directiva | Què controla | Valor i per què |
|---|---|---|
default-src 'self' |
El que no estigui especificat | Només el teu propi origen |
script-src 'self' |
D'on es carreguen scripts | La clau: bloqueja scripts injectats i en línia |
style-src 'self' |
Fulls d'estil | Ídem per al CSS |
img-src 'self' data: |
Imatges | data: permet SVG en línia i icones incrustades |
connect-src |
fetch, XHR, WebSocket |
Només la teva API: si t'injecten codi, no pot exfiltrar dades a un altre servidor |
object-src 'none' |
<object>, <embed> |
No els necessites i són vectors clàssics |
base-uri 'self' |
L'etiqueta <base> |
Impedeix reescriure totes les teves rutes relatives |
form-action 'self' |
Destí dels formularis | Impedeix que un formulari enviï dades fora |
frame-ancestors 'none' |
Qui et pot posar en un iframe | Evita el clickjacking; substitueix X-Frame-Options |
Per què la CSP és tan eficaç: és la segona línia de defensa contra XSS. A 11-03 vas aprendre la primera —textContent en lloc de innerHTML—, i la CSP és el paracaigudes de reserva: encara que un atacant aconsegueixi injectar <script>alert(1)</script> a la teva pàgina, script-src 'self' impedeix que s'executi, perquè no ve del teu origen.
Els dos esculls pràctics:
- Els estils en línia. Si el teu codi fa
element.style.color = 'red', la CSP ambstyle-src 'self'ho bloqueja. La solució correcta és fer servir classes CSS en lloc d'estils en línia — que és millor pràctica de tota manera. La solució mandrosa és'unsafe-inline', que desactiva bona part de la protecció. Evita-la. - Els scripts en línia. Si tens
<script>amb codi dins de l'HTML, es bloqueja. Vite no en genera cap per defecte, així que normalment no és problema.
Com implantar-la sense trencar res: comença en mode informe, que no bloqueja res i t'avisa del que bloquejaria:
Navega per tota la teva aplicació, mira els avisos de la consola, ajusta la política, i només llavors treu el -Report-Only.
9.2 Les altres
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=(), usb=()
Strict-Transport-Security: max-age=31536000; includeSubDomains
X-Frame-Options: DENY| Capçalera | Què fa | Per què la vols |
|---|---|---|
X-Content-Type-Options: nosniff |
Impedeix que el navegador endevini el tipus d'un fitxer pel seu contingut | Sense ella, un fitxer pujat per un usuari que «sembla» JavaScript es podria executar com a tal |
Referrer-Policy |
Quanta informació de la URL d'origen s'envia en navegar fora | strict-origin-when-cross-origin envia la URL completa dins del teu lloc i només el domini cap a fora: evita filtrar identificadors de les teves URL a tercers |
Permissions-Policy |
Desactiva funcions del navegador que no fas servir | Si no fas servir càmera ni micròfon, declarar-ho tanca la porta que un script injectat els demani |
Strict-Transport-Security |
Força HTTPS | Apartat 5. Compte amb max-age al principi |
X-Frame-Options: DENY |
Impedeix que et posin en un iframe | Redundant amb frame-ancestors però cobreix navegadors antics |
A Netlify o Cloudflare Pages (public/_headers):
/*
Content-Security-Policy: default-src 'self'; script-src 'self'; style-src 'self'; img-src 'self' data:; connect-src 'self' https://api.orbita.example; object-src 'none'; base-uri 'self'; form-action 'self'; frame-ancestors 'none'
X-Content-Type-Options: nosniff
Referrer-Policy: strict-origin-when-cross-origin
Permissions-Policy: camera=(), microphone=(), geolocation=(), payment=()
Strict-Transport-Security: max-age=31536000; includeSubDomainsCom verificar-ho. Hi ha serveis públics que analitzen les teves capçaleres i et donen una puntuació amb explicacions. També n'hi ha prou amb:
curl -sI https://orbita.example/ | grep -iE "content-security|x-content|referrer|permissions|strict-transport"Aconseguir una bona puntuació de capçaleres de seguretat és un detall petit amb molt de pes en una revisió tècnica: demostra que has pensat en el desplegament, no només en el codi.
- El backend: quines opcions hi ha sense escriure un servidor
El teu projecte pot funcionar perfectament sense servidor: les dades a localStorage, tot al navegador. És una decisió legítima si està documentada amb les seves limitacions (un sol dispositiu, sense privacitat real, sense compartir).
Si necessites més, hi ha tres camins:
| Opció | Què és | Esforç | Control | Quan |
|---|---|---|---|---|
| Sense backend | Tot al navegador | Cap | Total sobre el client | MVP, portafoli, ús personal |
| Backend com a servei (BaaS) | Un servei et dona base de dades, autenticació i API | Baix | Mitjà; depens del proveïdor | Quan necessites multiusuari sense escriure servidor |
| Funcions sense servidor | Trossos de codi al servidor per a casos concrets | Baix-mitjà | Alt en el que escrius | Amagar una clau, enviar un correu, validar alguna cosa |
| Backend propi | Node.js + base de dades, escrit per tu | Alt | Total | Quan la lògica de servidor és el producte |
Backend com a servei. Serveis com Supabase, Firebase o PocketBase et donen base de dades, API automàtica, autenticació i de vegades temps real, amb configuració en lloc de codi. Per a un projecte de portafoli que necessita usuaris i dades compartides, és l'opció amb millor relació entre esforç i resultat.
El que cal saber abans de triar-ne un:
- Depens del proveïdor. Migrar després té cost. Mitiga-ho mantenint la teva frontera del repositori (11-02): si el BaaS viu darrere de
RepositoriSupabase, canviar-lo és reescriure un fitxer. - Les regles de seguretat són responsabilitat teva. Aquests serveis exposen la base de dades directament al client, protegida per regles que tu configures. Una regla mal escrita exposa totes les dades de tots els usuaris. És l'errada més freqüent i més greu d'aquestes plataformes.
- Els plans gratuïts tenen límits i el servei pot canviar de preus o desaparèixer.
Funcions sense servidor. Quan només necessites amagar una clau o fer una operació puntual al servidor:
// netlify/functions/tipus-canvi.js — la clau MAI no arriba al navegador
export async function handler(esdeveniment) {
const clau = process.env.CLAU_SERVEI; // variable del servidor, no VITE_
const resposta = await fetch(`https://servei.example/api?key=${clau}`);
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'public, max-age=3600' },
body: JSON.stringify(await resposta.json())
};
}Fixa't que la variable no porta prefix VITE_: això és exactament el que la manté fora del paquet del client.
Backend propi. És el camí natural si vols créixer, i és el tema de la lliçó 11-07: Node.js amb Express o Fastify, una base de dades, autenticació amb tokens, i tota la lògica de servidor que la teva aplicació necessiti. Requereix aprendre coses noves i val la pena, però no és requisit d'aquest projecte.
- La validació del client mai no substitueix la del servidor
Aquest apartat és curt, no té matisos, i és dels que més importen.
Tens quinze regles de negoci, R1 a R15, implementades i provades a domini/. Funcionen perfectament… al navegador. I el navegador és un entorn que l'usuari controla completament:
- Pot obrir DevTools i cridar les teves funcions amb el que vulgui.
- Pot modificar el teu codi abans que s'executi.
- Pot saltar-se la teva aplicació sencera i cridar l'API amb
curl.
# Cap validacio de client no hi intervé
curl -X POST https://api.orbita.example/v1/tasques \
-H "Content-Type: application/json" \
-d '{"titol":"","horesEstimades":9999,"estat":"feta","responsableId":"altre-usuari"}'Si el servidor accepta això, les teves quinze regles no valen res.
La validació del client és una comoditat per a l'usuari. La validació del servidor és la que protegeix les dades. No són alternatives: són dues capes amb propòsits diferents, i totes dues són obligatòries així que hi ha servidor.
| Capa | Propòsit | Què passa si falta |
|---|---|---|
| Client | Retroalimentació immediata, evitar viatges inútils, bona experiència | L'aplicació se sent lenta i maldestra, però les dades continuen protegides |
| Servidor | Integritat de les dades i seguretat | Qualsevol pot escriure el que vulgui: dades corruptes, comptes aliens modificats |
La bona notícia és que la teva arquitectura ja hi està preparada. domini/ és JavaScript pur sense dependències del navegador — aquella va ser la primera frontera de 11-01. Si escrius el backend en Node.js, pots importar exactament els mateixos fitxers i executar les mateixes regles al servidor:
// servidor/rutes/tasques.js — el MATEIX domini, sense duplicar res
import { Tasca } from '../../src/domini/tasca.js';
import { ErrorDeValidacio } from '../../src/domini/errors.js';
app.post('/v1/tasques', async (peticio, resposta) => {
try {
const tasca = new Tasca(peticio.body); // R1-R15 aplicades al servidor
resposta.status(201).json(await repositori.desar(tasca));
} catch (error) {
if (error instanceof ErrorDeValidacio) {
return resposta.status(400).json({ codi: 'VALIDACIO', camp: error.camp, missatge: error.message });
}
throw error;
}
});Una sola implementació de les regles, executada als dos costats. Això és el que valia la pena de mantenir el domini lliure de dependències durant tot el projecte, i és un argument excel·lent per explicar en una entrevista (11-06).
I el que el servidor ha de validar a més de les teves regles, perquè són coses que el client no pot fer:
| Comprovació | Per què només la pot fer el servidor |
|---|---|
| Autenticació: qui ets? | El client pot dir que és qui vulgui |
| Autorització: pots tocar això? | R15 (rols) és trivial de saltar-se al client |
| Límit de peticions | Protegeix contra abús i contra bucles accidentals |
| Mida màxima del cos | Evita que algú enviï 500 MB |
| Unicitat real | Només la base de dades la pot garantir sense curses |
- Desplegament continu amb GitHub Actions
Desplegament continu significa que el que es fusiona a main arriba a producció automàticament, sense passos manuals. Amb la CI de 11-04 protegint la branca, això és segur: res no arriba a main sense lint, proves, cobertura, pressupost i recorreguts en verd.
# .github/workflows/desplegar.yml
name: Desplegar
on:
push:
branches: [main]
workflow_dispatch: # permet llancar-lo a ma des de la interficie
concurrency:
group: desplegament-produccio
cancel-in-progress: false # NO cancel·lar un desplegament a mitges
jobs:
desplegar:
runs-on: ubuntu-latest
environment:
name: produccio
url: https://orbita.example
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: 20, cache: npm }
- run: npm ci
- name: Verificar abans de desplegar
run: npm run verificar # cinturó i tirants: mai no es desplega en vermell
- name: Compilar per a producció
run: npm run build
env:
VITE_API_URL: ${{ vars.API_URL }}
VITE_VERSION: ${{ github.sha }}
- name: Comprovar que no hi ha secrets al paquet
run: |
if grep -rEq "(sk_live|api[_-]?key|BEGIN [A-Z ]*PRIVATE KEY)" dist/; then
echo "::error::Possible secret a dist/"
exit 1
fi
- name: Desplegar a Netlify
uses: nwtgck/actions-netlify@v3
with:
publish-dir: './dist'
production-deploy: true
deploy-message: ${{ github.event.head_commit.message }}
env:
NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
- name: Comprovació de fum
run: |
sleep 15
curl -sfI https://orbita.example/ | head -1
curl -sf https://orbita.example/ | grep -q "Òrbita" || exit 1Les decisions del fitxer:
| Element | Per què |
|---|---|
cancel-in-progress: false |
Cancel·lar un desplegament a mitges pot deixar el lloc inconsistent. Aquí sí que s'espera |
workflow_dispatch |
Poder redesplegar a mà sense fer un commit buit |
environment amb url |
GitHub mostra l'enllaç i desa l'historial de desplegaments |
npm run verificar un altre cop |
La CI ja va passar, però un desplegament no s'ha de fiar mai d'un altre flux |
VITE_VERSION: github.sha |
Cada desplegament sap de quin commit ve. Val or en depurar en producció |
El grep de secrets |
Última xarxa de seguretat automàtica abans de publicar |
| Comprovació de fum | Que el desplegament acabi no significa que el lloc funcioni |
La comprovació de fum és una idea que convé interioritzar: una verificació mínima i ràpida que el que s'ha desplegat respon i conté el que ha de contenir. Costa cinc línies i detecta el «s'ha desplegat una carpeta buida», que passa més del que sembla.
Els secrets del repositori (Settings → Secrets and variables): NETLIFY_AUTH_TOKEN i NETLIFY_SITE_ID van com a secrets (xifrats, no visibles ni als registres); API_URL va com a variable (no és secreta i així es pot llegir). No escriguis mai un token directament al YAML: el fitxer és al repositori.
- Entorns de vista prèvia per branca
Un entorn de vista prèvia és un desplegament temporal d'una branca, amb la seva pròpia URL. Netlify, Vercel i Cloudflare Pages ho fan automàticament en obrir una Pull Request.
Per què val la pena, encara que treballis sol:
| Benefici | Detall |
|---|---|
| Veus el canvi en condicions reals | Amb la compilació de producció, HTTPS, memòria cau i service worker |
| El pots provar al mòbil | N'hi ha prou amb obrir la URL; res de configurar xarxa local |
| Pots demanar opinió | Un enllaç, no «clona't el repo i executa això» |
| Compares abans i després | Producció i vista prèvia obertes en dues pestanyes |
| Lighthouse sobre el real | La CI de 11-04 pot auditar la vista prèvia en comptes d'un servidor local |
Tres precaucions importants:
noindexa les vistes prèvies. Si Google indexadeploy-preview-42--orbita.netlify.app, tindràs contingut duplicat competint amb el teu lloc. Les plataformes acostumen a posar-l'hi, però comprova-ho.- No apuntis mai una vista prèvia a dades de producció. Una prova destructiva en una vista prèvia que escriu a la base real és un desastre evitable. Fes servir un entorn de dades separat.
- Les vistes prèvies són públiques. Qualsevol amb la URL hi entra. No hi provis amb dades reals de ningú.
- Estratègia de reversió
Tot desplegament pot sortir malament. La pregunta no és si passarà, sinó quant trigaràs a arreglar-ho quan passi — probablement a una hora incòmoda i amb pressa.
Les opcions, de millor a pitjor:
| Estratègia | Temps | Risc | Disponibilitat |
|---|---|---|---|
| Tornar al desplegament anterior (botó de la plataforma) | < 1 min | Molt baix | Netlify, Vercel, Cloudflare |
git revert + desplegament automàtic |
3–5 min | Baix | Sempre |
| Arreglar cap endavant (fix forward) | 10–60 min | Alt amb pressa | Sempre |
| Restaurar des d'una còpia | Variable | Mitjà | Servidor propi |
La primera és la bona, i cal provar-la abans de necessitar-la. Les plataformes gestionades desen tots els desplegaments anteriors i permeten tornar a qualsevol amb un clic, perquè cada desplegament és un conjunt immutable de fitxers estàtics.
El procediment escrit, que ha de ser al teu README o a docs/operacions.md:
## Si un desplegament trenca producció
1. **Revertir primer, investigar després.** L'objectiu immediat és que
els usuaris tinguin alguna cosa que funcioni, no entendre què ha passat.
→ Tauler de Netlify → Deploys → l'anterior → "Publish deploy"
2. Comprovar que el lloc funciona (comprovació de fum manual).
3. `git revert <sha>` a `main` perquè el codi reflecteixi la realitat.
NO facis servir `git reset --force` sobre una branca compartida.
4. Obrir una incidència amb: què s'ha trencat, com s'ha detectat, què s'ha revertit.
5. Reproduir l'errada **en local o en una vista prèvia**, amb una prova
que falli (mètode de 11-04).
6. Corregir, amb la prova en verd, i tornar a desplegar.
7. Escriure un post mortem breu: què ha passat, per què la CI no ho ha detectat,
quina prova o comprovació s'hi afegeix perquè no torni a passar.El punt 1 és contraintuïtiu i és el correcte. La temptació és investigar mentre el lloc està trencat. Però cada minut d'investigació és un minut de servei caigut, i la pressa fa que es prenguin pitjors decisions. Revertir és reversible; arreglar amb pressa, no.
El punt 7 és el que converteix un incident en aprenentatge. I la pregunta clau del post mortem no és «qui s'ha equivocat» sinó «per què la CI no ho ha detectat», perquè la resposta és sempre una comprovació nova que evita tota una família d'errades futures.
Una precaució sobre la base de dades: si el teu desplegament incloïa una migració de dades (com les de 11-03), revertir el codi no reverteix les dades. Per això les migracions han de ser compatibles cap enrere sempre que es pugui: afegir camps, no reanomenar-los ni esborrar-los al mateix desplegament que els deixa de fer servir. La seqüència segura és en dos desplegaments: primer afegir i escriure als dos llocs, després deixar de fer servir el vell.
- Monitoratge d'errors en producció
En desenvolupament, un error apareix a la teva consola. En producció, apareix a la consola d'un usuari que no la mirarà i no t'ho explicarà: simplement deixarà de fer servir la teva aplicació.
A 11-02 vas muntar un registre local amb les dues xarxes de seguretat globals (error i unhandledrejection). Ara cal enviar-lo a algun lloc.
Les opcions:
| Opció | Esforç | Què obtens |
|---|---|---|
| Només el registre local + plafó de diagnòstic | Cap | Res fins que algú t'ho ensenyi |
| Servei de seguiment d'errors | Baix | Errors agrupats, amb traça, context i freqüència |
| Punt d'accés propi que rep errors | Mitjà | Control total, i també responsabilitat total |
Serveis de seguiment d'errors. Sentry, Bugsnag, Rollbar i similars tenen plans gratuïts suficients per a un projecte personal, s'integren amb tres línies i agrupen errors idèntics —cosa que importa molt: una errada que passa 400 vegades és una entrada, no 400 correus.
I aquí ve la part que cal fer bé:
Un servei de seguiment d'errors envia dades dels teus usuaris a un tercer. Això és una decisió de privacitat, no només tècnica.
El que cal configurar abans d'activar-lo:
| Configuració | Per què |
|---|---|
| Filtrar dades personals abans d'enviar | Noms, correus, contingut escrit per l'usuari |
| No enviar el cos de les peticions | Pot contenir qualsevol cosa |
| Emmascarar el DOM si fas servir enregistrament de sessió | Un enregistrament captura literalment tot el que es veu |
| Reduir el mostreig | No necessites el 100 % dels esdeveniments, i menys dades és millor |
| Esmentar-ho a l'avís de privacitat | És un encarregat de tractament; cal declarar-lo |
| Comprovar on s'emmagatzemen les dades | Les transferències fora de la UE tenen requisits propis |
// src/util/registre-remot.js
export function enviarError(entrada) {
if (import.meta.env.DEV) return;
const segur = {
missatge: entrada.missatge,
nom: entrada.nom,
pila: entrada.pila,
versio: import.meta.env.VITE_VERSION,
ruta: location.pathname, // NO location.href: pot portar parametres
context: { cas: entrada.context?.cas, camps: entrada.context?.camps }
// MAI: valors de formulari, noms d usuari, identificadors personals, tokens
};
navigator.sendBeacon('/api/errors', JSON.stringify(segur));
}Dos detalls del codi: location.pathname en lloc de location.href evita enviar paràmetres de consulta que poden contenir cerques o identificadors; i sendBeacon envia sense bloquejar i funciona encara que la pàgina s'estigui tancant, que és justament quan passen molts errors.
Advertiment. No enviïs dades personals a un servei extern sense haver-ho previst al teu avís de privacitat i sense saber on s'emmagatzemen. En un projecte d'aprenentatge amb dades fictícies el risc és nul; en un producte real, activar un servei de seguiment sense més és un problema de compliment normatiu, no un detall tècnic.
- Mètriques de camp amb
web-vitals
web-vitalsA 09-01 vas aprendre la distinció que més confusió evita: laboratori (el teu Lighthouse, la teva màquina, la teva xarxa) davant de camp (usuaris reals, dispositius reals, xarxes reals). Els números de camp són sempre pitjors, i són els que importen.
// src/util/vitals.js
import { onLCP, onINP, onCLS, onTTFB } from 'web-vitals';
function enviar(metrica) {
navigator.sendBeacon('/api/vitals', JSON.stringify({
nom: metrica.name, // LCP, INP, CLS, TTFB
valor: Math.round(metrica.value),
qualificacio: metrica.rating, // good | needs-improvement | poor
ruta: location.pathname,
versio: import.meta.env.VITE_VERSION,
connexio: navigator.connection?.effectiveType ?? 'desconeguda'
}));
}
if (import.meta.env.PROD) {
onLCP(enviar); onINP(enviar); onCLS(enviar); onTTFB(enviar);
}Com llegir els resultats, que és on hi ha el valor:
| Observació | Què significa | Què fer |
|---|---|---|
| Laboratori 1,9 s, camp p75 4,2 s | Els teus usuaris tenen pitjors dispositius i xarxes | Estrangula més en mesurar en local |
| CLS bo a escriptori, dolent a mòbil | Alguna cosa es reajusta només en pantalles petites | Revisar amb l'emulador de dispositius |
| INP dolent només en una ruta | Una pantalla concreta té feina excessiva | Perfilar aquella pantalla (09-02) |
| Empitjorament després d'un desplegament | Una regressió concreta | Comparar per versio: per això s'envia |
Aquest últim punt és el que justifica tot l'esforç: enviar la versió amb cada mètrica permet atribuir una regressió a un desplegament concret. Sense això, només saps que va empitjorar en algun moment.
I una nota de privacitat: les mètriques de rendiment no són dades personals si no inclouen identificadors. No hi afegeixis un identificador d'usuari «per poder correlacionar»; el percentil 75 no el necessita.
- Actualitzar una PWA ja instal·lada
Aquest és el problema més específic del desplegament d'una PWA, i produeix una situació exasperant: l'usuari té l'aplicació instal·lada, tu desplegues una correcció, i ell continua veient la versió antiga. Recarrega, i res.
La causa és al cicle de vida del service worker (07-05):
stateDiagram-v2
[*] --> Installant: es detecta un sw.js diferent
Installant --> Installat: install completat
Installat --> EnEspera: hi ha un SW actiu controlant pestanyes
EnEspera --> Activant: skipWaiting() o es tanquen TOTES les pestanyes
Activant --> Actiu: activate completat
Actiu --> [*]
L'estat «En espera» és el problema. Per defecte, un service worker nou espera que totes les pestanyes de l'aplicació es tanquin. I com que molta gent no tanca mai del tot una PWA instal·lada, aquesta espera pot durar dies.
La solució completa, en tres peces.
Peça 1 · El service worker permet saltar-se l'espera sota demanda:
// sw.js
const VERSIO = 'orbita-v7';
self.addEventListener('install', (esdeveniment) => {
esdeveniment.waitUntil(caches.open(VERSIO).then((c) => c.addAll(RECURSOS)));
// Res de skipWaiting() automatic: l usuari decideix
});
self.addEventListener('activate', (esdeveniment) => {
esdeveniment.waitUntil(
caches.keys()
.then((claus) => Promise.all(claus.filter((k) => k !== VERSIO).map((k) => caches.delete(k))))
.then(() => self.clients.claim())
);
});
self.addEventListener('message', (esdeveniment) => {
if (esdeveniment.data?.tipus === 'SALTAR_ESPERA') self.skipWaiting();
});Peça 2 · L'aplicació detecta que hi ha una versió nova i ho diu:
// src/util/actualitzacio.js
export async function vigilarActualitzacions() {
if (!('serviceWorker' in navigator)) return;
const registre = await navigator.serviceWorker.register('/sw.js');
registre.addEventListener('updatefound', () => {
const nou = registre.installing;
nou.addEventListener('statechange', () => {
if (nou.state === 'installed' && navigator.serviceWorker.controller) {
mostrarAvisActualitzacio(() => {
nou.postMessage({ tipus: 'SALTAR_ESPERA' });
});
}
});
});
// Quan el nou pren el control, recarregar UNA vegada
let recarregant = false;
navigator.serviceWorker.addEventListener('controllerchange', () => {
if (recarregant) return;
recarregant = true;
location.reload();
});
setInterval(() => registre.update(), 60 * 60 * 1000); // comprovar cada hora
}Peça 3 · L'avís, que ha de ser discret i accessible:
<div role="status" aria-live="polite" class="avis-actualitzacio" hidden>
<p>Hi ha una versió nova disponible.</p>
<button type="button" data-accio="actualitzar">Actualitzar ara</button>
<button type="button" data-accio="mes-tard">Més tard</button>
</div>Les quatre regles de l'actualització d'una PWA:
- No recarreguis mai sense avisar. L'usuari pot estar escrivint. Una recàrrega sorpresa que perd un formulari és imperdonable.
- La bandera
recarregantés imprescindible. Sense ella,controllerchangepot provocar un bucle de recàrregues: és una errada real i molt desagradable. sw.jsambCache-Control: no-cache(apartat 7). Sense això, res d'això no funciona perquè el navegador ni tan sols descarrega el service worker nou.- Neteja les memòries cau velles a
activate. Si no, s'acumulen versions i acabes ocupant centenars de megabytes al dispositiu d'una altra persona.
El pla d'emergència, que convé tenir escrit: si desplegues un service worker trencat que trenca l'aplicació per a tothom que la té instal·lada, la sortida és publicar un sw.js mínim que es desregistri a si mateix i netegi totes les memòries cau:
// sw.js d emergencia
self.addEventListener('install', () => self.skipWaiting());
self.addEventListener('activate', async () => {
const claus = await caches.keys();
await Promise.all(claus.map((k) => caches.delete(k)));
await self.registration.unregister();
const clients = await self.clients.matchAll();
clients.forEach((c) => c.navigate(c.url));
});Desa'l. El dia que el necessitis, ho agrairàs.
- La llista de comprovació de llançament
El lliurable final de la fita. Es recorre sencera, es marca, i es desa signada i datada a docs/llancament.md.
18.1 Tècnic
| # | Comprovació | Com |
|---|---|---|
| 1 | npm run verificar en verd |
Local i a la CI |
| 2 | Compilació de producció provada en local | build + preview |
| 3 | Zero secrets a dist/ |
grep de l'apartat 3 |
| 4 | HTTPS actiu i http redirigeix |
curl -I http://… retorna 301 |
| 5 | Una sola forma canònica (amb o sense www) |
L'altra redirigeix amb 301 |
| 6 | Reserva de SPA amb codi 200 | curl -I …/informe |
| 7 | Memòria cau correcta en fitxers amb hash | curl -I sobre assets/… |
| 8 | index.html i sw.js amb no-cache |
curl -I |
| 9 | Compressió activa | Comparar Size i Transferred |
| 10 | Les cinc capçaleres de seguretat presents | curl -I o analitzador públic |
| 11 | CSP sense unsafe-inline i sense errors a consola |
Navegar per tota l'aplicació |
| 12 | Pressupost de rendiment complert en producció | Lighthouse sobre la URL real |
| 13 | Accessibilitat ≥ 95 a Lighthouse, 0 incidències greus d'axe | Ídem |
| 14 | Funciona sense connexió | DevTools en Offline |
| 15 | La PWA s'instal·la i s'actualitza | Instal·lar, desplegar, veure l'avís |
| 16 | Reversió provada de debò | Tornar al desplegament anterior i tornar |
18.2 Contingut i descobribilitat
| # | Comprovació | Detall |
|---|---|---|
| 17 | <title> únic i descriptiu |
«Òrbita — Gestió de treball per a equips petits» |
| 18 | <meta name="description"> de 150–160 caràcters |
El que es llegeix als resultats de cerca |
| 19 | <html lang="ca"> |
Accessibilitat i cercadors |
| 20 | Open Graph complet | og:title, og:description, og:image (1200×630), og:url, og:type |
| 21 | La targeta social es veu bé | Provar amb un validador d'enllaços |
| 22 | Favicon en diverses mides | 16, 32, 180 (Apple), 192 i 512 (PWA) |
| 23 | robots.txt present |
Amb Sitemap: apuntant al sitemap |
| 24 | sitemap.xml amb les rutes públiques |
Encara que en siguin poques |
| 25 | Pàgina 404 pròpia i útil | Amb enllaç a l'inici, no una pantalla buida |
| 26 | manifest.json correcte |
name, short_name, start_url, display, theme_color, icones |
<meta property="og:title" content="Òrbita — Gestió de treball per a equips petits">
<meta property="og:description" content="Tasques, subtasques, càrrega per persona i historial de canvis. Sense frameworks.">
<meta property="og:image" content="https://orbita.example/og-imatge.png">
<meta property="og:url" content="https://orbita.example/">
<meta property="og:type" content="website">
<meta name="twitter:card" content="summary_large_image">18.3 Legal i operatiu
| # | Comprovació | Detall |
|---|---|---|
| 27 | Avís de privacitat | Quines dades es desen, on, quant de temps, i amb quins tercers |
| 28 | Cookies i emmagatzematge | Si fas servir analítica o seguiment, el consentiment té requisits legals |
| 29 | Avís visible sobre les dades | «Les dades es desen només en aquest navegador i no són privades davant de qui faci servir aquest dispositiu» |
| 30 | Llicència al repositori | MIT, Apache 2.0 o la que triïs |
| 31 | Còpia de seguretat | Exportació de dades disponible per a l'usuari |
| 32 | Procediment de reversió escrit | docs/operacions.md |
| 33 | Forma de contacte | Perquè algú pugui reportar una errada |
Advertiment legal. Els punts 27 i 28 no són formalitats. A la Unió Europea, informar sobre el tractament de dades personals és una obligació, i l'ús de cookies o emmagatzematge no estrictament necessari requereix consentiment previ, informat i revocable — un bàner que només diu «acceptar» no ho compleix. Si el teu projecte és un exercici amb dades fictícies i sense analítica, el risc pràctic és nul, però acostuma't a incloure l'avís. Si algun dia publiques un producte amb usuaris reals, això requereix assessorament legal específic: ni aquesta lliçó ni cap documentació tècnica no ho substitueixen.
Un avís de privacitat honest per a un projecte com aquest és curt i s'escriu en deu minuts:
# Avís de privacitat — Òrbita
**Quines dades es desen.** Les tasques, usuaris i historial que introdueixes es
desen **únicament a l'emmagatzematge local del teu navegador**. No s'envien
a cap servidor.
**Qui les pot veure.** Qualsevol persona amb accés a aquest navegador i a aquest
dispositiu. Òrbita **no** ofereix privacitat davant d'altres usuaris del mateix
equip. No hi introdueixis informació confidencial.
**Errors.** Si passa una errada, es registren el missatge tècnic, la ruta i la
versió de l'aplicació. **No** es registra el contingut de les teves tasques.
**Com esborrar les teves dades.** Configuració → Esborrar totes les dades. També pots
esborrar les dades del lloc des del teu navegador.
**Exportar les teves dades.** Configuració → Exportar (format JSON).
**Contacte.** <la teva forma de contacte>
Última actualització: 2026-11-28Errors Habituals i Consells
Posar un secret en una variable VITE_. Acaba escrit en un fitxer públic que qualsevol llegeix amb Ctrl+F, i els robots que rastregen repositoris el troben en minuts. Qualsevol cosa que doni accés a alguna cosa va darrere d'una funció de servidor. Comprova dist/ amb grep abans de cada desplegament, i automatitza-ho al flux.
No configurar la reserva de SPA. Tot funciona navegant des de la portada i dona 404 en recarregar o en obrir un enllaç compartit — que és exactament com arribarà la gent a qui ensenyis el projecte.
Memòria cau llarga a index.html. Els usuaris es queden amb la versió antiga durant dies i no hi ha manera de forçar-los a actualitzar. Els fitxers amb hash porten memòria cau d'un any; el punt d'entrada, no-cache.
Memòria cau a sw.js. Produeix l'errada més desconcertant que existeix: l'usuari recarrega, esborra l'historial, reinstal·la, i continua veient l'aplicació de fa tres desplegaments.
base mal configurat. A GitHub Pages amb subdirectori, sense base: '/repo/' la pàgina carrega en blanc i la consola s'omple de 404 sobre /assets/…. És l'errada número u del primer desplegament.
Afegir 'unsafe-inline' a la CSP perquè deixi de queixar-se. Desactiva bona part de la protecció contra XSS, que és justament el que la CSP existia per donar. Arregla l'estil o l'script en línia; gairebé sempre són dues línies.
Confiar només en la validació del client. Qualsevol pot cridar la teva API amb curl. Amb servidor, les regles hi van també — i el teu domini sense dependències fa que sigui el mateix codi.
Desplegar sense haver provat la reversió. El dia que la necessitis, amb el lloc caigut i pressa, no és el moment de descobrir com funciona. Prova-la avui, en fred.
Recarregar la PWA sense avisar quan hi ha versió nova. L'usuari pot estar escrivint. Avís discret, botó, i recàrrega només quan ho demani — amb la bandera que evita el bucle de recàrregues.
Enviar dades personals a un servei de seguiment d'errors. És un problema de compliment, no d'estil. Filtra abans d'enviar, envia pathname i no href, i declara el servei al teu avís de privacitat.
Consell · Desplega des del primer dia i sovint. Un desplegament per setmana durant dos mesos és avorrit i segur. Un desplegament enorme l'últim dia és la recepta d'un desastre.
Consell · Tingues un plafó de diagnòstic en producció. Una ruta discreta que mostri versió, commit, data de compilació, estat del service worker, espai ocupat i últims errors. Quan algú et digui «no em funciona», aquesta pantalla respon en deu segons.
Consell · Desa una captura de la línia base de producció. Lighthouse sobre la URL real, amb data. D'aquí a sis mesos sabràs si has millorat o empitjorat, i en tindràs la prova.
Consell · Prova el teu lloc desplegat en un mòbil de debò. No a l'emulador. Amb dades mòbils, no wifi. Descobriràs coses que cap estrangulament simulat no t'ensenya.
Exercicis
Aquests exercicis són la fita H6, primera part: l'aplicació desplegada amb HTTPS, el seu flux de desplegament continu i la llista de comprovació signada.
Exercici 1 — La compilació i el desplegament.
- Configura
vite.config.jsambbasecorrecte per a la teva plataforma,sourcemap,manualChunksichunkSizeWarningLimitamb el teu pressupost. - Crea
.env.exampleversionat i.env.productionignorat, i documenta al README quines variables calen. - Compila i audita
dist/: mida total, nombre de fitxers, igrepde secrets. Documenta els resultats. - Prova amb
npm run previewtota l'aplicació abans de desplegar. Anota els problemes que apareguin només en producció (n'hi haurà almenys un). - Desplega a la plataforma que triïs, amb domini propi o subdomini de la plataforma, i HTTPS actiu.
- Configura la reserva de SPA i comprova amb
curl -Ique retorna 200 en una ruta interna. - Configura la memòria cau completa segons la taula de l'apartat 7 i verifica-la amb
curl -Isobre un fitxer amb hash, sobreindex.htmli sobresw.js. - Configura les cinc capçaleres de seguretat, amb la CSP primer en mode informe i després activa, sense
unsafe-inline. - Documenta cada decisió a
docs/desplegament.md: quina plataforma, per què, i què implica.
Exercici 2 — Desplegament continu, vistes prèvies i reversió.
- Escriu
.github/workflows/desplegar.ymlamb: verificació prèvia, compilació amb la versió del commit, comprovació automàtica de secrets, desplegament i comprovació de fum. - Configura els secrets i variables del repositori correctament (secrets xifrats, variables normals).
- Activa les vistes prèvies per PR i comprova les tres precaucions:
noindex, dades separades, i consciència que són públiques. - Prova la reversió de debò: desplega un canvi visible, reverteix a l'anterior, comprova que el lloc torna, i torna a publicar el nou. Cronometra quant trigues.
- Escriu
docs/operacions.mdamb el procediment de reversió de set passos adaptat a la teva plataforma. - Configura el monitoratge d'errors amb filtratge de dades personals, i demostra que un error provocat a propòsit arriba amb la traça i sense dades d'usuari.
- Instrumenta
web-vitalsenviant la versió, i recull almenys una sessió de dades de camp. - Implementa l'actualització de la PWA amb les tres peces de l'apartat 17, i demostra-ho: instal·la l'aplicació, desplega un canvi, i comprova que apareix l'avís, que el botó actualitza i que no hi ha bucle de recàrregues.
Exercici 3 — El llançament.
- Recorre les 33 comprovacions de l'apartat 18 sobre el teu lloc desplegat. Marca cadascuna amb evidència (ordre executada, captura o URL), no de memòria.
- Completa el bloc de contingut:
<title>, descripció, Open Graph amb imatge de 1200×630, favicons en cinc mides,robots.txt,sitemap.xml, pàgina 404 pròpia imanifest.jsoncomplet. - Escriu l'avís de privacitat amb la plantilla de l'apartat 18.3, adaptat al que la teva aplicació fa realment.
- Afegeix la llicència al repositori.
- Executa Lighthouse sobre la URL de producció (no en local) i compara amb el teu pressupost de 11-01 i amb la teva línia base de 11-04. Documenta les diferències i explica-les.
- Passa el lloc per un analitzador de capçaleres de seguretat i documenta la puntuació.
- Obre'l en un mòbil real amb dades mòbils i anota tot el que no funcioni com esperaves.
- Signa i data
docs/llancament.md.
Solucions
Criteris d'acceptació de l'exercici 1 — Desplegament
| # | Criteri | Verificació |
|---|---|---|
| 1 | El lloc carrega per HTTPS | curl -sI https://… retorna 200 |
| 2 | HTTP redirigeix a HTTPS | curl -sI http://… retorna 301 |
| 3 | Només una forma canònica | L'altra retorna 301 |
| 4 | Una ruta interna recarregada funciona | curl -sI …/informe retorna 200 |
| 5 | Fitxer amb hash cachejat un any | cache-control: …max-age=31536000, immutable |
| 6 | index.html amb no-cache |
curl -sI |
| 7 | sw.js amb no-cache |
curl -sI |
| 8 | Compressió activa | Transferred < 40 % de Size |
| 9 | Les cinc capçaleres presents | curl -sI | grep -iE … |
| 10 | CSP sense unsafe-inline |
Inspecció + consola sense violacions |
| 11 | Zero secrets a dist/ |
`grep -rE "(sk_ |
| 12 | Lighthouse en producció compleix el pressupost | Informe adjunt |
Rúbrica de l'exercici 1 (21 punts)
| Dimensió | 0 | 1 | 2 | 3 |
|---|---|---|---|---|
| Compilació | Sense configurar | Compila | base i trossos correctes |
A més amb límit de mida actiu |
| Secrets | N'hi ha algun | Cap per sort | Verificat a mà | Verificat al flux automàtic |
| HTTPS i domini | Sense HTTPS | Amb HTTPS | Amb redirecció | A més canònic i amb HSTS progressiu |
| Rutes | 404 en recarregar | Reserva | Amb codi 200 | A més amb API exclosa |
| Memòria cau | Sense configurar | Alguna cosa | Taula completa | Verificada amb curl i documentada |
| Seguretat | Cap capçalera | Algunes | Les cinc | CSP sense unsafe-inline i provada en mode informe |
| Documentació | No n'hi ha | Esmenta la plataforma | Amb justificació | Amb les implicacions de cada elecció |
Llindar: 15/21, amb obligatòriament 3 a «Secrets». Un secret filtrat invalida l'exercici: és l'única errada d'aquesta lliçó amb conseqüències reals fora del projecte.
Criteris d'acceptació de l'exercici 2 — Continu i operacions
| # | Criteri | Verificació |
|---|---|---|
| 1 | Un push a main desplega tot sol |
Veure el flux i el lloc actualitzat |
| 2 | Una fallada de verificar impedeix el desplegament |
Provocar-la |
| 3 | El paquet amb un secret fals trenca el flux | Provocar-ho i treure-ho |
| 4 | La comprovació de fum detecta un desplegament buit | Provocar-ho |
| 5 | Cada PR genera una vista prèvia | Obrir-ne una |
| 6 | Les vistes prèvies no s'indexen | Comprovar X-Robots-Tag o la meta |
| 7 | Revertir triga menys de 2 minuts | Cronometrat |
| 8 | El procediment està escrit | docs/operacions.md |
| 9 | Un error de producció arriba al servei | Provocar-lo |
| 10 | Aquest error no conté dades personals | Inspeccionar l'esdeveniment rebut |
| 11 | Les mètriques de camp inclouen la versió | Inspeccionar l'enviament |
| 12 | La PWA avisa de la versió nova | Demostració amb l'aplicació instal·lada |
| 13 | No hi ha bucle de recàrregues | Recarregar diverses vegades després d'actualitzar |
| 14 | Les memòries cau velles es netegen | caches.keys() a la consola: una de sola |
Criteris d'acceptació de l'exercici 3 — Llançament
| # | Criteri | Verificació |
|---|---|---|
| 1 | Les 33 comprovacions amb evidència | No n'hi ha prou amb marcar la casella |
| 2 | L'enllaç compartit mostra targeta correcta | Validador d'enllaços |
| 3 | El favicon es veu a la pestanya | Visual, en dos navegadors |
| 4 | El 404 és propi i ofereix sortida | Visitar una ruta inventada |
| 5 | robots.txt i sitemap.xml accessibles |
Per URL |
| 6 | El manifest permet instal·lar | Apareix l'opció d'instal·lació |
| 7 | L'avís de privacitat és honest i específic | Descriu la teva aplicació, no una plantilla genèrica |
| 8 | Hi ha llicència | Fitxer LICENSE |
| 9 | Lighthouse en producció documentat | Amb les diferències respecte a local explicades |
| 10 | Puntuació de capçaleres documentada | Amb les pendents anotades |
| 11 | Provat en mòbil real | Llista de troballes |
| 12 | Document signat i datat | docs/llancament.md |
Rúbrica global de la fita H6 primera part (24 punts)
| Dimensió | Pes | Què s'avalua |
|---|---|---|
| Compilació i secrets | 5 | base, trossos, zero secrets verificat automàticament |
| Servidor | 5 | Rutes, memòria cau, compressió, HTTPS canònic |
| Seguretat | 4 | Les cinc capçaleres, CSP real sense unsafe-inline |
| Desplegament continu | 4 | Automàtic, amb verificació, fum i vistes prèvies |
| Operacions | 3 | Reversió provada, procediment escrit, monitoratge sense dades personals |
| PWA | 2 | Actualització amb avís, sense bucle, memòries cau netes |
| Llançament | 1 | Les 33 comprovacions amb evidència |
Llindar: 17/24. Amb una condició que no es compensa: zero secrets al paquet. Tota la resta es pot millorar a la iteració següent; una clau filtrada, no.
Autoavaluació de la fita H6:
| Pregunta | Sí / No |
|---|---|
| Puc obrir una ruta interna en una pestanya nova i funciona? | |
He comprovat amb grep que no hi ha secrets a dist/? |
|
| Sé quant trigo a revertir, perquè ho he cronometrat? | |
| Un usuari amb la PWA instal·lada rebrà la meva propera correcció? | |
| El meu avís de privacitat descriu el que la meva aplicació fa de debò? | |
| He obert el meu lloc en un mòbil real amb dades mòbils? | |
| Podria explicar cadascuna de les cinc capçaleres de seguretat? |
Conclusió
El teu projecte ja no viu al teu portàtil: és a internet, amb HTTPS, i qualsevol el pot fer servir.
Saps què canvia quan el codi surt de la teva màquina —nou diferències, cadascuna font de sorpreses— i tens la regla que n'evita la meitat: no despleguis mai res que no hagis provat amb build i preview. Coneixes la compilació de producció per dins: empaquetat que redueix peticions, divisió de codi que retarda el que no es veu, minificació que només funciona bé gràcies als mòduls ES estàtics de 05-04, i el hashing del nom, que és la peça elegant que permet tenir alhora memòria cau d'un any i actualització immediata — i que explica per què index.html és l'únic que no en porta. Amb base ben configurat, que és la causa número u de primers desplegaments en blanc.
Tens claríssim el més important de la lliçó: no hi ha secrets al client. import.meta.env.VITE_* no és una variable: és una constant pública escrita literalment en un fitxer que es descarrega. Saps què hi pot anar i què no, saps que el raonament erroni («està en una variable d'entorn») confon on es va escriure el valor amb on acaba, i saps que l'única resposta correcta quan cal una clau és un intermediari al servidor. Amb la verificació per grep automatitzada al flux, perquè les bones intencions s'obliden i els robots que rastregen repositoris no.
Saps on allotjar amb una taula honesta que inclou el que les comparatives no diuen: que GitHub Pages no permet capçaleres reals, que Netlify, Vercel i Cloudflare Pages són equivalents per a un lloc estàtic, que un servidor propi és l'únic que t'obliga a entendre què passa, i que els plans gratuïts tenen límits que canvien. I saps per què HTTPS no és opcional: sense ell no hi ha service worker, ni PWA, ni contextos segurs, ni confiança.
Saps resoldre el problema que sorprèn tothom —les rutes d'una SPA—, amb reserva a index.html que retorna 200 i no 404, a les quatre plataformes, i amb l'exclusió de les rutes d'API que evita rebre HTML on esperaves JSON.
Tens la memòria cau ben feta, que és on més s'equivoca la gent: un any i immutable per al que porta hash, no-cache per al punt d'entrada, el service worker i el manifest. Saps que no-cache significa «revalida», no «no desis». I saps que un sw.js cachejat produeix l'errada més desconcertant que existeix: usuaris atrapats en una versió de fa tres desplegaments per molt que recarreguin.
Saps configurar les capçaleres de seguretat una a una, entenent-les: la CSP com a segona línia de defensa contra XSS —amb connect-src impedint l'exfiltració, frame-ancestors evitant el clickjacking, i l'advertiment que 'unsafe-inline' desactiva justament el que la política venia a donar—, implantada primer en mode informe; nosniff, Referrer-Policy, Permissions-Policy i HSTS amb el seu max-age progressiu perquè és difícil de revertir.
Saps què fer amb el backend: que no tenir-ne és legítim si està documentat amb les seves limitacions; que un backend com a servei resol el multiusuari sense escriure servidor, amb l'advertiment que les regles de seguretat són teves i una de mal escrita exposa totes les dades; que una funció sense servidor de vint línies n'hi ha prou per amagar una clau; i que escriure el teu és el camí de 11-07. I saps, sense matisos, que la validació del client mai no substitueix la del servidor — amb la recompensa que el teu domini sense dependències del navegador es pot importar tal qual a Node i executar les mateixes R1–R15 als dos costats, que és exactament per això que valia la pena la primera frontera de 11-01.
Tens desplegament continu amb verificació prèvia, versió del commit injectada —cosa que fa possible atribuir una regressió a un desplegament—, comprovació automàtica de secrets i prova de fum, perquè que un desplegament acabi no significa que el lloc funcioni. Amb vistes prèvies per branca i les seves tres precaucions, i amb una estratègia de reversió la regla de la qual és contraintuïtiva i correcta: revertir primer, investigar després, perquè revertir és reversible i arreglar amb pressa no ho és. Amb el post mortem la pregunta clau del qual no és qui s'ha equivocat sinó per què la CI no ho ha detectat.
Saps monitorar en producció sense convertir-te en un problema de privacitat: filtrar abans d'enviar, pathname en lloc de href, sendBeacon perquè funciona mentre la pàgina es tanca, i la consciència que un servei de seguiment és un tercer que rep dades dels teus usuaris. I saps recollir mètriques de camp amb web-vitals, amb la distinció de 09-01 entre laboratori i camp, i amb la versió adjunta per poder atribuir.
Saps actualitzar una PWA instal·lada, que és el problema més específic del desplegament d'una aplicació web moderna: l'estat «en espera» que pot durar dies, les tres peces que el resolen, les quatre regles —avisar sempre, la bandera contra el bucle de recàrregues, no-cache a sw.js, netejar memòries cau velles— i el service worker d'emergència que convé tenir desat abans de necessitar-lo.
I tens la llista de comprovació de llançament amb els seus 33 punts en tres blocs: tècnic, contingut i descobribilitat, i legal i operatiu — inclosos l'avís de privacitat honest i específic, l'advertiment que el consentiment de cookies té requisits legals que un botó d'«acceptar» no compleix, i la llicència.
El producte està publicat. I aquí apareix el que separa un projecte acabat d'un projecte que a més serveix per a alguna cosa: ningú no sap que existeix, ningú no sap quines decisions hi ha al darrere, i tu encara no has practicat com explicar-ho. Una feina que no es pot ensenyar ni defensar val, a la pràctica, molt menys del que és. Convertir-la en alguna cosa que algú entengui en dos minuts, que puguis demostrar en cinc, i que sàpigues explicar en una entrevista tècnica sense sonar ni insegur ni fanfarró, és Presentació i Revisió del Projecte.
Curs de JavaScript: De Principiant a Avançat
Mòdul 1: Introducció a JavaScript
- Què és JavaScript?
- Configuració del teu Entorn de Desenvolupament
- El teu Primer Programa en JavaScript
- Sintaxi i Conceptes Bàsics de JavaScript
- Variables i Tipus de Dades
- Operadors Bàsics
- Conversió de Tipus i Comparacions
- El Projecte del Curs: Nómada Tasques
Mòdul 2: Estructures de Control
- Sentències Condicionals
- Bucles: for, while, do-while
- Sentències Switch
- Control del Flux: break, continue i Bucles Imbricats
- Gestió d'Errors amb try-catch
Mòdul 3: Funcions
- Definició i Crida de Funcions
- Expressions de Funció i Funcions Fletxa
- Paràmetres i Valors de Retorn
- Àmbit i Closures
- Hoisting i el Context d'Execució
- Funcions d'Ordre Superior
- Recursivitat
Mòdul 4: Objectes i Arrays
- Introducció als Objectes
- Mètodes d'Objecte i la Paraula Clau
this - Arrays: Conceptes Bàsics i Mètodes
- Iteració sobre Arrays
- Cercar, Ordenar i Agregar Dades: find, sort i reduce
- Desestructuració d'Arrays
- Desestructuració d'Objectes, Spread i Rest
- JSON i Còpies d'Objectes
Mòdul 5: Objectes i Funcions Avançades
- Prototips i Herència
- Classes i Programació Orientada a Objectes
- Encapsulació: Getters, Setters i Camps Privats
- Mòduls i Importació/Exportació
- JavaScript Asíncron: Callbacks
- Promeses i Async/Await
- El Bucle d'Esdeveniments i la Cua de Microtasques
- Iteradors i Generadors
Mòdul 6: El Model d'Objectes del Document (DOM)
- Introducció al DOM
- Selecció i Manipulació d'Elements del DOM
- Gestió d'Esdeveniments
- Propagació, Delegació i Esdeveniments Personalitzats
- Creació i Eliminació d'Elements del DOM
- Renderitzat de Llistes i Plantilles HTML
- Gestió i Validació de Formularis
Mòdul 7: APIs del Navegador i Temes Avançats
- Emmagatzematge Local i de Sessió
- Fetch API i AJAX
- Peticions Robustes: Errors, Timeouts i AbortController
- WebSockets
- Service Workers i Aplicacions Web Progressives (PWAs)
- APIs del Navegador Essencials
- Introducció a WebAssembly
Mòdul 8: Proves i Depuració
- Depuració de JavaScript
- Qualitat de Codi: ESLint, Prettier i Convencions
- Proves Unitàries amb Jest
- Dobles de Prova: Mocks, Stubs i Spies
- Proves d'Integració
- Proves d'Extrem a Extrem amb Cypress
Mòdul 9: Rendiment i Optimització
- Mesurar Abans d'Optimitzar: DevTools i Web Vitals
- Optimització del Rendiment de JavaScript
- Gestió de Memòria
- Manipulació Eficient del DOM
- Càrrega Diferida i Divisió de Codi
Mòdul 10: Frameworks i Llibreries de JavaScript
- Per Què Existeixen els Frameworks
- Introducció a React
- Gestió d'Estat amb Redux
- Conceptes Bàsics de Vue.js
- Conceptes Bàsics d'Angular
- Triar el Framework Adequat
