Professional Portfolio PWA
Build one polished, public professional profile that opens from a link, installs on a phone, works offline, and makes it easy to contact its owner. Prefer a static PWA and Firebase Hosting for the first version; defer paid services, native app stores, AI replies, and subscription infrastructure until a working public profile has been tested. If the owner explicitly requests private recruiter chat, add Firebase Auth and Firestore with strict per-user security rules; do not ship open Firestore rules or place service-account credentials in browser code.
Successful default design
- Mobile-first editorial profile with the owner's real portrait, concise headline, location, prominent contact action, and résumé download above the fold.
- Add measured accomplishments, chronological experience, scannable skills, and education below the hero.
- Keep the contact options explicit:
mailto:,sms:,tel:, and the owner's public professional profile. - Let CSS follow
prefers-color-scheme; avoid a redundant in-app theme toggle if the owner wants the system appearance. - Use the same unaltered portrait on the page and app icon. Do not add a badge, arbitrary avatar, decorative halo, or invented photo.
- Include an honest privacy page and an accurate “Get your own app” early-access CTA. Do not imply that a paid plan, checkout, live chat, or chatbot exists until it is actually implemented.
Workflow
1. Verify and model the profile
- Use a résumé explicitly provided by the owner as the source of truth. Verify names, dates, employers, role titles, education, contact details, and metrics.
- Treat LinkedIn data as public only when it is accessible without authentication. Never request account credentials or use private LinkedIn data. Say when only a public excerpt is available; do not invent missing employment history.
- Reuse the owner's portrait only with explicit permission. Record its source and edits. Preserve the original résumé PDF as a read-only asset.
- Put factual profile content into a small structured JSON file. Keep private deployment configuration, tokens, API keys, and personal credentials out of both public assets and source control.
- Verify the bundled and hosted PDF byte-for-byte with SHA-256.
2. Build the static PWA first
Use semantic HTML, responsive CSS, and small dependency-free JavaScript. A static portfolio does not need Firebase Authentication, Firestore, Cloud Functions, server-side AI, or an npm build pipeline. Keep the profile static unless the owner explicitly requests authenticated chat or another feature that needs a backend.
Include:
index.html,styles.css, and a local profile JSON file;- a Web App Manifest with portrait icons in the required sizes;
- a service worker with an explicit, versioned offline app-shell cache;
- the unchanged résumé and portrait;
- a privacy page explaining direct handoffs and offline storage;
- a Firebase Hosting configuration with restrictive security headers.
Use hosting response headers to set Cache-Control: no-cache for mutable, unversioned app-shell resources: /, HTML, CSS, JavaScript, profile JSON, manifests, and /service-worker.js. Add explicit rules for root paths, manifests, and the service worker where the hosting provider's glob rules need them. Inspect the actual deployed response headers; do not assume the config overrides the host's default max-age.
no-cache means a browser may keep a response but must revalidate it; it does not mean no-store. Prefer it over no-store for an offline PWA. Large, unchanged media may use normal or long-lived caching.
Version the service-worker app-shell cache and bump its name whenever cached shell resources change. When installing a new shell, pass Request(url, { cache: "reload" }) for mutable entries to cache.addAll(); otherwise a still-fresh HTTP-cache response can seed the new offline cache with stale files. Activate the new worker, remove only older caches with the app's own prefix, and claim clients. Cache-Control alone does not bypass a resource already served from the service worker's Cache Storage.
The first version should send email/text/call actions directly to the visitor's chosen app; it should not claim to collect messages. Real in-app chat, AI replies, and an owner inbox require a separate authenticated backend and clear privacy, retention, rate-limit, and human-handoff behavior.
3. Respect install-prompt rules
Browsers do not allow an app to force the native install confirmation on page load. Prefer a small, non-modal invitation in normal document flow at the very top of the profile, before navigation. Never obscure the portfolio, dim the page, disable interaction, or open a full-screen prompt automatically.
- Listen for
beforeinstallprompt. - Call
preventDefault()and retain the browser event. - When supported, show an accessible, dismissible banner at the top of the page with Install app and Not now buttons. The browser's native confirmation must only open after the user taps Install app.
- Do not assume every browser fires
beforeinstallprompt. On iOS Safari, Android browsers with a manual home-screen install path, or desktop Chromium browsers with a browser-menu install path, show the same non-modal banner with a How to add action when the native event is absent. Reveal concise iOS Share → Add to Home Screen, Android browser-menu, or desktop address-bar and browser-menu instructions inline rather than in an automatic full-screen overlay. If a native event arrives later, change the action copy without restarting the countdown. Where no supported install path exists, do not show a misleading install action. - Keep the banner in normal document flow so it never covers or blocks the page. Respect safe-area insets, use compact responsive layout, maintain 44–48px action targets, and verify 320px, 360px, and 390px widths. Avoid unnecessary countdown timers; use one only when the owner explicitly requests a time-limited message, and respect reduced-motion preferences.
- If the owner explicitly wants a novelty invitation on every visit, show it once per page load. A Not now click or self-destruct closes only the current page's invitation; do not persist shown/dismissed state across reloads.
- After
appinstalled, persist a local installed marker. Also detectdisplay-mode: standaloneandnavigator.standalone, hide install controls, and set the marker when an installed PWA starts. If a newbeforeinstallpromptevent fires, clear a stale marker. - Never display the invitation in an already installed standalone PWA.
Test native-prompt eligibility, Android/iOS manual fallback, dismissed, self-destructed, and already-installed states separately. Assert that an install banner is a visible top-page <aside>, that no install dialog opens on load, and that the document remains interactive while the banner is shown. With per-page novelty behavior, verify Not now and self-destruct close only the current page's invitation and that a reload shows it again until installation.
4. Match system theme and launch splash
Set CSS variables under @media (prefers-color-scheme: dark) and set matching light/dark theme-color meta tags. For Android PWA splash screens, provide a light manifest and a dark manifest with identical app ID, scope, start URL, and icons, but different background_color and theme_color.
Run a small same-origin script synchronously in <head> before the browser discovers the manifest link. It checks matchMedia("(prefers-color-scheme: dark)") and inserts a link to the matching manifest. Precache both manifests and this selector script. Bump the service-worker cache name after changing shell resources.
Android caches the launch/splash color in the installed WebAPK. Reinstall or wait for the WebAPK metadata update after changing system theme. Verify the cold-start splash on an actual device; do not assume CSS alone changes an installed PWA's splash.
5. Test locally and on a phone before publishing
Run JavaScript syntax checks and parse all JSON manifests/configuration. Check unique HTML IDs, working in-page anchors, actual install icon sizes, all service-worker precache URLs, and the résumé checksum.
Serve over loopback:
python -m http.server 4173 --bind 127.0.0.1 --directory .\public
For a USB-connected Android phone:
adb reverse tcp:4173 tcp:4173
Open http://127.0.0.1:4173/ in Chrome. Localhost/loopback is a secure context for service-worker testing; this address is not public and works only while the computer's server and USB connection are available.
Only keep a local preview server running while actively testing. Stop the exact server process you started and verify its port has no listener afterward; identify the process before stopping it, and never kill an unrelated process just because it uses the preview port.
Verify at phone and desktop widths:
- the portrait, primary contact action, and résumé action are visible;
- all email/SMS/phone/LinkedIn links are correctly formed;
- light/dark page colors follow the OS;
- the install invitation appears only when appropriate and is dismissible;
- if configured as a full-screen modal, its backdrop is frosted, Yes, Install and Not now are obvious, narrow-screen buttons do not overlap, and it auto-closes after five seconds without input;
- the prompt-only design has no separate header install button;
- mutable hosted app resources actually return
Cache-Control: no-cache, and the updated worker fills its versioned cache with fresh shell resources; - the already installed PWA has no install invitation;
- service worker controls the page and caches the résumé for offline use.
When someone reports stale styling or behavior, first inspect the live HTML/CSS/ JavaScript response headers and active service-worker/cache version. Browsers may have responses cached under an earlier max-age, and Cache Storage is separate from the HTTP cache. Deploy revalidation headers and a new worker/cache version instead of routinely asking visitors to clear all browsing data. A previously open page may still need a refresh or reopen to adopt the updated worker; a new header cannot retroactively change an already cached response.
Test binary document integrity with the SHA-256 of the original file. Use actual phone screenshots for launcher icons and splash; emulator screenshots alone do not prove installed-WebAPK behavior.
6. Deploy to Firebase Hosting
For a static site, Firebase Hosting's Spark tier is a suitable initial option; confirm current plan limits and do not enable billing or paid products unless the owner explicitly requests them.
For maintenance of an existing portfolio, deploy verified changes to its currently configured Firebase Hosting project by default unless the owner asks to keep the work local. Resolve the project ID from existing Firebase configuration, CLI aliases, and the known live URL; do not ask the owner to repeat the project ID, public directory, or deploy command when the project already establishes them. Deploy Hosting only with the owner's local CLI authentication. If the target is inconsistent or authentication is missing, ask only for the specific blocker. Never enable billing without explicit approval.
- The owner must sign in with their own Google account, complete MFA/passkey verification, and personally review/accept Firebase Terms when creating a project.
- Authenticate Firebase CLI locally:
npx.cmd --yes firebase-tools@latest login
npx.cmd --yes firebase-tools@latest projects:list
Non-interactive environments may provide a browser device/code flow. Keep one-time codes and OAuth tokens in the owner's local browser/terminal; never request, echo, or paste them into chat.
- Deploy the existing static project, not an invented sample app:
powershell -NoProfile -ExecutionPolicy Bypass -File .\Deploy.ps1 -ProjectId PROJECT_ID
- Verify the public HTTPS
web.apphomepage, current modal/stylesheet/script, mutable-assetCache-Controlheaders, light/dark manifests, icons, updated service-worker cache version, security headers, and exact résumé SHA-256. Check the actual hosted responses, not only local config. A Firebase API key is not CLI authentication and is not required to deploy static Hosting.
7. Connect a purchased custom domain safely
The owner must purchase/control the domain. Never buy it on the owner's behalf. Add the domain with Firebase Hosting's Add custom domain wizard and use only the exact records Firebase supplies.
For a GoDaddy-managed apex domain:
- Navigate to the domain's DNS-record editor. If GoDaddy requires phone/profile verification, the owner completes that directly; never handle or request password, passkey, SMS, or authenticator codes.
- Add the exact Firebase ownership TXT and A records with host/name
@. - Remove only the specific parking A records Firebase explicitly says to remove.
- Preserve nameservers, MX records, SPF/DKIM/DMARC and every unrelated TXT record. Do not replace nameservers or guess
www, AAAA, CAA, or A values. - Check the authoritative nameserver and at least two public resolvers. Click Verify in Firebase after TXT/A propagation. Keep the TXT for certificate renewal.
- Firebase certificate provisioning may take up to 24 hours. Until TLS is ready, the custom domain can return a certificate error; keep the live
web.appURL available. Distinguish DNS correctness, Firebase verification status, and HTTPS certificate readiness instead of repeatedly changing correct DNS.
8. Keep deployment efficient and safe
- Avoid multiple near-identical public portfolio apps; publish one polished PWA and keep design candidates as private backups.
- Prefer local static assets, browser testing, and Firebase Hosting; do not add analytics or paid backend services for a simple portfolio.
- Avoid duplicate AI-agent work. If the user explicitly requests an agent, assign one bounded deploy owner and do not deploy concurrently from the parent session.
- Use the external source docs when DNS behavior is uncertain; do not guess.
- At handoff, state clearly which URL is public, whether the custom domain is live, what DNS/certificate step remains, and whether chat/payment functionality is actually implemented.
Optional verified-provider one-to-one chat
Only implement chat when the owner explicitly requests it. Keep this a real, first-party, text-only conversation, not a bot pretending to be the owner.
Architecture and identity
- Visitors sign in with Firebase Authentication using a verified Google or GitHub identity before starting a conversation. Require a verified email and explain, before the first message, that the owner stores and reads the conversation. Request GitHub's
user:emailscope when needed. - Use one Firestore document per visitor at
conversations/{auth.uid}and put messages below it atconversations/{auth.uid}/messages/{messageId}. This makes the intended privacy boundary obvious: a visitor may only address their own thread; visitors cannot list the parent collection or read each other's message subcollections. - Prefer one sign-in controller and one role-aware chat UI: each verified visitor sees only their thread, while the verified owner sees the inbox and settings in the same panel. Check
user.emailVerifiedand that the active provider is explicitly supported (google.comorgithub.com) before starting visitor reads. Keep owner access stricter: require both the exact owner email andsignInProvider == "google.com"in the client and Firestore Rules. This UI decision needs neither a server function nor a Blaze upgrade. - Treat the client role check as presentation only. Enforce the same verified visitor-provider identity and Google-only owner email independently in Firestore Rules; never rely on hidden elements, route names, or client checks to authorize data access.
- Gate private subscriptions by role before issuing reads: an ordinary verified visitor may get only their own conversation and message subcollection, while only the owner may list the inbox or read other visitors' threads. Public chat availability may be readable if it contains only non-sensitive status; it must not expose conversation data.
- Keep sign-in, the visitor thread, and the owner's inbox/settings inside one fixed, collapsible lower-right chat bubble; don't send the user to a separate chat/admin page. Preserve old
/chat.htmland/admin.htmllinks by redirecting to the portfolio with a one-use query flag that opens the bubble, then remove that flag withhistory.replaceState. Reuse the same role-aware component and controller; don't duplicate the chat implementation. - Make the panel phone-safe and accessible: use safe-area insets and dynamic viewport units, an accessible expanded state, focus restoration, and Escape to collapse. Verify at 320px and 360px that the panel is not clipped and that the minimize control and Escape both close it. Add every shared fragment and controller module to the service-worker app shell and bump its cache version when those assets change.
- On mobile PWAs,
signInWithRedirectleaves and reloads the current page. Save a short-lived panel-open marker insessionStorageimmediately beforesignInWithRedirectand reopen the bubble aftergetRedirectResultreturns. This keeps the owner or recruiter in the same conversation surface after provider sign-in. - Show no login credential, service-account key, or backend secret in the PWA. The Firebase Web API key and Web SDK config are public client configuration, not authorization. Restrict the key to expected HTTP referrers/APIs as defense-in-depth; the Firestore rules are the security boundary.
- Store an explicit public chat-availability setting in a readable, single non-sensitive settings document; let only the verified owner change it. Fail closed (no new chats) until the owner explicitly turns availability on. Distinguish "accept new chats" from job-search status. The owner can remain open to networking while not looking for a job. A missing initial settings document means chat is not yet set up: show the owner the settings controls, expand them, and explain that saving will publish availability. Don't treat a missing document as an authentication or network error.
- Provide a private owner list of conversations, a live open-thread window, unread indicators, send/reply, and close/reopen. The visitor's verified provider profile name and email become visible to the owner; disclose that fact. Messages and identity are stored by Firebase. Avoid claiming end-to-end encryption, anonymity, automatic deletion, or AI replies unless implemented.
Moderated public recommendations
- Require the same verified sign-in for submission; keep the form inside the authenticated visitor view. Collect the recommender's public name, title, organization, working relationship, and quote, plus an explicit consent to publish those fields after owner approval. Keep the verified sign-in email private and disclose that the owner can see it.
- Store private submissions and public approved entries in separate collections. Visitors may read only their own submission/status; only the Google-only owner can list submissions or review them. Require owner approval before any recommendation is publicly readable.
- Publish a sanitized record atomically with the private status transition using a Firestore batch and
getAfter()checks. Never put email, Firebase UID, provider details, or consent metadata into the public record. Use random publication document IDs rather than exposing the submitter's UID. Unpublish with an atomic status change and public-document deletion; let rejected submitters revise and resubmit if that behavior is desired. - Render public text with
textContent, notinnerHTML, and show the public section only when approved records exist. Emulator tests must prove verified provider access, visitor isolation, explicit consent, owner-only moderation, atomic publish/unpublish, and that no private email is exposed.
Firestore security-rule invariants
- Start with deny-by-default rules. Never use Firebase's permissive test mode or
allow read, write: if truein a deployment. - A verified Google/GitHub visitor can create/read only a conversation whose document ID equals their own Firebase Auth UID. Other visitors cannot list or access that conversation. Do not allow unauthenticated listing.
- Only the verified owner can list all conversations, read all threads, change shared availability, or reply as the owner. A recruiter cannot write with an owner UID/name or update another user's conversation.
- Validate allowed field sets, sender UID/name against the auth token, server timestamps, message length (for example, 2,000 characters), chat status, and per-thread unread counters. Keep messages append-only. For each message, atomically create the message document and update only that parent conversation in one Firestore transaction/batch; use
getAfter()so rules verify that both writes refer to the same sender/body/message ID. Add a short per-thread cooldown to the parent update rule to constrain chat spam. - A visitor may update only their own read counter and send state. The owner may update the owner read counter, set
open/closed, and reply. New threads require the availability setting; existing open threads can continue while the owner pauses creation of new threads. - Do not include attachments or arbitrary URL fetching in an initial text-chat MVP. Don't advertise private upload or message-deletion features unless implemented and security-tested.
Test these invariants against the Firestore Emulator, not just by clicking through the UI. An isolated rule test should prove: unauthenticated reads fail; visitor A can read A but not B; visitors cannot list threads; an unverified or unsupported-provider account is denied; a look-alike account cannot act as owner; owner can query/reply; visitor and owner message transactions are atomic; payload limits, first-message consent, closed state, chat availability, unread state, and cooldown are enforced. Run the emulator under a demo project ID. If port 8080 is already occupied by another user/workload, leave that process alone and configure the emulator to use another available port (for example 18080).
Firebase setup gates and user interactions
- Register a Firebase Web app and use the official SDK config. A Firebase API key does not authenticate Firebase CLI and is not needed for static Hosting.
- Enable the Firestore API if the database-create command returns HTTP 403 for a disabled API. Enable Google sign-in in Firebase Console; enable GitHub only after creating the OAuth App and entering its client ID/secret in Firebase. Keep the GitHub secret in Firebase Console, never in browser code or source. Firebase LinkedIn OIDC requires Identity Platform; get explicit approval for any plan/billing change and do not show a nonfunctional LinkedIn button.
- Add both active Hosting origins to Firebase Authentication → Authorized domains (
<project>.web.appand the purchased custom apex domain). A working Firebase Hosting page does not imply that Authentication trusts its hostname. - Firestore database location is permanent. Ask the owner to choose the region before creating
(default); prefer a nearby regional location where suitable. Create Standard edition with deletion protection, and deploy rules before enabling the chat. Don't ask the owner to change an existing database region without first explaining that recreation is destructive. - Only the owner can accept Firebase/Google legal terms, complete MFA/passkey verification, choose the permanent Firestore region, and approve billing. Stop for those actions instead of clicking consent or enabling a paid plan.
Google OAuth redirect mismatch
The PWA must use an authDomain that works with the actual origin and browser. When using signInWithRedirect with Firebase Hosting on <project>.web.app or a custom domain, Firebase's documented same-origin-auth approach needs both:
- The actual site host in Firebase Authentication → Authorized domains, and
- The exact corresponding OAuth Web client Authorized redirect URI:
https://<actual-host>/__/auth/handler
For example, https://example.web.app/__/auth/handler and https://example.com/__/auth/handler are distinct callbacks. Preserve the existing https://<project>.firebaseapp.com/__/auth/handler; add each actively used Hosting callback instead of replacing the Firebase default. If Google displays redirect_uri_mismatch, use the attempted host from the browser URL/error, add that exact callback, save, then retry sign-in. Don't expose OAuth client secrets. signInWithPopup is an alternative when the popup works and is launched from an explicit click, but can be less reliable on mobile and installed PWAs; test on target devices.
Firebase Google Authentication also loads https://apis.google.com/js/api.js. If the Hosting Content Security Policy omits https://apis.google.com from script-src, the gapi loader is blocked and sign-in can return to the app without restoring a Firebase user. Inspect the phone's DevTools Console and allow that exact script origin (alongside Firebase's https://www.gstatic.com SDK scripts); recheck the deployed CSP header rather than reinstalling the PWA. A connect-src error for a Firebase .js.map source-map request is also a useful indication that the deployed policy differs from the SDK's resource origins. Confirm the real Firebase endpoints and wss://firestore.googleapis.com remain allowed. After deploying a header change, reopen or refresh the PWA; reinstalling does not repair a CSP response header.
GitHub OAuth is a separate provider setup: use the exact Firebase Auth callback for the active authDomain, request the user:email scope, and keep the GitHub client secret in Firebase Authentication. A single GitHub OAuth App callback does not automatically cover both the custom domain and the web.app host.
Authentication/chat diagnostics
- If a supported-provider sign-in fails, inspect the installed phone PWA's Console and Network responses. Check the actual origin (
<project>.web.appversus the custom domain), the deployed Content Security Policy, and the OAuth error before changing OAuth callbacks or reinstalling. Separate OAuth errors (redirect_uri_mismatch, unauthorized domain, popup/redirect failure) from Firebase Auth restoration and Firestore permission-denied errors. For GitHub, also check that the provider is enabled and its OAuth callback exactly matches the activeauthDomain. - On redirect return, call
getRedirectResultand also subscribe toonAuthStateChanged; do not assume a successful provider page redirect means Firestore accepted the identity or an owner role. - Don't collapse redirect restoration, database initialization, public-settings reads, and message-listener errors into the same "chat couldn't connect" message. Log/show the safe Firebase
error.codeand identify which stage failed; never display OAuth codes, API keys, or raw identity tokens. - Keep connection/settings errors in a shared visible region, not inside a role-specific panel that is hidden while signed out; otherwise a Firestore failure can look like endless setup with no explanation.
- Distinguish the absent
chatSettings/publicdocument (owner needs to save initial settings) from a FirestoreonSnapshoterror. Show the safe Firebase error code in a shared region even before sign-in; never label every status as "Chat is being set up." - If sign-in succeeds but there is no composer, show whether chat is unavailable, the conversation is closed, owner rules rejected access, or the signed-in account is not the owner. Identify the current role in the shared UI without requiring a manual switch to an owner URL.
- For redirect sign-in launched inside a widget, restore the widget after
getRedirectResulton the original page. If redirect restoration fails, keep the panel visible and report the safe Firebase error code; do not send users to a separate URL to complete owner routing. - Track whether a redirect was actually launched. If its return yields neither a
getRedirectResulterror nor an authenticatedonAuthStateChangeduser, report that Firebase restored no session instead of silently showing the sign-in button again; don't invent an SDK error code for an empty result. - Keep the Firebase
web.appaddress available when a custom domain's TLS certificate is still provisioning. Once Auth is tied to the custom host, add that host's Authorized domain and OAuth callback before testing its login.
Live messaging versus push
Firestore real-time listeners support an in-app conversation while browser pages are open. Firebase Cloud Messaging is a separate push-notification system. A secure push sender must run in trusted server code; never put service-account credentials or FCM server secrets in the PWA. Treat the server trigger/Cloud Functions/Cloud Run, service account, browser notification permission, service worker push handler, billing plan, budgets, and abuse controls as a separate explicitly approved follow-on. Clearly disclose if messages are delivered only while the owner inbox is open.
Learned Firebase and deployment pitfalls
- A public recommendation must not share a Firestore document with a private submission: document-level rules cannot hide the recruiter's email or UID. Store the private queue entry separately, and publish only consented fields under a random document ID after a Google-owner approval batch succeeds.
- When a requested install banner has a self-destruct countdown, start the timer when the banner first appears and do not restart it when a later
beforeinstallpromptevent only changes the button copy. Use a visible burst of bright paper confetti and a flash/ring rather than glass shards, but close immediately whenprefers-reduced-motionis enabled. Keep the banner non-modal and in flow; if the owner wants it on every visit, do not persist dismissal between page loads. - A vertical glass section rail may replace the native scrollbar when it remains keyboard-accessible with 44px targets. Keep it persistent on desktop; on mobile, make it a full-height edge overlay that appears during scrolling and fades after roughly 1.5 seconds. Do not reserve content padding for an auto-hiding rail: compare content bounds while it is visible and idle, and disable pointer events when hidden. Give every visible section its own rail stop in page order; mapping an unlinked section to the previous item can make the active marker jump backwards when scrolling forward. Keep Contact active at the document bottom, hide the recommendations stop while its section is empty, and verify no horizontal overflow at 320px, 360px, and 390px.
- When a visitor says one tile is the correct width and the others are too narrow, measure the rendered card bounds against that reference rather than comparing only their parent sections. Desktop metric, career, skills, recommendation, or contact grids can narrow individual tiles even when the main containers match. Keep narrative panels full-width, but retain responsive two-column rows for concise metrics, skills, and education when they remain readable. Do not solve a width mismatch by making every section a single vertical list; verify the chosen rows and columns at mobile, tablet, and desktop sizes.
- When an install invitation appears broken, check eligibility and saved state before changing its design: native
beforeinstallprompt, iOS manual-install support, Android manual install, desktop Chromium's browser-menu path, per-page shown/dismissed state, the installed marker, and standalone mode all affect visibility. Test fresh and dismissed page loads, an installed context, Android and desktop Chromium with the native event blocked, and iOS instructions separately; do not clear all browser state or force a misleading install action. Prefer iOS instructions even if a desktop browser emulating an iPhone still fires a native prompt event. - If an owner says panels are too rounded, don't assume a tiny shared radius is the right design. Change one representative panel in a preview, compare it with the owner's reference, and keep or restore the original until the owner approves the corner treatment. Do not flatten all card, account, and chat surfaces with one global override when the owner prefers to tune them manually.
- A beveled-glass visual system can be rendered with CSS gradients, inset highlights/recesses, shadows, and
backdrop-filter; keep theme variables for light/dark mode and a solid fallback. Avoid bitmap noise or external 3D dependencies for a simple glass surface so edges stay crisp at high density. - For a clean lilac/plum profile, remove moving tile grids and radial spotlights rather than layering more effects. Mobile browsers may ignore or scroll
background-attachment: fixedonbody; use an isolated fixed pseudo-element behind normal-flow content (position: fixed; inset: 0; pointer-events: none) and verify it on the target phone. Lower the glass fill alpha and keep blur modest (about 6–8px) while strengthening crisp inset rim highlights and recessed/outer shadows; high opacity plus heavy blur reads frosted, not glass.
- On Windows, recent
firebase-tools/Node combinations may exit with aUV_HANDLE_CLOSINGassertion after an API operation succeeded. Verify postconditions with a safe project/database/Hosting query before retrying a potentially destructive create/delete/deploy command; don't infer failure from the assertion alone or run the mutation twice. - Deleting a Firestore default database to change region destroys its data and may reserve the
(default)name for several minutes. Select the right location before database creation; never delete/recreate merely to experiment. - Firestore rules can prevent visitors chatting while the UI still loads. Test availability document creation/reading and the atomic parent+message transaction in the Emulator before publishing. A settings
get()must read a real public status document and remain unwritable by visitors. - If the default database was accidentally created in the wrong location and is confirmed empty, explain that location is immutable, deletion destroys the database, and Firestore temporarily reserves a deleted database ID; obtain explicit owner approval before changing it. After deletion, verify the database is gone and wait for the ID reservation to expire before recreating it.
- If Auth reports
redirect_uri_mismatchafter a Google sign-in page opens, do not rotate the Firebase API key or create a service account. Compare the browser's exact origin with the Google OAuth Web client's/__/auth/handlercallback, add the missing exact callback while preserving the existing Firebasefirebaseapp.comcallback, and verify the same host is in Firebase Authentication's Authorized Domains. - Do not build a role-aware panel as a server-login redirect or add Cloud Functions/Blaze solely to route owner versus visitor. Firebase Auth state can select the UI; Firestore Rules must independently protect every read/write. Reserve a trusted server and billing for features that actually need one, such as sending FCM push while the owner is offline.
- When Firebase Auth silently returns to the signed-out UI on a phone, use the browser Console evidence. In this PWA,
script-srcblocked Google'shttps://apis.google.com/js/api.jsloader, so redirect completion restored no Firebase user. Addinghttps://apis.google.comto the deployedscript-srcfixed real sign-in on the installed phone; the visible symptom alone looked like an auth/session failure. Verify the live CSP header after deploy before asking someone to try again. - Hosting response headers (including CSP) are evaluated at the deployed origin and are not fixed by reinstalling the PWA. Update
firebase.json, deploy Hosting, check the live header on both active domains, then refresh or reopen the installed app. Use a service-worker cache-version bump for changed cached shell assets; do not routinely ask users to clear all app data. - When a replacement résumé comes from Google Docs, PDF bytes and text-layout extraction may change while the actual words remain the same. Compare normalized extracted text and inspect the new source, then copy that exact PDF to both Android and PWA assets, update every Android/deploy SHA-256 assertion, update source-date documentation, and bump the service-worker cache because the offline PDF changed. Remove machine-specific Downloads paths from profile JSON; keep the original Downloads file untouched.
- If an owner identifies duplicated portrait/brand marks or repeated résumé links, audit the rendered page, native screen, and footer together. Keep one profile portrait and one prominent website résumé download; preserve distinct native save/open/share actions, but don't duplicate a brand banner plus hero identity. Prefer a compact, typographic section index to a second logo row.
- The Firestore Emulator may report that Java is missing even when Android Studio's JDK is installed outside
PATH. On Windows, use its exactjava.exeby settingJAVA_HOMEand prependingJAVA_HOME\bintoPATHin the same PowerShell process that runsfirebase emulators:exec; keep using the isolated demo-project emulator and configured non-default port. - For GoDaddy, preserve nameservers, MX, SPF/DKIM/DMARC and unrelated records. Remove only parking A records Firebase specifically names; add Firebase's exact apex A and persistent ownership TXT. Check GoDaddy's authoritative nameserver plus public resolvers before repeatedly pressing Verify.
Learnings from the visual feedback cycle
For each meaningful owner correction, preserve the reusable loop here as feedback → observed cause → design change → measurable verification. Avoid a generic “polished the UI” note; keep the reference component and the behavior that exposed the issue.
- Mobile rail: The first disappearing rail still reduced the content width because the page reserved a right gutter even while the rail was hidden. The final design overlays a full-height rail at the edge, removes the number labels, keeps the page width unchanged, and allows smooth section jumps plus track dragging/tapping. Verification compares content bounds in both visible and idle states and checks the rail at phone widths.
- Section order: Moving MCP ahead of Education exposed an old rule that mapped Education back to Expertise, so the active marker appeared to reverse during a forward scroll. Give Education its own stop and test that the active rail sequence follows the rendered section order.
- Tile width: “Get your own app” was the owner’s reference tile. Comparing actual card boxes showed that multi-column metric, career, skills, and contact layouts were narrower than the reference despite sharing the same page width. Wide narrative panels now share its content track, while concise sections use selected two-column grids at larger widths; measure tile bounds across viewport breakpoints rather than forcing every section into one column.
- Layout variety: The owner corrected an all-vertical layout. Keep long-form career/contact panels broad, and use rows for short metric, skill, or education cards only where the columns stay comfortable to scan.
- Install invitation: A fresh eligible browser shows the delayed prompt, but the owner wants the novelty to repeat on every page load until installation. Remove session-persistent shown/dismissed flags; keep only page-local duplicate guards and the persistent installed marker. Android devices and desktop Chromium browsers that omit
beforeinstallpromptneed truthful browser-menu guidance, while unsupported desktop browsers should stay free of a misleading CTA. The initial shard-like burst was too subtle; use larger, brighter paper confetti and test native, desktop and Android fallbacks, iOS instructions, dismissal reload, self-destruct reload, and installed states separately. - Corner shape: A uniform 6px panel-radius experiment looked worse to the owner, so the prior radius values were restored for manual tuning. Treat “less rounded” as a design direction, not an exact radius value; verify a focused preview before changing every panel.
- Skill publishing: Updating this personal skill is not complete until the exact current
SKILL.mdis synced to the explicit allowlist atweb/public/skills/professional-portfolio-pwa/SKILL.md, the MCP catalog and static HTML/LLM discovery pages are regenerated withnpm run build:sitefromweb/mcp-server, and the MCP protocol suite plusnode --test tests/public-discovery.test.jsfromweb/pass. Update the service-worker precache/version and deploy Hosting when publishing changed skill content; never discover or publish other skill folders automatically. This adds a local read-only MCP resource and does not imply a remote protocol endpoint is deployed.
Install this custom skill
Review and read the raw SKILL.md or download SKILL.md. Create a folder named professional-portfolio-pwa in your agent's supported skills directory and save the file there as SKILL.md.
Project-local Copilot example: .github\skills\professional-portfolio-pwa\SKILL.md. Personal installation, only if you choose it: .agents\skills\professional-portfolio-pwa\SKILL.md under your profile. Copy only this authored file, not any downloaded skill collection. Inspect another project's code before adapting these Ryan-specific workflows.
The instruction file is usable independently of MCP. Local MCP setup is a separate option; no public MCP protocol endpoint is deployed.
Catalog name: professional-portfolio-pwa. Source: original-custom. Size: 42596 UTF-8 bytes. JSON catalog and provenance.