Upload files to "/"
This commit is contained in:
425
pwa-notes.md
Normal file
425
pwa-notes.md
Normal file
@@ -0,0 +1,425 @@
|
||||
|
||||
|
||||
---
|
||||
|
||||
## Part 1 – Turn your current app into a PWA
|
||||
|
||||
You already have:
|
||||
|
||||
* `index.html`
|
||||
* `style.css`
|
||||
* `script.js`
|
||||
|
||||
We’ll add:
|
||||
|
||||
* `manifest.webmanifest`
|
||||
* `sw.js`
|
||||
* A couple of small changes to `index.html`
|
||||
|
||||
I’ll assume your app is served from the site root (e.g. `https://mydomain.com/`) and that your icon is a PNG. You can keep everything inside `web/` in your repo; when you deploy, make sure these files end up in the same directory as `index.html`.
|
||||
|
||||
---
|
||||
|
||||
### 1. Create icons from your existing icon file
|
||||
|
||||
Chrome expects at least:
|
||||
|
||||
* **192×192** PNG
|
||||
* **512×512** PNG
|
||||
|
||||
If your icon file is large enough (e.g. 1024×1024), generate:
|
||||
|
||||
* `icons/icon-192.png`
|
||||
* `icons/icon-512.png`
|
||||
|
||||
(Use something like GIMP, Figma, or an online “PWA icon generator”.)
|
||||
|
||||
Folder structure (in `web/`):
|
||||
|
||||
```text
|
||||
web/
|
||||
index.html
|
||||
style.css
|
||||
script.js
|
||||
sw.js
|
||||
manifest.webmanifest
|
||||
icons/
|
||||
icon-192.png
|
||||
icon-512.png
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2. Add `manifest.webmanifest`
|
||||
|
||||
Create `web/manifest.webmanifest`:
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Life Manager",
|
||||
"short_name": "LifeMgr",
|
||||
"description": "Offline-first life organizer with calendar, tags, and backups.",
|
||||
"start_url": "/",
|
||||
"scope": "/",
|
||||
"display": "standalone",
|
||||
"background_color": "#0f1014",
|
||||
"theme_color": "#0f1014",
|
||||
"orientation": "portrait",
|
||||
"icons": [
|
||||
{
|
||||
"src": "/icons/icon-192.png",
|
||||
"sizes": "192x192",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
},
|
||||
{
|
||||
"src": "/icons/icon-512.png",
|
||||
"sizes": "512x512",
|
||||
"type": "image/png",
|
||||
"purpose": "any maskable"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
> `start_url` and `scope` assume the app is at the root. If you serve from `/life-manager/`, change both to `"/life-manager/"`.
|
||||
|
||||
---
|
||||
|
||||
### 3. Wire manifest + theme color into `index.html`
|
||||
|
||||
Modify your `<head>` in `web/index.html` to include the manifest and theme color.
|
||||
|
||||
Right now you have:
|
||||
|
||||
```html
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>life-manager</title>
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
```
|
||||
|
||||
Change to:
|
||||
|
||||
```html
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>life-manager</title>
|
||||
|
||||
<!-- PWA: theme color (matches manifest) -->
|
||||
<meta name="theme-color" content="#0f1014">
|
||||
|
||||
<!-- PWA: manifest -->
|
||||
<link rel="manifest" href="manifest.webmanifest">
|
||||
|
||||
<link rel="stylesheet" href="style.css">
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;600;700&display=swap" rel="stylesheet">
|
||||
</head>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 4. Register a service worker from `index.html`
|
||||
|
||||
Just before `</body>` you already have:
|
||||
|
||||
```html
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
Change to:
|
||||
|
||||
```html
|
||||
<script src="script.js"></script>
|
||||
|
||||
<!-- PWA: Service worker registration -->
|
||||
<script>
|
||||
if ('serviceWorker' in navigator) {
|
||||
window.addEventListener('load', () => {
|
||||
navigator.serviceWorker.register('/sw.js')
|
||||
.catch(err => {
|
||||
console.error('SW registration failed', err);
|
||||
});
|
||||
});
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
```
|
||||
|
||||
> If you host the app under a path (e.g. `/life-manager/`), change the registration path to `'/life-manager/sw.js'`.
|
||||
|
||||
---
|
||||
|
||||
### 5. Add `sw.js` (service worker)
|
||||
|
||||
Create `web/sw.js`:
|
||||
|
||||
```js
|
||||
// sw.js - simple cache-first service worker
|
||||
|
||||
const CACHE_NAME = 'lifemgr-cache-v1';
|
||||
|
||||
// List everything needed for offline
|
||||
const ASSETS = [
|
||||
'/',
|
||||
'/index.html',
|
||||
'/style.css',
|
||||
'/script.js',
|
||||
'/manifest.webmanifest',
|
||||
'/icons/icon-192.png',
|
||||
'/icons/icon-512.png'
|
||||
];
|
||||
|
||||
// Install: cache core assets
|
||||
self.addEventListener('install', (event) => {
|
||||
event.waitUntil(
|
||||
caches.open(CACHE_NAME).then((cache) => {
|
||||
return cache.addAll(ASSETS);
|
||||
})
|
||||
);
|
||||
self.skipWaiting();
|
||||
});
|
||||
|
||||
// Activate: cleanup old caches if you bump the version
|
||||
self.addEventListener('activate', (event) => {
|
||||
event.waitUntil(
|
||||
caches.keys().then((keys) =>
|
||||
Promise.all(
|
||||
keys.map((key) => {
|
||||
if (key !== CACHE_NAME) {
|
||||
return caches.delete(key);
|
||||
}
|
||||
})
|
||||
)
|
||||
)
|
||||
);
|
||||
self.clients.claim();
|
||||
});
|
||||
|
||||
// Fetch: cache-first strategy
|
||||
self.addEventListener('fetch', (event) => {
|
||||
const request = event.request;
|
||||
|
||||
// Only handle GET
|
||||
if (request.method !== 'GET') return;
|
||||
|
||||
event.respondWith(
|
||||
caches.match(request).then((cached) => {
|
||||
// Serve from cache if available, else network
|
||||
return (
|
||||
cached ||
|
||||
fetch(request).catch(() => {
|
||||
// Optional: custom offline response for HTML requests
|
||||
if (request.headers.get('accept')?.includes('text/html')) {
|
||||
return caches.match('/index.html');
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
);
|
||||
});
|
||||
```
|
||||
|
||||
> Again, if hosted under a sub-path, prepend `/life-manager` in `ASSETS`.
|
||||
|
||||
Now when you deploy over **HTTPS**, Chrome will see:
|
||||
|
||||
* a valid manifest,
|
||||
* a service worker controlling the scope,
|
||||
* and will treat it as a PWA with install capability.
|
||||
|
||||
---
|
||||
|
||||
### 6. Deploy and sanity-check
|
||||
|
||||
1. Deploy `web/` to somewhere HTTPS (Netlify, Vercel, etc.).
|
||||
|
||||
2. Open it in Chrome (Android or desktop).
|
||||
|
||||
3. In DevTools → Application → Manifest/Service Workers:
|
||||
|
||||
* Confirm manifest is found,
|
||||
* service worker is active,
|
||||
* and pages are controlled.
|
||||
|
||||
4. On Android Chrome, you should see **“Install app” / “Add to Home screen”** in the menu.
|
||||
|
||||
Once this works, you’re ready for the **Trusted Web Activity** step.
|
||||
|
||||
---
|
||||
|
||||
## Part 2 – Wrap your PWA in a Trusted Web Activity (TWA) with Bubblewrap
|
||||
|
||||
Now you have a real PWA live at (for example):
|
||||
|
||||
```text
|
||||
https://your-domain.com/
|
||||
```
|
||||
|
||||
We’ll use **Bubblewrap**, which takes your hosted `manifest.webmanifest` and generates an Android project that starts your PWA as a TWA.
|
||||
|
||||
### 1. Prereqs checklist
|
||||
|
||||
You need:
|
||||
|
||||
* Node.js (v14+)
|
||||
|
||||
* Java JDK
|
||||
|
||||
* Android SDK
|
||||
|
||||
* Your PWA served via **HTTPS** with:
|
||||
|
||||
* manifest,
|
||||
* service worker,
|
||||
* and working offline (which we just did).
|
||||
|
||||
And of course, a **Google Play developer account** for publishing later.
|
||||
|
||||
---
|
||||
|
||||
### 2. Install Bubblewrap CLI
|
||||
|
||||
```bash
|
||||
npm install -g @bubblewrap/cli
|
||||
bubblewrap --version
|
||||
```
|
||||
|
||||
(Just to confirm it’s installed.)
|
||||
|
||||
---
|
||||
|
||||
### 3. Initialize a Bubblewrap project from your manifest
|
||||
|
||||
Pick a new folder for the Android project:
|
||||
|
||||
```bash
|
||||
mkdir life-manager-twa
|
||||
cd life-manager-twa
|
||||
```
|
||||
|
||||
Run the init command, pointing to your **hosted** manifest:
|
||||
|
||||
```bash
|
||||
bubblewrap init --manifest https://your-domain.com/manifest.webmanifest
|
||||
```
|
||||
|
||||
Bubblewrap will:
|
||||
|
||||
* Download the web manifest.
|
||||
|
||||
* Ask you about:
|
||||
|
||||
* **Package ID** (e.g. `com.yourdomain.lifemanager`)
|
||||
* **App name** (can reuse `"Life Manager"`)
|
||||
* **Colors, icons, orientation** (it can reuse values from the manifest)
|
||||
* **Signing key** (create a new keystore or use an existing one)
|
||||
|
||||
It will generate:
|
||||
|
||||
* An Android project (Gradle, Java/Kotlin code that opens your URL as a TWA).
|
||||
* A `twa-manifest.json` with config about your app.
|
||||
* An `assetlinks.json` file template for your website.
|
||||
|
||||
---
|
||||
|
||||
### 4. Build the APK/App Bundle
|
||||
|
||||
Inside that new project folder:
|
||||
|
||||
```bash
|
||||
bubblewrap build
|
||||
```
|
||||
|
||||
This compiles the Android project and outputs a release build (APK/App Bundle).
|
||||
|
||||
You can also install directly on a connected device:
|
||||
|
||||
```bash
|
||||
bubblewrap install
|
||||
```
|
||||
|
||||
At this stage, when you open the app on the device:
|
||||
|
||||
* It will usually open as a **Custom Tab** (with a little browser UI) until we set up the trust link (Digital Asset Links).
|
||||
|
||||
---
|
||||
|
||||
### 5. Set up Digital Asset Links (`assetlinks.json`)
|
||||
|
||||
To get true **Trusted Web Activity** (full screen, no browser bar), Android needs to verify that:
|
||||
|
||||
* The app signing key
|
||||
* And your website’s domain
|
||||
|
||||
belong to the same owner, via **Digital Asset Links**.
|
||||
|
||||
Bubblewrap will give you an `assetlinks.json` snippet. If you need a template, it looks like this:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"relation": ["delegate_permission/common.handle_all_urls"],
|
||||
"target": {
|
||||
"namespace": "android_app",
|
||||
"package_name": "com.yourdomain.lifemanager",
|
||||
"sha256_cert_fingerprints": [
|
||||
"AA:BB:CC:DD:EE:FF:..."
|
||||
]
|
||||
}
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
* Replace `com.yourdomain.lifemanager` with your actual package ID.
|
||||
* Replace the fingerprint with the SHA-256 of the keystore you **actually sign the Play build with** (Bubblewrap/Android Studio docs show how to get this).
|
||||
|
||||
Host this file at:
|
||||
|
||||
```text
|
||||
https://your-domain.com/.well-known/assetlinks.json
|
||||
```
|
||||
|
||||
Once this is accessible and valid:
|
||||
|
||||
* Opening your app on device will show your PWA **full-screen** as a Trusted Web Activity, not a normal Chrome tab.
|
||||
|
||||
---
|
||||
|
||||
### 6. Open in Android Studio (optional but recommended)
|
||||
|
||||
You can open the generated Android project in Android Studio for:
|
||||
|
||||
* Changing app name, icon, version code, etc.
|
||||
* Building signed App Bundles for the Play Store.
|
||||
* Running on emulators.
|
||||
|
||||
Bubblewrap’s `build` and `install` commands already use Gradle under the hood, so this is optional, but very handy for Play Store upload.
|
||||
|
||||
---
|
||||
|
||||
### 7. Upload to Google Play
|
||||
|
||||
Once:
|
||||
|
||||
* PWA runs well over HTTPS.
|
||||
* Asset Links are configured and verified.
|
||||
* The app works as full-screen TWA on a device.
|
||||
|
||||
Then:
|
||||
|
||||
1. In the **Google Play Console**, create a new app.
|
||||
2. Upload the signed **App Bundle/APK** from your Bubblewrap/Android Studio build.
|
||||
3. Fill in listing details (screenshots, description, etc.).
|
||||
4. Roll out a test track or production release.
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user