import { access, readFile, readdir, stat } from 'node:fs/promises'; import { extname, resolve } from 'node:path'; import sharp from 'sharp'; import { allowedUploadExtensions, maxBuiltImageBytes, maxBuiltImageDimension } from './image-policy.mjs'; const key = process.env.SITE_KEY; if (!key) throw new Error('SITE_KEY is required.'); const root = resolve(`dist/${key}`); const required = ['index.html', '404.html', 'robots.txt', 'sitemap.xml']; if (key === 'www') required.push('about/index.html', 'services/index.html', 'contact/index.html'); for (const file of required) await access(resolve(root, file)); const pages = await Promise.all(required.filter((file) => file.endsWith('.html')).map(async (file) => ({ file, html: await readFile(resolve(root, file), 'utf8') }))); for (const { file, html } of pages) { if (html.includes('{{') || html.includes('{%')) throw new Error(`${file} contains an unresolved template placeholder.`); if (!/[^<]+<\/title>/.test(html)) throw new Error(`${file} is missing a title.`); if (!html.includes('data-demo-banner') || !html.includes('href="https://sloanops.com/"')) throw new Error(`${file} is missing the SloanOps demo banner.`); for (const match of html.matchAll(/(?:src|href)="(\/images\/[^"?#]+)"/g)) await access(resolve(root, `.${match[1]}`)); } const robots = await readFile(resolve(root, 'robots.txt'), 'utf8'); if (process.env.DEPLOY_ENV === 'production') { if (!robots.includes('Allow: /') || !robots.includes('Sitemap: https://')) throw new Error(`${key} production robots rules are not indexable.`); } else { if (robots !== 'User-agent: *\nDisallow: /\n') throw new Error(`${key} preview robots rules must disallow all crawling.`); for (const { file, html } of pages) if (!html.includes('<meta name="robots" content="noindex,nofollow">')) throw new Error(`${file} is missing preview no-index metadata.`); } await Promise.all(['admin/index.html', 'admin/config.yml', '_headers'].map((file) => access(resolve(root, file)))); const admin = await readFile(resolve(root, 'admin/index.html'), 'utf8'); if (!admin.includes('content="noindex,nofollow,noarchive"')) throw new Error('Admin shell is missing no-index metadata.'); if (!admin.includes('decap-cms@3.15.1/dist/decap-cms.js')) throw new Error('Admin shell is missing the pinned Decap CMS application.'); const headers = await readFile(resolve(root, '_headers'), 'utf8'); if (!/\/admin\/\*[\s\S]*X-Robots-Tag: noindex, nofollow, noarchive/.test(headers)) throw new Error('Admin response headers are missing the no-index rule.'); const builtUploads = resolve(root, 'images/uploads'); for (const entry of await readdir(builtUploads, { withFileTypes: true })) { if (!entry.isFile()) throw new Error(`Built CMS media contains a nested or special path: ${entry.name}.`); const extension = extname(entry.name).toLowerCase(); if (!allowedUploadExtensions.has(extension)) throw new Error(`Built CMS media has a disallowed extension: ${entry.name}.`); const path = resolve(builtUploads, entry.name); const info = await stat(path); if (info.size > maxBuiltImageBytes) throw new Error(`Built CMS image exceeds ${maxBuiltImageBytes} bytes: ${entry.name}.`); const metadata = await sharp(path).metadata(); if ((metadata.width ?? 0) > maxBuiltImageDimension || (metadata.height ?? 0) > maxBuiltImageDimension) throw new Error(`Built CMS image exceeds ${maxBuiltImageDimension}px: ${entry.name}.`); if (metadata.exif || metadata.icc || metadata.iptc || metadata.xmp) throw new Error(`Built CMS image retained metadata: ${entry.name}.`); } const home = await readFile(resolve(root, 'index.html'), 'utf8'); if (key === 'www') { for (const { file, html } of pages) if (!html.includes('href="https://blog.3031023.xyz/"')) throw new Error(`${file} is missing the Happy Tails Journal link.`); for (const question of ['What services do you offer?', 'How do I book a visit?', 'How far ahead should I book pet care?', 'Can you accommodate special routines?']) if (!home.includes(question)) throw new Error(`Primary FAQ Markdown entry is missing: ${question}.`); if (home.includes('What information should I bring to a meet and greet?')) throw new Error('Draft FAQ entry appeared in the homepage.'); const services = await readFile(resolve(root, 'services/index.html'), 'utf8'); for (const title of ['Dog walking', 'Grooming', 'Overnight care']) if (!services.includes(title)) throw new Error(`Service Markdown entry is missing: ${title}.`); const about = await readFile(resolve(root, 'about/index.html'), 'utf8'); for (const name of ['Sarah Mitchell', 'Marcus Rivera', 'Emily Chen', 'David Okonkwo']) if (!about.includes(name)) throw new Error(`Team Markdown entry is missing: ${name}.`); for (const image of ['/images/uploads/hero-dogs.jpg', '/images/uploads/happy-tails-team.jpg']) if (!home.includes(image)) throw new Error(`CMS-controlled homepage image is missing: ${image}.`); for (const image of ['/images/uploads/sarah-mitchell.jpg', '/images/uploads/marcus-rivera.jpg', '/images/uploads/emily-chen.jpg', '/images/uploads/david-okonkwo.jpg']) if (!about.includes(image)) throw new Error(`CMS-controlled team image is missing: ${image}.`); const contact = await readFile(resolve(root, 'contact/index.html'), 'utf8'); if (!contact.includes('/images/uploads/dog-walking.jpg')) throw new Error('CMS-controlled contact image is missing.'); if (!services.includes('/images/uploads/dog-grooming.jpg')) throw new Error('CMS-controlled services image is missing.'); } if (key === 'blog') { const publishedPosts = ['first-walk-after-work', 'gentler-grooming-day', 'overnight-care-checklist', 'weekend-care-prep']; await Promise.all(publishedPosts.map((slug) => access(resolve(root, `posts/${slug}/index.html`)))); for (const slug of publishedPosts) { const html = await readFile(resolve(root, `posts/${slug}/index.html`), 'utf8'); if (!html.includes('data-demo-banner') || !html.includes('href="https://sloanops.com/"')) throw new Error(`Blog post is missing the SloanOps demo banner: ${slug}.`); } if (home.includes('Draft only example for editorial review')) throw new Error('Draft blog post appeared in the journal.'); try { await access(resolve(root, 'posts/draft-only-example/index.html')); throw new Error('Draft blog post generated a public page.'); } catch (error) { if (error?.code !== 'ENOENT') throw error; } } console.log(`Verified ${key}: ${required.join(', ')}`);