Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions apps/web/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
},
"dependencies": {
"@profullstack/affiliates": "0.3.0",
"@profullstack/footer": "latest",
"@profullstack/profile": "0.1.6",
"@profullstack/redeem-credits": "0.2.0",
"@profullstack/sealed-secrets": "0.1.0",
Expand Down
7 changes: 1 addition & 6 deletions apps/web/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@
</a>
<div style="display: flex; flex-wrap: wrap; align-items: center; gap: 28px; font-size: 15px; color: #A3ACA6">
<a href="#product" style="color: #A3ACA6; text-decoration: none">Product</a>
<a href="/health" style="color: #A3ACA6; text-decoration: none">Health tracker</a>
<a href="#navigation" style="color: #A3ACA6; text-decoration: none">Get paid</a>
<a href="#agents" style="color: #A3ACA6; text-decoration: none">Agents</a>
<a href="#import" style="color: #A3ACA6; text-decoration: none">Import</a>
Expand Down Expand Up @@ -339,12 +340,6 @@ <h2 style="margin: 0; font-size: clamp(32px, 4vw, 52px); letter-spacing: -0.035e
</div>
</section>

<footer class="site-foot">
<span>&copy; <!--year--> Profullstack, Inc.</span>
<nav class="webring" aria-label="Profullstack webring"><a href="https://rssamplifier.com/ring/profullstack/previous?from=https%3A%2F%2Ftleehealth.com%2F" rel="prev" title="Previous site">&lt;&lt;</a><a href="https://rssamplifier.com/ring/profullstack">Profullstack</a><a href="https://rssamplifier.com/ring/profullstack/next?from=https%3A%2F%2Ftleehealth.com%2F" rel="next" title="Next site">&gt;&gt;</a></nav><span style="display:inline-flex;flex-wrap:wrap;gap:.6rem;align-items:center;justify-content:center"><a class="builtindie-badge" href="https://built.indie.pit.world/for?site=https://tleehealth.com/" style="display:inline-block;line-height:0"><img src="https://built.indie.pit.world/for.svg?site=https://tleehealth.com/" alt="Listed on built.indie" width="200" height="44" loading="lazy" style="display:block;max-width:100%"></a><a class="pwamart-badge" href="https://pwamart.com/for?site=https://tleehealth.com/" style="display:inline-block;line-height:0"><img src="https://pwamart.com/for.svg?site=https://tleehealth.com/" alt="Get it on pwamart" width="200" height="44" loading="lazy" style="display:block;max-width:100%"></a></span>
<a href="/terms">Terms</a>
<a href="/privacy">Privacy</a>
</footer>
</div>
<script>if('serviceWorker' in navigator)navigator.serviceWorker.register('/sw.js').catch(()=>{});</script>
</body>
Expand Down
26 changes: 21 additions & 5 deletions apps/web/src/app.js
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ import { Hono } from 'hono';
import { x402Gateway } from '@profullstack/x402-gateway/hono';
import { createCrawlGate } from './crawl-gate.js';
import { api, coinpayWebhook, saveSignup } from './api.js';
import { withPitFooter } from './pit-footer.js';
import { withUniversalFooter } from './universal-footer.js';
import * as auth from './auth.js';
import { config } from './config.js';
import * as credits from './credits.js';
Expand Down Expand Up @@ -35,12 +35,28 @@ export const crawlGate = createCrawlGate();
if (crawlGate) app.use('*', x402Gateway(crawlGate));

// The landing page is built from the design canvas (design/Main.dc.html). Every
// page gets the pit.world network footer as the last thing in <body>.
// HTML page gets the shared Profullstack footer as the last thing in <body>.
const page = async (c, name) =>
c.html(await withPitFooter(pub(name).toString().replace('<!--year-->', String(new Date().getFullYear()))));
c.html(await withUniversalFooter(pub(name).toString().replace('<!--year-->', String(new Date().getFullYear()))));
app.get('/', (c) => page(c, 'index.html'));
app.get('/healthz', (c) => c.text('ok'));

// The personal health tracker is a local-first PWA. It has no account or API
// calls; readings live in the visitor's IndexedDB and never reach the server.
app.get('/health/', (c) => c.redirect('/health', 301));
app.get('/health', (c) =>
c.body(pub('health/index.html'), 200, { 'content-type': 'text/html; charset=utf-8', 'cache-control': 'no-cache' }),
);
app.get('/health/health.css', (c) =>
c.body(pub('health/health.css'), 200, { 'content-type': 'text/css; charset=utf-8', 'cache-control': 'no-cache' }),
);
app.get('/health/health.js', (c) =>
c.body(pub('health/health.js'), 200, { 'content-type': 'text/javascript; charset=utf-8', 'cache-control': 'no-cache' }),
);
app.get('/health/manifest.webmanifest', (c) =>
c.body(pub('health/manifest.webmanifest'), 200, { 'content-type': 'application/manifest+json', 'cache-control': 'no-cache' }),
);

// The app: one shell for the team, patients and sign-in. It talks only to /api/v1.
// The shared profile fields (@profullstack/profile) ride in the shell as a
// <template>: the signup form and Settings clone them, so the markup is the
Expand All @@ -50,7 +66,7 @@ const PROFILE_TEMPLATE = `<template id="profile-fields">${renderProfileFields({
})}</template>`;
const shell = async (c) =>
c.html(
await withPitFooter(
await withUniversalFooter(
pub('app/index.html')
.toString()
.replace('<!--year-->', String(new Date().getFullYear()))
Expand Down Expand Up @@ -116,7 +132,7 @@ app.get('/i/:code', async (c) => {
);
const esc = (s) => String(s).replace(/[&<>"']/g, (ch) => `&#${ch.charCodeAt(0)};`);
return c.html(
await withPitFooter(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
await withUniversalFooter(`<!doctype html><html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>You're invited to tleehealth</title><meta name="robots" content="noindex"><link rel="stylesheet" href="/assets/app.css"><link rel="icon" href="/icon.svg"></head>
<body><div class="center"><div class="auth"><h1>You're invited to tleehealth</h1>
<p class="muted">Scheduling, prescriptions, labs and AI follow-up calls for your practice. Join with this invite and you both get ${credits.INVITE_BONUS_DAYS} days of the paid plan, on top of the 14-day trial.</p>
Expand Down
42 changes: 0 additions & 42 deletions apps/web/src/pit-footer.js

This file was deleted.

41 changes: 41 additions & 0 deletions apps/web/src/universal-footer.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
import { footerHtml, footerHtmlSync } from '@profullstack/footer';

const options = {
site: 'https://tleehealth.com/',
owner: { name: 'tleehealth', url: 'https://tleehealth.com/' },
links: [
{ label: 'Start a practice', href: '/app' },
{ label: 'Terms', href: '/terms' },
{ label: 'Privacy', href: '/privacy' },
{ label: 'Contact', href: 'mailto:hello@tleehealth.com' },
],
pwamartHref: 'https://pwamart.com/for?site=https://tleehealth.com/',
builtIndieHref: 'https://built.indie.pit.world/for?site=https://tleehealth.com/',
};
const TTL_MS = 15 * 60 * 1000;

let html = footerHtmlSync(options);
let fetchedAt = 0;
let inflight = null;

async function refresh() {
inflight ??= footerHtml(options)
.then((value) => {
html = value;
fetchedAt = Date.now();
})
.catch(() => {})
.finally(() => {
inflight = null;
});
await inflight;
}

/** Append the universal footer to each HTML page, keeping it in server markup. */
export async function withUniversalFooter(page) {
if (Date.now() - fetchedAt > TTL_MS) await refresh();
const i = page.lastIndexOf('</body>');
if (i === -1) return page;
const registration = page.includes('/pwa-register.js') ? '' : '<script src="/pwa-register.js" defer></script>\n';
return `${page.slice(0, i)}${registration}${html}\n${page.slice(i)}`;
}
3 changes: 3 additions & 0 deletions bun.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions test/app.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,9 @@ describe('web', () => {
});
test('landing footer server-renders the Profullstack webring row, one copyright line', async () => {
const home = await (await get('/')).text();
const foot = home.slice(home.indexOf('<footer class="site-foot">'), home.indexOf('</footer>', home.indexOf('<footer class="site-foot">')));
expect(foot).toContain('class="webring"');
const foot = home.match(/<footer\b[^>]*data-pfs-footer[^>]*>[\s\S]*?<\/footer>/)?.[0] ?? '';
expect(foot).not.toBe('');
expect(foot).toContain('class="webring pfs-footer__ring"');
expect(foot).toContain('https://rssamplifier.com/ring/profullstack/next?from=https%3A%2F%2Ftleehealth.com%2F');
expect(foot).toContain('https://rssamplifier.com/ring/profullstack/previous?from=https%3A%2F%2Ftleehealth.com%2F');
expect(foot.match(/&copy;/g)?.length).toBe(1);
Expand Down
5 changes: 3 additions & 2 deletions test/signup-profile.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,9 @@ test('the signup page shell carries the profile fields, once each', async () =>
const html = await (await app.request('/signup')).text();
for (const name of ['first_name', 'last_name', 'company_domain', 'job_title', 'linkedin_url'])
expect(html.match(new RegExp(`name="${name}"`, 'g'))?.length).toBe(1);
// Email is the form's own box, not the template's.
expect(html).not.toContain('name="email"');
// The shared footer has its own email field; the profile template does not.
const fields = html.match(/<template id="profile-fields">[\s\S]*?<\/template>/)?.[0] ?? '';
expect(fields).not.toContain('name="email"');
expect((await app.request('/assets/profile.js')).headers.get('content-type')).toContain('javascript');
});

Expand Down
Loading