← back to Professional Directory

scripts/cache-healthcare-templates.js

90 lines

#!/usr/bin/env node
/**
 * Cache top-rated healthcare-practice site bodies as structural templates
 * for mockup generation. Adapted from lawyer-directory-builder/public/
 * mockup-templates/top12.json + firm-*.body.txt pattern.
 *
 * Run once (or weekly):
 *   node scripts/cache-healthcare-templates.js
 *
 * Outputs:
 *   data/site-templates/top10.json          — manifest [{id,name,site,score,body_file}]
 *   data/site-templates/<id>.body.txt       — 6KB structural HTML excerpt per template
 *
 * The body files are scripts/styles-stripped HTML excerpts intended as
 * "section ordering + layout density" inspiration for the LLM. The LLM is
 * instructed to ignore the template's colors/fonts and apply our aesthetic.
 */
const fs = require('fs');
const path = require('path');
const cheerio = require('cheerio');
const { fetch } = require('undici');

const OUT = path.resolve(__dirname, '../data/site-templates');
fs.mkdirSync(OUT, { recursive: true });

// Curated top-rated healthcare practice landing pages — mix of
// big-system marketing pages, modern primary-care brands, and DTC telehealth.
// Score is a hand-assigned proxy for SEO+UX quality (90-100).
const TEMPLATES = [
  { id: 'mayo',          name: 'Mayo Clinic',        site: 'https://www.mayoclinic.org/',         score: 100 },
  { id: 'cleveland',     name: 'Cleveland Clinic',   site: 'https://my.clevelandclinic.org/',     score: 100 },
  { id: 'onemedical',    name: 'One Medical',        site: 'https://www.onemedical.com/',         score: 98  },
  { id: 'forward',       name: 'Forward Health',     site: 'https://goforward.com/',              score: 95  },
  { id: 'carbon',        name: 'Carbon Health',      site: 'https://carbonhealth.com/',           score: 95  },
  { id: 'parsley',       name: 'Parsley Health',     site: 'https://www.parsleyhealth.com/',      score: 95  },
  { id: 'tia',           name: 'Tia',                site: 'https://www.asktia.com/',             score: 95  },
  { id: 'oak',           name: 'Oak Street Health',  site: 'https://www.oakstreethealth.com/',    score: 92  },
  { id: 'galileo',       name: 'Galileo Health',     site: 'https://galileo.io/',                 score: 90  },
  { id: 'hims',          name: 'Hims',               site: 'https://www.hims.com/',               score: 90  },
];

const UA = 'pd-template-cache/0.1 (research; contact: steve@designerwallcoverings.com)';
const MAX_BODY = 6500;   // chars — gemma3:12b context allows more, this is the LLM-input cap

function stripToStructure(html) {
  const $ = cheerio.load(html);
  $('script, style, noscript, link, meta, head, iframe, svg').remove();
  // Drop attributes that are noise for structural inspiration.
  $('*').each((_, el) => {
    if (el.type !== 'tag') return;
    const keep = ['class', 'href', 'role'];
    Object.keys(el.attribs || {}).forEach(k => { if (!keep.includes(k)) delete el.attribs[k]; });
  });
  // Cap class strings to first 60 chars to keep file size sane.
  $('[class]').each((_, el) => {
    el.attribs.class = String(el.attribs.class || '').slice(0, 60);
  });
  const body = $('body').html() || $.html();
  return body
    .replace(/<!--[\s\S]*?-->/g, '')
    .replace(/\s+/g, ' ')
    .replace(/>\s+</g, '><')
    .trim()
    .slice(0, MAX_BODY);
}

async function main() {
  const manifest = [];
  for (const t of TEMPLATES) {
    process.stdout.write(`[tpl] ${t.id.padEnd(12)} ${t.site} ... `);
    try {
      const r = await fetch(t.site, { headers: { 'User-Agent': UA, Accept: 'text/html' }, signal: AbortSignal.timeout(15_000), redirect: 'follow' });
      if (!r.ok) { console.log(`HTTP ${r.status}`); continue; }
      const html = await r.text();
      const body = stripToStructure(html);
      if (body.length < 500) { console.log('body too small, skip'); continue; }
      const file = `${t.id}.body.txt`;
      fs.writeFileSync(path.join(OUT, file), body);
      manifest.push({ id: t.id, name: t.name, site: t.site, score: t.score, body_file: file, body_size: body.length });
      console.log(`${body.length}B`);
    } catch (e) {
      console.log(`ERR ${e.message}`);
    }
  }
  fs.writeFileSync(path.join(OUT, 'top10.json'), JSON.stringify(manifest, null, 2));
  console.log(`\n[tpl] cached ${manifest.length}/${TEMPLATES.length} → ${OUT}/top10.json`);
}

main().catch(e => { console.error(e); process.exit(1); });