← Original custom skills library Ryan's portfolio

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

Workflow

1. Verify and model the profile

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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:

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.

  1. Listen for beforeinstallprompt.
  2. Call preventDefault() and retain the browser event.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
  7. After appinstalled, persist a local installed marker. Also detect display-mode: standalone and navigator.standalone, hide install controls, and set the marker when an installed PWA starts. If a new beforeinstallprompt event fires, clear a stale marker.
  8. 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:

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.

  1. The owner must sign in with their own Google account, complete MFA/passkey verification, and personally review/accept Firebase Terms when creating a project.
  2. 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.

  1. Deploy the existing static project, not an invented sample app:
   powershell -NoProfile -ExecutionPolicy Bypass -File .\Deploy.ps1 -ProjectId PROJECT_ID
  1. Verify the public HTTPS web.app homepage, current modal/stylesheet/script, mutable-asset Cache-Control headers, 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:

  1. 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.
  2. Add the exact Firebase ownership TXT and A records with host/name @.
  3. Remove only the specific parking A records Firebase explicitly says to remove.
  4. Preserve nameservers, MX records, SPF/DKIM/DMARC and every unrelated TXT record. Do not replace nameservers or guess www, AAAA, CAA, or A values.
  5. Check the authoritative nameserver and at least two public resolvers. Click Verify in Firebase after TXT/A propagation. Keep the TXT for certificate renewal.
  6. 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.app URL available. Distinguish DNS correctness, Firebase verification status, and HTTPS certificate readiness instead of repeatedly changing correct DNS.

8. Keep deployment efficient and safe

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

Moderated public recommendations

Firestore security-rule invariants

  1. Start with deny-by-default rules. Never use Firebase's permissive test mode or allow read, write: if true in a deployment.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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

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:

  1. The actual site host in Firebase Authentication → Authorized domains, and
  2. 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

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

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.

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.