Skip to content

Repository files navigation

Grand Website

Static landing-page wireframe for Grand.

Status

  • Added a first-pass one-page wireframe for the public landing page.

  • Positioning is companion-first for the older person, with family reassurance as the buyer story.

  • The wireframe uses the existing Grand product language: warm surfaces, sage/clay accents, editorial type moments, privacy by default, and no surveillance framing.

  • Added an optimized hero image from the selected Pexels option: Moe Magners photo pexels-moe-magners-5335290.jpg, resized to 2400px wide at assets/hero-pexels-moe-magners-5335290.jpg.

  • Deployed the static page with GitHub Pages from the gh-pages branch under rainforestapp/grand-website; the Deploy GitHub Pages workflow publishes main to gh-pages automatically when main changes.

  • Configured GitHub Pages for the canonical custom domain www.grandeldercare.com, with DNSimple records pointing www to rainforestapp.github.io and the apex domain to GitHub Pages A/AAAA records. GitHub is issuing a certificate for both www.grandeldercare.com and grandeldercare.com.

  • Refined the landing-page copy after review feedback, including more specific music examples, clearer reminder language, and replacing the abstract product preview with concrete Grace app ideas.

  • Reframed the landing page around the broader Grand system: Grace is the smart speaker companion, and optional Grand Satellites add home tracking.

  • Added a "How Grand works" section that explains Grace as the smart speaker and Grand Satellites as the optional home-tracking add-on.

  • Added a Grand satellites/home-tracking section focused on daily rhythm, routine changes, and filtered emergency-like alerts without cameras or live feeds.

  • Split use cases into "Ask Grace" and "Grand notices" so companion moments and home-tracking moments can sit together without blurring their roles.

  • Replaced the abstract satellite floor-plan with resized real Grand iOS app screenshots from ../grand-ios/docs/screenshots: ios-home.png and pr1-settings-index.png.

  • Simplified the hero headline and strapline so first-time readers understand the promise without needing prior context for Grand, Grace, or satellites.

  • Rewrote the hero to lead with reassurance instead of the two-part independence/worry construction. Why: "You'll know she's okay." states the outcome the buyer actually wants in one breath, and the shorter strapline ("A few discreet sensors to flag when something's off — no cameras, no wearables, nothing to charge.") drops the "small hub" mechanism detail that the "How Grand works" section already covers. Also swapped the hero CTAs so "See how Grand works" is now primary (filled) and reordered ahead of the secondary "Be first to know" outline button, because an unconvinced first-time visitor needs to understand the product before committing to the waitlist. Removed the "Care at home" hero eyebrow so nothing competes with the headline, added text-wrap: balance on #hero-title so the two-line headline doesn't orphan "okay." on its own line, and raised the hero copy's bottom padding (clamp(42px, 9vw, 140px)) so the shorter block sits optically centered on tall desktop viewports instead of pinned to the bottom edge.

  • Wired the waitlist form for a Google Sheets backend via Google Apps Script, including client-side email validation, status messaging, and browser/user-agent metadata capture.

  • Repositioned the entire page from companion-first to peace-of-mind / dignity-first, aimed squarely at the adult child (the buyer). This supersedes the companion-first framing above. Why: the product's MVP is a quiet, passive sensor system (a hub + sensors) that lets an adult child know their parent is okay without cameras, wearables, or anything that announces "you're old" — not a voice companion. The "Grace" companion concept and all companion features (music, reading aloud, trivia, conversation, family-message dictation) were removed because they describe a different product and were not in the MVP spec.

    • "Grand satellites" renamed to "Grand sensors"; the device story is now simply "the Grand hub + Grand sensors."
    • Hero now leads with the fear→relief promise. (The wording was later tightened to "You'll know she's okay." — see the hero rewrite below.)
    • Added an upfront "The worry" problem section after the hero that sets up the fear and contrasts pendants/watches, call-for-help buttons, and cameras before presenting the solution. (This replaced a later "Why not a wearable, button, or camera?" comparison block, which said the same thing twice once the problem was framed up front.)
    • Trust close ties privacy directly to dignity and drops conversation-era rules; no pricing and no public mention of human-in-the-loop alert review, per product decision. (The human-in-the-loop decision was later reversed — see the Grand Call Center section below.)
    • Reused the existing iOS screenshots (assets/grand-ios-home.png, assets/grand-ios-settings.png) — they already show the passive-sensing view with no "Grace" UI.
  • Added a "Grand Call Center" section (#response) between the caregiver experience and the waitlist. Why: this reverses the earlier "no public mention of human-in-the-loop alert review" decision — the call center is now a headline differentiator, because "what actually happens in an emergency?" is the buyer's biggest pre-purchase question and the human agent + EMS escalation is the answer. The section presents the emergency flow as a four-step process (a real person calls through the hub/sensors → confirms she's safe → calls EMS if not → the family is notified throughout and can join the call), with a call-center agent photo (assets/call-center-pexels-kampus-8204317.jpg, Pexels / Kampus Production, cropped 3:4 around the agent and optimized to ~250KB). The caregiver-experience intro and the urgent signal tile were reworded to hand off to this section instead of implying an automated-only reach-out, and an "Emergency response" nav link was added.

  • Reformatted "The worry" section from a three-card competitor teardown into a narrative "anxiety window" timeline plus a compact "you've probably already thought about…" strikethrough list. Why: user research (May–June interviews) showed the problem is emotional, not comparative — the single most vivid finding was the adult child who worries from the moment she wakes until it's socially acceptable to call at 8am, and the failed alternatives land as stories (the pendant on the nightstand during the fall, the button that's "a reminder you're old") rather than spec-sheet dismissals. The old equal-cards format asked visitors to evaluate product categories before feeling understood, and gave competitor categories the same visual weight as the worry itself. The timeline dramatizes a familiar morning (clay dots for the anxious beats, a sage dot for the relief beat), a payoff line bridges into "How Grand works," and the three alternatives survive as demoted one-line dismissals so the content wasn't lost.

  • Added baseline SEO and indexing plumbing: canonical URL, Open Graph and Twitter card tags, JSON-LD structured data (Organization + WebSite), robots.txt, sitemap.xml, and a favicon set. Why: the site previously had none of this, so Google had little to work with and link previews in iMessage/Slack/social showed no image or branding. This is intentionally the crawlability baseline, not an optimization pass. See the "SEO & Indexing" section below.

  • Enriched waitlist signups to qualify fit against the ICP without adding friction to the email capture. Why: the sheet previously held only an email, giving no way to tell whether a signup matches the target buyer (an anxious adult child of a single senior who lives alone) or where to launch first. Three changes:

    • Passive location at signup. script.js fires a best-effort client-side IP geolocation lookup (https://ipapi.co/json/) on page load and attaches geo (city/region/country/postal) to the signup payload. It is time-boxed to ~1.2s and can never block or fail a signup — if it's slow or blocked, the row just has no location. This is on top of the coarse timezone already captured.
    • Post-signup profile page (welcome.html). The email stays the only required field; on success the homepage stores the email in sessionStorage (not the URL, to avoid leaking it into referrer/pixel traffic) and redirects to welcome.html, which asks five optional questions — full name, ZIP code, why they're interested, whether the person Grand is for lives alone, and alpha-tester interest. This is progressive profiling: motivated signers answer, hesitant ones still convert. The page is noindex.
    • Real conversion events. Both pixels previously fired only PageView/PageVisit, so ad platforms couldn't see signups. script.js now fires fbq('track','Lead') + rdt('track','SignUp') on signup, and fbq('track','CompleteRegistration') + rdt('track','Lead') on profile completion.
  • Added Privacy Policy (privacy.html) and Terms of Service (terms.html) pages and linked both from the footer of index.html, welcome.html, and each other. Why: the site runs Meta and Reddit ad pixels and collects waitlist emails plus optional profile data, so it needs a published privacy policy and terms — and the ad platforms require a linked privacy policy for pixel/conversion use. Both pages are boilerplate written from what we know about Grand (pre-launch home-sensor product, waitlist only, US-based call center is not yet live), using a new shared .legal-* layout in styles.css (readable 760px prose column, serif section headings, clay links) that matches the site's warm surfaces and editorial type. The privacy policy explicitly discloses that a waitlist sign-up is recorded as a conversion event and shared back to advertising partners (Meta, Reddit) so campaigns can be measured — the specific fact the team wanted stated. The legal pages deliberately omit the Meta/Reddit pixel snippets (no need to fire ad tracking on the privacy/terms pages themselves) and are indexable (added to sitemap.xml). One placeholder to confirm with counsel: governing law is set to Delaware. All contact addresses site-wide use hello@grandeldercare.com — a mailto: typo (grandelderare.com, missing a "c") in the index.html and welcome.html footers was corrected as part of this change.

  • Rebuilt the site footer: a top row with a "Contact us" label + hello@grandeldercare.com mailto: link on the left and the four nav links as a single right-aligned column, above a bottom bar with the grand. wordmark logo (assets/grand-logo.png) on the bottom-left and the copyright on the bottom-right (no divider — the footer reads as one section). Why: earlier footer iterations laid the nav links out as a run-on horizontal line, then as labelled columns that still read as cluttered. This is the simplified layout the team asked for. The "Grand is not a replacement for 911 or professional medical care" disclaimer was dropped per request, and the earlier "Quiet home monitoring…" tagline was removed from the footer (it wasn't requested). The logo is the brand wordmark trimmed and made transparent (cream background removed) so it blends on the footer surface.

  • Added "Grace" product-experiment subpages at /gracecompanion and /gracephone as lean, de-branded scaffolds, with /grace now available as a clone of /gracecompanion. Why: we want to test separate product lines under the Grand umbrella without touching the main Grand page or linking the sites together. See the "Grace Product Subpages" section below. The main Grand page (index.html) and all its supporting files were left completely untouched.

  • Consolidated the Grace experiment to a single /grace page. Deleted the gracecompanion/ and gracephone/ folders, leaving /grace as the only Grace site. Why: we no longer need three parallel product-line experiments — one Grace page is enough to maintain and iterate on. This supersedes the "three independent sites" model above. On the next deploy the rsync --delete step drops the live /gracecompanion and /gracephone URLs automatically; nothing else referenced them (no cross-nav, not in sitemap.xml), and the root Grand site is untouched. /grace keeps its existing product = "gracecompanion" waitlist tag so signups continue flowing to the GraceCompanion sheet tabs (data continuity; the label is invisible to users).

  • Standardized the /grace "What Grace does" section to list format. Converted the two flowing-paragraph blocks ("Grace keeps her company", "She brings people closer") to the same .does-list bold-lead-in style as the middle block, and removed the leftover [TK - confirm in v1] marker. Why: the section mixed prose and list formatting; the founder found the scannable bold-lead-in list easier to read, so all three blocks now match. HTML-only change — .does-list was already defined in grace/styles.css.

  • Switched the Grand alpha signup from email to phone number, on its own sheet tabs. The "Become a tester" field on index.html now collects a phone number (type="tel", 10–15 digit validation) instead of an email, and the whole Grand site is tagged product = "grandphone" so signups, profile answers, and analytics events route to two new tabs — Grand phone number alpha list and Grand phone number events — via the existing PRODUCT_SHEETS mechanism. Why: the team wants to reach alpha testers by phone for onboarding calls, and a clean break onto new tabs keeps phone numbers out of the historical email column and freezes the email-era Waitlist/Events tabs as an archive. waitlist.gs gained a phone column (appended last, so existing sheets auto-migrate with a blank column) and now identifies people by phone (digits-only match) for phone products, keeping the email contract for everything else. Deploy dependency: the Apps Script must be redeployed (Deploy → Manage deployments → New version) for phone signups to be accepted — the currently deployed version validates email server-side and would reject them. Ship the frontend and the redeploy together.

  • Reworked the post-submit screen on welcome.html into an alpha-onboarding CTA. Moved the alpha-tester Calendly link out of the profile form onto the confirmation screen, then dropped the "Thank you."/"we'll be in touch" copy in favor of a single focused ask: heading "Fast track getting set up as a test user of Grand", a short explainer, and a primary "Schedule a 20-minute call with us" button (Calendly). Removed the "Back to home" button. Why: once someone finishes the profile, booking the onboarding call is the only next step worth surfacing — a confirmation-and-dead-end screen wasted the highest-intent moment.

  • Fixed phone numbers landing in the sheet as #ERROR!. Phone values start with + (e.g. +1 (781) 492-4290), and Google Sheets parses any cell beginning with +, =, -, or @ as a formula, so the phone column evaluated to #ERROR!. waitlist.gs already prefixed the value with a text-forcing apostrophe (plainTextPhone_); this adds a second guard, ensurePhoneColumnIsText_, which sets the phone column's number format to plain text (@) before every write so a raw value can never be parsed as a formula. Why belt-and-suspenders: the format guard is independent of the apostrophe and protects any future write path. Deploy dependency: this only takes effect once the Apps Script is redeployed (Deploy → Manage deployments → New version) — the error rows in the live sheet were written by the pre-fix deployment. Existing #ERROR! cells must be corrected by hand; the original number is still recoverable from that row's raw_payload JSON ("phone":"…").

  • Gated the "Schedule 20-minute call" link behind a fit qualifier on welcome.html. The profile form gained two questions — the caregiver's phone type (iPhone / Android / Something else) and "Does your loved one have any pets?" (Yes / No) — and, together with the existing "does the person live alone?" question, these three now qualify the visitor. On submit, only a caregiver who uses an iPhone, whose loved one lives alone, and has no pets sees the Calendly scheduling panel; everyone else lands on a new Grace-style "You're on the list." panel (no call link). Every submission's answers are still saved either way, and script.js tags the waitlist_profile_submit_success / profile_completed analytics events with qualified so the split is visible. Why: too many low-fit people were booking onboarding calls — the alpha needs iPhone users (the companion app is iOS-only), single seniors living alone (the core ICP), and pet-free homes (pets confound the passive motion sensing), so the call slot should be reserved for candidates who clear all three. Before/After: Before — every profile submitter saw "Schedule 20-minute call". After — only all-three qualifiers see it; the rest get a graceful waitlist dead-end. waitlist.gs gained phone_type and has_pets columns (appended last, so existing sheets auto-migrate with blank columns). Deploy dependency: redeploy the Apps Script (Deploy → Manage deployments → New version) so the two new profile answers are written to the sheet — the site otherwise works, the columns just stay blank until redeploy.

    • Made every profile field except "Why are you interested?" required, and moved that free-text question to the end. Full name, email, ZIP, phone type, lives-alone, and pets must all be answered before the form submits (blank → "Please complete all required fields." and focus jumps to the first empty field); email also gets a format check ("Please enter a valid email address."). The open-ended "Why are you interested in Grand?" is now the last field and the only optional one. Each required label/legend carries a clay * (with a "* Required" note at the top of the form); the optional field keeps its "Optional" hint. Why: the qualifier and follow-up fields are the ones we act on, so we want them complete; the essay question is nice-to-have and belongs after the quick taps so it never blocks a motivated signup.
    • Pets lists "No" first, with no option pre-selected. Why: most target households are pet-free, so No leads; leaving it unselected keeps pets a genuine required choice (a pre-checked default would auto-pass the qualifier) — consistent with the other required questions.
    • Tightened the welcome.html layout so the now-longer form sits higher on the page. Cut the .profile-section top/bottom padding (clamp(56px,8vw,104px)clamp(32px,4vw,56px)), split the .profile-layout grid gap into a smaller row-gap (clamp(16px,2vw,24px)) so the heading no longer floats far above the content, and trimmed the .profile-main gap to 16px. Also moved the "* Required" note out of the form and dropped its margin-bottom — inside the form it added its own margin on top of the 24px field gap, leaving a ~44px hole above the first field. Now the description, "* Required", and the first field sit at an even 16px. Why: adding the required questions made the form tall enough to risk pushing the first fields below the fold; pulling the whole block up and closing the intro gap keeps the top of the form visible on a typical laptop viewport without cramping it.
  • Replaced the "Grand call center" content in the #response section with a caregiver-decides emergency flow. The section keeps its exact layout (photo-left/copy-right two-column grid, eyebrow → <h2> → intro → auto-numbered .response-steps), but the copy no longer describes a U.S.-based human agent who calls the parent and escalates to EMS. New eyebrow "When something's wrong", headline "You decide what happens next.", and three steps: (1) Grand pings your caregiver circle on a fall or routine change, (2) you talk to her directly through the hub and sensors, (3) you decide if emergency services are needed — if she needs help, Grand surfaces the right local emergency number for her area. This dropped the step count from four to three; the CSS counter renumbers automatically. Why: the emergency model changed — the family, not a Grand call center, is in the loop and makes the call, so the copy now puts the caregiver in control and reflects that Grand alerts the circle rather than dialing EMS itself. This supersedes the earlier "Grand Call Center" / human-in-the-loop framing described above. Before/After: Before — "The Grand call center · If the worst does happen, we're there with her.", four steps ending in a Grand agent calling EMS. After — "When something's wrong · You decide what happens next.", three steps ending in the caregiver placing the call with Grand's help. The #response id and "Emergency response" nav label are unchanged.

    • Swapped the section photo from the call-center agents (call-center-pexels-kampus-8204317*.jpg, now deleted) to a caregiver at home checking her phone — assets/caregiver-phone-pexels-kaboompics-6135183.jpg (Pexels / Kaboompics, photo #6135183). Why: the old image showed a call center, which the new copy no longer describes; a caregiver looking at her phone matches the "Grand pings your circle / you decide" flow. Same responsive setup as before: desktop 3:4 crop (1200×1600, ~146KB) framed on the subject, with <picture> mobile sources at ≤560px — a tighter 4:3 crop at -mobile.jpg (340w) and -mobile-2x.jpg (680w). The alt text was updated to match.
  • Refreshed the homepage share preview (thumbnail + description). The card previously used a tight portrait crop of a stock headshot and a description built around the old “no cameras, nothing to charge” framing. Why: the crop showed only a face — nothing about the product — so a shared link communicated “some elder-care thing” and nothing more. The new image is a wider shot of the same woman in her living room with a Grand sensor plugged into the wall outlet behind her, so the preview shows the product in situ, and the copy now leads with detection (“both acute events like falls and subtle changes in her routine”) rather than only what Grand isn't.

    • Published at a new filename (og-image-v2.jpg) instead of overwriting og-image.jpg. Why: Facebook, LinkedIn, Slack, and iMessage cache share images by URL, often for weeks. Reusing the old path would have kept serving the old thumbnail to every platform that had already scraped the site; a new URL forces a fresh fetch. It also leaves the Grace subpage's existing card untouched, since it still points at the old file.
    • Then aligned <title> and <meta name="description"> to the same copy. Why: leaving them on the old wording meant the search snippet and the share card described the product two different ways. <title> keeps the established Grand · … brand prefix (38 chars, inside Google's ~60 limit) rather than matching og:title exactly — og:site_name already renders “Grand” as a separate line on the share card, but a search result has no such affordance, so dropping the brand there would cost brand-query recognition for nothing.
    • The search description is a deliberately shorter variant of the share description (147 vs 178 chars). Why: Google truncates at ~155–160, and the full sentence got cut mid-word at “No c…”, losing “No cameras, no wearables.” — the sharpest differentiator in the line. Dropping the “so you can be sure she’s safe” clause fits the whole thought in the snippet. The share card keeps the longer version because those platforms allow ~300 chars and the reassurance clause earns its space there.
    • The Organization JSON-LD description was updated to the short variant too, replacing the old “quiet home sensor system” sentence. It feeds brand/knowledge-panel surfaces where the same length pressure applies. Note this string lives inside a <script> tag, so it uses a literal — HTML entities like &rsquo; are not decoded there and would render as raw text.
    • Three description strings now exist by design: the long one on og:description (share cards), and the short one shared by <meta name="description"> and the JSON-LD (search surfaces). The visible hero and “The worry” copy still use the older “nothing to charge” framing — out of scope for this pass.
  • Replaced the share thumbnail again with a branded, headlined version (assets/og-image-v3.jpg). Same photograph and framing as og-image-v2.jpg, but the delivered art now has the grand · wordmark and the headline “You'll know she's okay” composited into the blank wall on the left. Why: the v2 card carried no brand mark and no words of its own — in a feed or a group chat it had to borrow all of its meaning from the og:title line underneath, which several surfaces truncate or de-emphasise. Baking the promise into the pixels means the card says what Grand does even when the text around it is clipped. Before/After: Before — photo only, wall empty on the left. After — same photo with wordmark + two-line headline in that empty space. og:image:alt was rewritten to describe the text as well as the scene, since the alt string is what screen readers get instead of the baked-in words.

    • New filename again (-v3) rather than overwriting -v2. Why: the same URL-keyed caching that motivated the v1→v2 rename applies here; og-image-v2.jpg has been live and scraped since the previous pass.
    • Kept og:title as “Know your aging parent is okay” rather than matching the image headline verbatim. Why: the title also has to work as a standalone line on cards that don't render the image at all, where “You'll know she's okay” has no subject. The two read as a pair when both show.
  • Softened the waitlist launch-date copy from a fixed season to a relative timeframe. The .waitlist-note above the phone sign-up on index.html now reads “Grand will officially launch to the public later this year, but we are accepting prototype testers today (completely free). Sign up to learn more.” Why: the public launch date is not firm, and “Fall 2026” had to be re-edited every time it moved — a relative timeframe stops the page from advertising a specific season we may miss, while keeping the prototype-tester ask (the only thing the section actually converts on) unchanged. Before/After: Before — “Grand will officially launch to the public Fall 2026, but we are…”. After — “Grand will officially launch to the public later this year, but we are…”. Copy-only change; the eyebrow, headline, form, and validation are untouched. Known trade-off: “later this year” silently expires on 2026-12-31 — unlike “Fall 2026” it will read as current while being false, and this repo has no build step, CMS, or test that could flag it, so it needs a manual calendar reminder to revisit.

SEO & Indexing

All URLs are canonicalized to https://www.grandeldercare.com/ (the www host, matching CNAME).

  • index.html <head> carries the canonical link, theme-color, Open Graph tags, a twitter:card tag, and JSON-LD (Organization + WebSite). Only twitter:card is set for X/Twitter — scrapers fall back to the og:* tags for title/description/image, so there's one canonical copy of each string instead of hand-synced duplicates.
  • The deploy workflow excludes README.md and google-apps-script/ from the published site. Why: GitHub Pages was serving the whole repo, so internal strategy notes and the waitlist backend source (including the spreadsheet ID) were live URLs — and robots.txt + the sitemap would have invited crawlers to index them.
  • The social share image is assets/og-image-v3.jpg, 1200×630 (the standard large-card size for iMessage, Slack, and social previews). It is a wide shot of a Grand user at home with a sensor visible in the wall outlet, with the grand · wordmark and the headline “You'll know she's okay” set into the empty wall space on the left. Trimmed from 16:9 to 1.91:1 by cropping height only (18px off the top, 45px off the bottom at the 1672px source width) so her face, the sensor, and the headline all clear the frame. Superseded: assets/og-image-v2.jpg (no longer referenced) and assets/og-image.jpg (still referenced by the Grace subpage).
  • Known limitation: the baked-in text does not survive a centred square crop. Because og:image:width/height are declared at 1200×630, the surfaces that matter for a shared link render the full 1.91:1 card — iMessage, X (summary_large_image), Facebook, LinkedIn, Discord, and Slack (Slack only falls back to its small square thumbnail when an image is small or its dimensions are undeclared). WhatsApp's compact preview is the real exception, and it is inconsistent about when it applies one. Measured against the centred 1:1 safe band (x 285–915 of the 1200px canvas): the grand · wordmark sits at x 121–260 and survives 0%; the headline spans x 118–694 and survives 51%, i.e. it cuts mid-word, which reads as a broken render rather than as a plain photo. This is structural, not a crop-tuning problem — the subject occupies the right half of the frame and the square-safe zone is the middle 630px, so the 576px-wide text block would have to shift 165px right onto her face to survive. The trade was taken deliberately: the large-card surfaces are where shared links actually land.
  • Safe-area rule for future share cards. Keep the wordmark and any words inside the centre 630×630 of the 1200×630 canvas (x 285–915). Anything outside that band is large-card-only by definition. If a future card needs its text to survive every surface, the photo has to be composed for it — subject further right and smaller, leaving centre wall free — which costs sensor visibility, so decide it at art-brief time rather than at review time.
  • robots.txt allows all crawlers and points at the sitemap.
  • sitemap.xml lists the public pages (/, /privacy.html, /terms.html); bump a page's <lastmod> when its content changes meaningfully, and add entries as the site grows. welcome.html is deliberately excluded because it's noindex.
  • Favicons: favicon.svg is the source of truth (serif "g" on the charcoal --surface-charcoal rounded square); favicon.ico (32px) is the legacy fallback that crawlers request blindly, and apple-touch-icon.png (180px, square-cornered because iOS applies its own mask) covers iOS home-screen bookmarks.
  • Google Search Console: the site is verified via the google-site-verification meta tag in index.html (URL-prefix property for https://www.grandeldercare.com/). Don't remove that tag — verification lapses without it. After content changes, the sitemap doesn't need resubmitting; Google re-crawls on its own.

Ad Pixels

  • Meta Pixel is installed in index.html and welcome.html and tracks the standard PageView event on load.
  • Reddit Pixel is installed in index.html and welcome.html with pixel ID a2_jb07ge9fad9n and tracks the standard PageVisit event on load.
  • Conversion events fire from script.js (not the pixel snippets): a waitlist signup fires fbq('track','Lead') + rdt('track','SignUp'), and completing the profile page fires fbq('track','CompleteRegistration') + rdt('track','Lead'). All track calls are guarded, so a blocked or absent pixel never throws.

PostHog Website Analytics

The site sends privacy-conscious website analytics to the same PostHog Cloud US project as the Grand iOS app, with a strict namespace boundary. posthog.js loads the US ingestion endpoint in cookieless_mode: "always", limits person profiles to identified alpha candidates, disables general autocapture, exception capture, feature-flag requests, and session recording, then manually captures pageviews. Web-vitals capture is explicitly enabled and emits PostHog's $web_vitals events independently of general autocapture. Every website event carries platform = "web" and analytics_surface = "marketing_website"; custom events also use a website_ prefix so they cannot be confused with the iOS taxonomy.

The deliberate website events are website_cta_clicked, website_contact_clicked, website_waitlist_started, website_waitlist_signup, website_waitlist_submission_failed, website_profile_completed, website_profile_submission_failed, and website_onboarding_call_clicked. Event properties describe only the page/experience, CTA location/label, form type, or fixed failure reason. At a successful waitlist submission the browser generates a random UUID candidate_id, stores it only for that browser tab's session, calls PostHog identify(), and sends the same ID to the Google Sheet. This makes the candidate's PostHog journey findable from their Sheet row while keeping phone numbers, email addresses, names, ZIP codes, and free-text answers out of PostHog. Earlier anonymous PostHog events from the same session are linked to the identified candidate; the ID deliberately does not persist across a later browser session or another device.

PostHog project dependency: in the shared PostHog project's Project settings → Web analytics, enable Cookieless server hash mode before deploying this integration. Web vitals are enabled explicitly in the JavaScript SDK configuration, so they work while the general autocapture and remote feature-flag requests remain disabled.

Open Locally

Open index.html in a browser. No build step is required.

Production

Production is served by GitHub Pages at:

https://www.grandeldercare.com/

Because this is a static site with no build step, the Deploy GitHub Pages workflow copies main to the gh-pages branch on every push to main. The CNAME file keeps the custom domain attached to the Pages deployment.

Waitlist Backend

The waitlist form posts to a Google Apps Script web app and appends rows to a Google Sheet. The current configured Sheet is Grand Waitlist: https://docs.google.com/spreadsheets/d/1i2_lUmRSIVA1iN3zaE8mLrR-8QEpOUPKtM8Y6aHfw1w/edit

For a fresh setup:

  1. Create a Google Sheet for the waitlist.
  2. In the sheet, open Extensions -> Apps Script.
  3. Paste the contents of google-apps-script/waitlist.gs.
  4. Optional but recommended: paste the Google Sheet ID into SPREADSHEET_ID at the top of the Apps Script file.
  5. Deploy as a Web app.
  6. Set "Execute as" to yourself and "Who has access" to anyone.
  7. Copy the /exec web app URL.
  8. Paste that URL into index.html on the waitlist form's data-waitlist-endpoint attribute.

When updating the Apps Script code, use Deploy -> Manage deployments -> Edit -> New version. Saving the code alone does not update the deployed web app. Visiting the /exec URL directly should return JSON with spreadsheet_url, waitlist_last_row, and event_last_row; this confirms which spreadsheet the script is writing to.

The client sends the tester's phone number (the Grand site is tagged product = "grandphone", so signups land in the Grand phone number alpha list tab — see the product-routing note below), the random non-personal candidate_id shared with PostHog, plus source, page URL, referrer, user agent, user-agent client hints where available, language, timezone, viewport, screen, connection hints, a coarse IP-derived geo object when the lookup has already completed (city/region/country/postal, best-effort), and other browser metadata. The initial phone capture uses sendBeacon with a keepalive fetch fallback so the visitor can advance immediately instead of waiting for the Google Apps Script round trip. The request uses a simple text/plain POST because Google Apps Script web apps are easiest to call from a static GitHub Pages site without a CORS preflight.

Privacy note: the IP geolocation lookup sends the visitor's IP to a third party (ipapi.co) and we store their coarse location. If the site gains a privacy policy, it should disclose this. The free ipapi.co tier is ~1,000 lookups/day, which is ample at current volume — revisit (or add an API key) if traffic grows.

Profile fields and the profile page

After queueing the signup, index.html stores the phone number in sessionStorage under grand_signup_phone and redirects to welcome.html, a noindex page that collects five optional qualifying fields: full_name, email, zipcode, reason_interested, and lives_alone (yes/no/not_sure). It POSTs a { type: "waitlist_profile", product: "grandphone", phone, ... } payload to the same endpoint. phone stays the identity used to match the row; the optional email, if given, is written into the row's existing email column (only when non-empty, so an empty submission never clears it). The alpha tester question is no longer a yes/no field — instead, the confirmation screen shown after the profile form is submitted (headed "Fast track getting set up as a test user of Grand") invites the tester to book an onboarding call via a primary "Schedule a 20-minute call with us" button linking to Calendly (https://calendly.com/d/dz47-vkm-rb2/grand-early-tester-program), so no alpha_tester value is collected from the form anymore. The backend still tolerates the field for older submissions.

handleWaitlistProfile_ in waitlist.gs looks up the person's existing row by phone (digits-only match so formatting differences don't matter, most-recent match wins) and updates that row in place — one row per person, no duplicates. It matches by phone for phone-based products and by email otherwise. Because the initial signup is queued optimistically, the profile handler briefly retries the lookup before appending a standalone profile row. If the identifier is missing or still unmatched, it appends a standalone profile row so the answers aren't lost. The profile form itself submits fire-and-forget (sendBeacon with a keepalive fetch fallback), so the confirmation screen appears instantly instead of blocking on this round trip (the script lock plus the retry loop can take a second or more). This mirrors the initial signup capture; the fallback-row behavior means nothing is lost even if the beacon is dropped.

ensureHeaders_ now auto-migrates the live sheet: because new columns are only ever appended to the end of HEADERS (geo_*, then the profile fields, phone, and candidate_id), it rewrites the header row in place when the sheet has fewer columns than HEADERS, so no manual column setup is needed after deploying a new version.

The same Apps Script endpoint also receives anonymous interaction analytics. For the Grand site these route to the Grand phone number events tab (via the grandphone product tag); the legacy Events tab is the email-era archive. The site records section views, link/button clicks, and waitlist funnel events (waitlist_phone_focus, waitlist_submit_attempt, waitlist_submit_success, waitlist_submit_error, and the profile-page equivalents waitlist_profile_submit_attempt, waitlist_profile_submit_success, waitlist_profile_submit_error). These events use a per-browser-tab session_id stored in sessionStorage; they do not include the tester's phone number.

Current Sections

Every content section leads with a standardized eyebrow (uppercase, 12px, clay --status-clay) above its title. The hero itself has no eyebrow — the headline leads directly. The .eyebrow is excluded from the > p intro-paragraph rules so it always renders at the base 12px.

  • Hero promise ("You'll know she's okay.") with a primary "See how Grand works" CTA and a secondary "Be first to know" waitlist CTA.
  • "The worry" problem section: a two-column narrative with the independence/worry copy on the left and an iMessage-style multi-day concern graphic on the right, followed by a compact strikethrough list dismissing pendants/watches, call-for-help buttons, in-home carers, and cameras with one-line stories.
  • "How Grand works" section titled as such, with a combined lead ("There's a better way to know they're okay. A small hub and a few sensors. No cameras, nothing to wear, nothing to charge.") and two product cards, each showing a real product photo: assets/grand-sensor.jpg (sensor in a wall outlet) and assets/grand-hub.jpg (hub on a kitchen counter), both optimized to ~120–210KB JPGs. The .card-media slot renders a cover-fit image via :has(img), falling back to a dashed placeholder when no image is present.
  • "What Grand pays attention to": everyday activity, the kitchen (meals), and a call for help.
  • "Caregiver experience": the daily "she's okay" app view with real iOS app screens, plus a parent-perspective dignity note.
  • "The Grand call center" (#response): what happens in an emergency — a mirrored two-column section (photo left, copy right) with a four-step numbered process (real person calls through the hub/sensors, confirms she's safe, calls EMS if not, family stays notified and can join the call). Step numerals are bare clay Georgia counters via CSS counter(); the photo slot falls back to the standard dashed placeholder if the image is removed.
  • Waitlist form with validation and Google Sheets handoff. On success it redirects to the post-signup profile page.
  • Post-signup profile page (welcome.html, noindex): optional full name, ZIP, reason for interest, whether the person Grand is for lives alone, and alpha-tester interest, styled with the shared styles.css .profile-* rules.
  • Site footer: a top row ("Contact us" label + hello@grandeldercare.com on the left, nav links — including Privacy Policy and Terms of Service — as a right-aligned single column) above a bottom bar with the grand. logo bottom-left and the copyright bottom-right.

Grace Product Subpage

/grace is an independent product-experiment site for the Grace companion — a voice companion that sits on a parent's kitchen counter. It began as a de-branded duplicate of the main Grand page and is now the only Grace page (the earlier /gracecompanion and /gracephone experiments were removed; see the consolidation note in Status).

  • Independent, no cross-links. There are deliberately no navigational links between Grand and /grace — the only way to reach it is to type/visit its URL. It is also left out of sitemap.xml so it isn't publicly discoverable via SEO.
  • Self-contained folder (isolation over DRY). grace/ has its own index.html, welcome.html, and own copies of styles.css and script.js, so editing it can never affect the Grand site. The tradeoff: style changes made to the root site must be re-applied here by hand. Shared images use root-absolute paths (/assets/...) so they resolve from the subfolder. GitHub Pages serves /grace from its in-folder index.html automatically; the deploy workflow needs no config.
  • Page structure. Hero, The worry (#worry, centered serif + three real interview quotes, grounded by assets/worry-alone.jpg in a .worry-lead two-column grid that collapses to one column ≤900px), What Grace does (#what-grace-does), How Grace works (#how-grace-works), Grace helps her reach out (#reach-out), Caregiver experience (#caregiver), What Grace isn't (#what-grace-isnt), and the Early access waitlist (#waitlist). CTA/profiling copy is tester-focused ("Become a tester", an early Grace tester note, "ZIP code of your loved one").
  • "What Grace does" is list-formatted. All three blocks use the .does-list bold-lead-in style (see the Status note) so the section scans consistently.
  • Post-signup flow. After a waitlist signup /grace redirects to its in-folder welcome.html profiling page (same five optional fields as Grand, rebranded to Grace).
  • Ad pixels. The Meta/Reddit pixels reuse Grand's pixel IDs; Grace conversions can be split from Grand's by filtering on URL in Ads Manager. The footer links to the shared root /privacy.html and /terms.html (no Grace-specific legal pages).
  • Known a11y note: the terracotta eyebrow labels (#b85f4a) sit at ~3.9:1 on cream and ~3.4:1 on the stone band, below WCAG AA 4.5:1; left unchanged pending a brand-color decision (body text and dark pill buttons pass).

Grace waitlist storage (shared endpoint)

/grace POSTs to the same Apps Script endpoint as the Grand site. Its signups carry product = "gracecompanion" (set in grace/script.js, and also on the profile and analytics payloads plus product-scoped sessionStorage keys), so waitlist.gs routes them to the dedicated GraceCompanion Waitlist/GraceCompanion Events tabs in the shared spreadsheet — kept as-is after the consolidation for data continuity.

  • PRODUCT_SHEETS + sheetNamesForProduct_() map each product to its own tabs: gracecompanion → the GraceCompanion tabs, and grandphone → the Grand phone number alpha list/Grand phone number events tabs (the Grand site's current tag). Payloads with no/unknown product still fall through to the legacy Waitlist/Events tabs, which now hold only the historical email-era data. (The gracephone mapping remains in waitlist.gs but is unused.)
  • New tabs are auto-created by getSheet_ + ensureHeaders_ on first write.

Deploy step (required for routing to take effect): after changing waitlist.gs, re-deploy the existing web app via Deploy → Manage deployments → Edit → New version so the endpoint URL stays identical.

Legal Pages

privacy.html and terms.html are standalone pages sharing the site header, footer, and stylesheet. They use the .legal-* rules in styles.css: a centered 760px reading column, an .legal-header block (eyebrow, title, "Last updated" line, intro paragraph), and .legal-body prose with serif Georgia section headings, --ink-soft body text, clay underlined links, and a .legal-contact call-out card at the end.

  • Privacy Policy covers what's collected (waitlist email; optional full-name/ZIP/reason/lives-alone/alpha-tester answers; automatic technical data; cookies/pixels), how it's used and shared, retention, security, children's privacy, and GDPR/CCPA-style choices. Its "Advertising and conversion tracking" section states plainly that joining the waitlist is treated as a conversion event and shared with advertising partners (Meta, Reddit) to measure campaigns — the fact the team asked to disclose.
  • Terms of Service is pre-launch boilerplate: it makes clear the product/service is not yet available and the waitlist is not a purchase or a guarantee, plus eligibility, acceptable use, IP, an explicit "not an emergency/medical service" clause, disclaimers, limitation of liability, and Delaware governing law.
  • Unlike index.html/welcome.html, the legal pages do not include the Meta/Reddit pixel snippets. They do include cookieless PostHog pageview analytics so overall site navigation is complete. They are indexable (canonical tags set, listed in sitemap.xml).
  • Both use the correctly-spelled contact address hello@grandeldercare.com. Confirm the governing-law jurisdiction (Delaware placeholder) and have counsel review before relying on these.

Releases

Packages

Used by

Contributors

Languages