Public pages¶
The public pages are marketing/landing pages served at quill-medical.com. They are built separately from the clinical React SPA and deployed to a GCS bucket behind the Global HTTPS Load Balancer, allowing content updates without clinical release gates.
Architecture¶
frontend/
├── public_pages/ ← Separate Vite workspace
│ ├── package.json ← "public-pages" workspace (no runtime deps)
│ ├── vite.config.ts ← Multi-page Vite build
│ ├── tsconfig.json ← Extends parent tsconfig
│ ├── templates/
│ │ └── page.html ← HTML shell template
│ ├── scripts/
│ │ └── generate-pages.cjs ← Generates HTML from TSX
│ └── src/pages/
│ ├── about.tsx
│ ├── careers.tsx
│ ├── clinical-messaging.tsx
│ ├── clinical-teaching.tsx
│ ├── company-information.tsx
│ ├── competency-access.tsx
│ ├── contact.tsx
│ ├── cookie-policy.tsx
│ ├── external-referrals.tsx
│ ├── features.tsx
│ ├── index.tsx ← Home page
│ ├── modular-deployment.tsx
│ ├── not-found.tsx ← 404 page
│ ├── pricing.tsx
│ ├── privacy-policy.tsx
│ ├── storybook-test.tsx ← Component demo
│ ├── structured-records.tsx
│ └── terms-of-service.tsx
├── src/
│ ├── theme.ts ← Shared Mantine theme
│ └── components/
│ └── public-layout/ ← PublicLayout wrapper
└── public/ ← Shared static assets (logo, favicon)
How it differs from the clinical app¶
| Aspect | Clinical app (frontend/src/) |
Public pages (frontend/public_pages/) |
|---|---|---|
| Routing | React Router SPA | Multi-page — each page is a separate HTML file |
| Deployment | Cloud Run container via Caddy | GCS bucket with CDN |
| Authentication | Required (JWT cookies) | None |
| Release process | DCB0129 clinical sign-off | Update anytime |
| Domain | staging.quill-medical.com |
quill-medical.com |
Shared code¶
Public pages import components and the Mantine theme from the parent workspace via path aliases (@/components/..., @/theme). To avoid dual-context errors where React or Mantine resolves from a separate node_modules, the Vite config forces shared packages to resolve from the parent:
// vite.config.ts — resolve aliases
resolve: {
alias: {
"@": path.resolve(__dirname, "../src"),
react: path.resolve(__dirname, "../node_modules/react"),
"react-dom": path.resolve(__dirname, "../node_modules/react-dom"),
"@mantine/core": path.resolve(__dirname, "../node_modules/@mantine/core"),
"@mantine/hooks": path.resolve(__dirname, "../node_modules/@mantine/hooks"),
},
}
The public-pages workspace has no runtime dependencies in its package.json — only dev dependencies (Vite, chokidar, etc.). This prevents Yarn from installing duplicate copies of React/Mantine.
Page generation (pages:gen)¶
Each TSX file in src/pages/ is a standalone React entry point, not a route. The pages:gen script (scripts/generate-pages.cjs) auto-generates an HTML shell for each:
- Scans
src/pages/for.tsxfiles - Reads
templates/page.htmlas a template - Generates a title from the filename (e.g.
features→ "Features",not-found→ "Not Found") - Writes an HTML file with the correct
<script>entry point
When it runs:
yarn workspace public-pages dev— runs once, then watches for new/renamed TSX filesyarn workspace public-pages build— runs once before Vite build
Example: Creating src/pages/pricing.tsx automatically generates pricing.html with:
<title>Pricing</title>
<script type="module" src="/src/pages/pricing.tsx"></script>
You must also add the new page to rollupOptions.input in vite.config.ts for production builds.
Adding a new page¶
- Create
frontend/public_pages/src/pages/my-page.tsx:
import PublicLayout from "@/components/layouts/PublicLayout";
import { theme } from "@/theme";
import { Container, MantineProvider, Title } from "@mantine/core";
import "@mantine/core/styles.css";
import { createRoot } from "react-dom/client";
createRoot(document.getElementById("root")!).render(
<MantineProvider theme={theme} defaultColorScheme="light">
<PublicLayout>
<Container size="lg" py="xl">
<Title order={1}>My page</Title>
</Container>
</PublicLayout>
</MantineProvider>,
);
- Add the entry to
vite.config.ts→build.rollupOptions.input:
input: {
// ... existing entries
"my-page": path.resolve(__dirname, "my-page.html"),
},
- Run
just pup— the HTML file is generated automatically and the dev server opens.
PublicLayout¶
All public pages use PublicLayout from @/components/layouts/PublicLayout.tsx. It provides:
- Header —
PublicTopRibbonnavigation with logo and links - Main content area (flex-grows to fill viewport)
- Footer —
PublicFootercomponent
Props:
| Prop | Type | Default | Description |
|---|---|---|---|
children |
ReactNode |
required | Page content |
Local development¶
just pup # Start dev server (alias for public-pages)
This runs yarn workspace public-pages dev, which:
- Generates HTML files from TSX pages
- Starts a chokidar watcher for new/renamed TSX files
- Opens Vite dev server at
http://localhost:5173
Navigation between pages uses full page loads (anchor tags), not client-side routing.
Dev-mode 404 handling¶
The Vite config includes a vite:mpa-clean-urls plugin that provides clean URL rewriting (e.g. /about → /about.html) and serves not-found.html with a 404 status code for non-existent pages, matching production behaviour.
Deployment¶
CI workflow¶
The public-site.yml workflow triggers on pushes to main that change:
frontend/public_pages/**frontend/public/**(shared static assets)frontend/src/components/**(shared components)frontend/src/theme.tsfrontend/src/styles/**
Pipeline:
- Build —
yarn workspace public-pages build→ outputs tofrontend/dist/public_pages/ - Deploy staging — syncs to
{staging-project}-landingGCS bucket - Deploy production — syncs to
{prod-project}-landingGCS bucket
Cache strategy¶
| File type | Cache-Control | Reason |
|---|---|---|
Hashed assets (assets/*.js, assets/*.css) |
public, max-age=31536000, immutable |
Filename hash changes on content change |
HTML files (*.html) |
no-cache |
Always revalidate to pick up new deploys |
Infrastructure¶
The GCS landing bucket sits behind the Global HTTPS Load Balancer (managed by Terraform):
- SSL — Google-managed certificate covering
quill-medical.comandwww.quill-medical.com - CDN — Enabled on the backend bucket for edge caching
- 404 page — Load balancer serves
not-found.htmlfor missing paths - Cloud Armor — WAF with rate limiting (500 req/min per IP)
DNS¶
| Record | Type | Value |
|---|---|---|
quill-medical.com |
A | Staging LB IP |
www.quill-medical.com |
CNAME | quill-medical.com |