Skip to content

Commit f250762

Browse files
markhazletonclaude
andcommitted
Migrate deployment docs to Azure Static Web Apps, add lint/test/clean scripts, extract shared CSS
The site deploys via Azure Static Web Apps, not GitHub Pages, so remove the leftover CNAME/.nojekyll files and correct README, copilot-instructions, and the constitution to describe the real deployment pipeline (custom domain is bound in the Azure portal, not a repo file). Add npm run clean/lint/test alongside the existing build script: ESLint (flat config) over the Node sources, and a Node built-in test-runner suite that builds the site and checks every page exists, required Azure/SEO files are copied, and internal links resolve. Wire lint + test + a docs/ drift check into a new CI workflow so a stale committed docs/ or a broken build is caught before the Azure deploy workflow uploads it. Extract the shared inline <style> block into one docs/styles.css referenced via <link> from every page, so browsers cache it once across navigations instead of re-downloading ~28KB of duplicate CSS on every page load (index.html alone drops from 60KB to 32KB). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
1 parent a896a7e commit f250762

27 files changed

Lines changed: 1997 additions & 8425 deletions

.documentation/memory/constitution.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,11 +14,11 @@ against `content.json`, brand documentation, and generated output in `docs/`.
1414
This repository is a static marketing site. Prefer generated HTML, CSS, and
1515
small focused JavaScript over adding client-side frameworks, backends,
1616
databases, or build complexity. New dependencies must have a clear benefit and
17-
must not make GitHub Pages deployment harder to reason about.
17+
must not make Azure Static Web Apps deployment harder to reason about.
1818

1919
### III. Production Output Integrity
2020

21-
The committed `docs/` directory is the GitHub Pages deployment output and must
21+
The committed `docs/` directory is the Azure Static Web Apps deployment output and must
2222
stay synchronized with source changes. Any change to `build.js`, `content.json`,
2323
or files under `static/` or `attached_assets/` that affects rendered pages must
2424
include a regenerated `docs/` output.
@@ -49,7 +49,7 @@ brand/project reference material.
4949
- Node.js 18+ with npm scripts
5050
- Static site generator implemented in `build.js`
5151
- Site content stored in `content.json`
52-
- Generated GitHub Pages output in `docs/`
52+
- Generated Azure Static Web Apps output in `docs/`
5353
- Source static assets in `static/` and `attached_assets/`
5454
- No backend, database, or client-side framework
5555

.github/copilot-instructions.md

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -18,10 +18,10 @@ MakeBoldSolutionsCom/
1818
├── dev.js # Local dev server with rebuild-on-change
1919
├── serve.js # Plain static file server for docs/
2020
├── static/ # Files copied verbatim into docs/ on every build
21-
│ ├── CNAME, .nojekyll, robots.txt, sitemap.xml, favicon.*, .well-known/
21+
│ ├── staticwebapp.config.json, robots.txt, sitemap.xml, favicon.*, .well-known/
2222
├── attached_assets/ # Logo files, vCards, contact photos used during build
2323
├── documentation/ # Brand guide, logo source files
24-
└── docs/ # Build output, committed to git for GitHub Pages
24+
└── docs/ # Build output, committed to git for Azure Static Web Apps
2525
```
2626

2727
## Development Workflows
@@ -35,8 +35,8 @@ npm run serve # Serve existing docs/ without rebuilding
3535

3636
### Build & Deployment
3737
- Build output goes to `docs/`, which is committed to git.
38-
- GitHub Pages serves from `main` branch, `/docs` folder, custom domain `makeboldsolutions.com` (see `static/CNAME`).
39-
- `build.js` copies `static/CNAME`, `static/.nojekyll`, `static/robots.txt`, `static/sitemap.xml`, favicons, and `static/.well-known/security.txt` into `docs/` on every run — edit the source files in `static/`, not the generated copies in `docs/`.
38+
- Azure Static Web Apps deploys on push to `main` via `.github/workflows/azure-static-web-apps-*.yml`, which uploads `docs/` as a pre-built artifact (`skip_app_build: true`) — there is no build step in CI, so `docs/` must be committed already up to date. Custom domain `makeboldsolutions.com` is configured in the Azure portal, not a repo file.
39+
- `build.js` copies `static/staticwebapp.config.json`, `static/robots.txt`, `static/sitemap.xml`, favicons, and `static/.well-known/security.txt` into `docs/` on every run — edit the source files in `static/`, not the generated copies in `docs/`.
4040

4141
## Code Conventions
4242
- Pages are built by individual `build*()` functions in `build.js` (e.g. `buildHome`, `buildAbout`, `buildContact`) and registered in the `pages` array near the bottom of the file.

.github/workflows/ci.yml

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
name: CI
2+
3+
on:
4+
push:
5+
branches: [main]
6+
pull_request:
7+
branches: [main]
8+
9+
jobs:
10+
verify:
11+
runs-on: ubuntu-latest
12+
steps:
13+
- uses: actions/checkout@v4
14+
15+
- uses: actions/setup-node@v4
16+
with:
17+
node-version: 20
18+
cache: npm
19+
20+
- run: npm ci
21+
- run: npm run lint
22+
- run: npm test
23+
24+
- name: Verify docs/ is up to date with source
25+
run: |
26+
npm run build
27+
if ! git diff --quiet -- docs/; then
28+
echo "::error::docs/ is out of date with build.js/content.json. Run 'npm run build' locally and commit the result."
29+
git diff --stat -- docs/
30+
exit 1
31+
fi

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
# Dependencies
22
node_modules
33

4-
# Note: docs/ is NOT ignored - it's the GitHub Pages deployment output
4+
# Note: docs/ is NOT ignored - it's the Azure Static Web Apps deployment output
55

66
# Environment variables
77
.env

README.md

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,9 @@ The site is a Node.js static site generator: `build.js` reads content from `cont
3737

3838
## Available Scripts
3939

40+
- `npm run clean` - Remove the generated `docs/` directory (build.js never clears stale files on its own, so this catches orphaned output from removed pages/assets)
41+
- `npm run lint` - Lint `build.js`, `dev.js`, `serve.js`, and `test/` with ESLint
42+
- `npm test` - Run the build against a smoke-test suite (Node's built-in test runner): verifies every page builds, required Azure/SEO files are copied, and internal links resolve to real files
4043
- `npm run build` - Run the static site generator (`build.js`) and write output to `docs/`
4144
- `npm run dev` - Build, serve `docs/` locally, and rebuild automatically when `build.js` or `content.json` changes
4245
- `npm run serve` - Serve the existing `docs/` output without rebuilding
@@ -50,15 +53,14 @@ MakeBoldSolutionsCom/
5053
├── dev.js # Local dev server with rebuild-on-change
5154
├── serve.js # Plain static file server for docs/
5255
├── static/ # Source files copied into docs/ on every build
53-
│ ├── CNAME # Custom domain for GitHub Pages
54-
│ ├── .nojekyll # Prevents Jekyll processing
56+
│ ├── staticwebapp.config.json # Azure Static Web Apps routing, headers, caching
5557
│ ├── robots.txt
5658
│ ├── sitemap.xml
5759
│ ├── favicon.png / favicon.svg
5860
│ └── .well-known/security.txt
5961
├── attached_assets/ # Logo files, vCards, and contact photos used during build
6062
├── documentation/ # Brand guide, logo source files, project docs
61-
└── docs/ # Build output (committed to git for GitHub Pages)
63+
└── docs/ # Build output (committed to git for Azure Static Web Apps)
6264
```
6365

6466
## Pages
@@ -76,7 +78,7 @@ Site copy, navigation, and page sections live in `content.json`. Edit that file
7678

7779
## Deployment
7880

79-
GitHub Pages is configured to serve from the `main` branch, `/docs` folder, at the custom domain `makeboldsolutions.com` (see `static/CNAME`). After making changes:
81+
The site deploys to Azure Static Web Apps via [.github/workflows/azure-static-web-apps-lemon-plant-08b5fed10.yml](.github/workflows/azure-static-web-apps-lemon-plant-08b5fed10.yml), which triggers on every push to `main`. It uploads the pre-built `docs/` folder as-is (`skip_app_build: true`, `app_location: "docs"`) — there is no build step in CI, so `docs/` must be regenerated and committed locally before pushing. The custom domain `makeboldsolutions.com` is configured in the Azure Static Web Apps portal (Custom domains blade), not via a repo file. Routing, security headers, and caching rules live in [static/staticwebapp.config.json](static/staticwebapp.config.json), which `build.js` copies into `docs/` on every build. After making changes:
8082

8183
```bash
8284
npm run build

build.js

Lines changed: 6 additions & 68 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,7 @@ const staticDir = join(__dirname, 'static');
3636
copyFileSync(join(staticDir, 'favicon.png'), join(docsDir, 'favicon.png'));
3737
copyFileSync(join(staticDir, 'favicon.svg'), join(docsDir, 'favicon.svg'));
3838

39-
// Copy GitHub Pages essentials (custom domain, Jekyll bypass, crawler rules, security contact)
40-
copyFileSync(join(staticDir, 'CNAME'), join(docsDir, 'CNAME'));
41-
copyFileSync(join(staticDir, '.nojekyll'), join(docsDir, '.nojekyll'));
39+
// Copy crawler rules and security contact
4240
copyFileSync(join(staticDir, 'robots.txt'), join(docsDir, 'robots.txt'));
4341
copyFileSync(join(staticDir, 'sitemap.xml'), join(docsDir, 'sitemap.xml'));
4442
// Copy Azure Static Web Apps config (routing, redirects, MIME types, security headers)
@@ -1014,6 +1012,9 @@ h1, h2, h3, h4, h5, h6 {
10141012
@media (max-width: 640px) { .deck-hero h1 { font-size: 1.9rem; } }
10151013
`;
10161014

1015+
// Write CSS once as a shared external file so browsers cache it across page
1016+
// navigations instead of re-downloading it inlined in every HTML page.
1017+
writeFileSync(join(docsDir, 'styles.css'), css.trim() + '\n', 'utf-8');
10171018

10181019
// ─── Shared HTML Builders ────────────────────────────────────────────────────
10191020

@@ -1097,7 +1098,7 @@ ${robotsMeta} <link rel="canonical" href="${escapeAttr(canonicalUrl)}">
10971098
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
10981099
<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,400;0,500;0,600;0,700;0,800;0,900;1,400&family=Inter+Tight:wght@300;400;500;600;700&display=swap" rel="stylesheet">
10991100
<title>${escapeHtml(pageTitle)}</title>
1100-
<style>${css}</style>
1101+
<link rel="stylesheet" href="styles.css">
11011102
</head>
11021103
<body>`;
11031104
}
@@ -1447,69 +1448,6 @@ function buildServicesCfo() {
14471448
</section>` + footer() + closePage();
14481449
}
14491450

1450-
function buildCaseStudies() {
1451-
const cs = content.caseStudies;
1452-
1453-
const studies = cs.studies.map(study => `
1454-
<div class="case-study">
1455-
<div class="grid grid-12" style="gap:2rem">
1456-
<div class="md-col-span-8">
1457-
<div style="display:flex;align-items:center;gap:1rem;margin-bottom:1rem">
1458-
<span class="badge">${escapeHtml(study.sector)}</span>
1459-
<span class="badge-muted">${escapeHtml(study.category)}</span>
1460-
</div>
1461-
<h2 class="text-2xl font-heading font-bold mb-6">${escapeHtml(study.title)}</h2>
1462-
<div style="margin-bottom:2rem">
1463-
<h4 class="text-sm font-bold uppercase tracking-wider text-primary mb-1">Challenge</h4>
1464-
<p class="text-muted">${escapeHtml(study.challenge)}</p>
1465-
</div>
1466-
<div style="margin-bottom:2rem">
1467-
<h4 class="text-sm font-bold uppercase tracking-wider text-primary mb-1">Solution</h4>
1468-
<p class="text-muted">${escapeHtml(study.solution)}</p>
1469-
</div>
1470-
<div class="outcome-box">
1471-
<h4 style="font-weight:700;margin-bottom:0.25rem">Outcome</h4>
1472-
<p style="font-size:1.125rem;font-weight:500;opacity:0.8">${escapeHtml(study.outcome)}</p>
1473-
</div>
1474-
</div>
1475-
<div class="md-col-span-4 metrics-panel">
1476-
${study.metrics.map(m => `
1477-
<div class="metric">
1478-
<div class="value">${escapeHtml(m.value)}</div>
1479-
<div class="label">${escapeHtml(m.label)}</div>
1480-
</div>`).join('')}
1481-
</div>
1482-
</div>
1483-
</div>`).join('');
1484-
1485-
return htmlHead('Case Studies', cs.header.description) + navigation('case-studies.html') + `
1486-
1487-
<div class="section-dark header-accent page-header">
1488-
<div class="container" style="position:relative;z-index:1">
1489-
<span class="text-primary font-bold tracking-widest uppercase text-sm mb-4" style="display:block">Client Results</span>
1490-
<h1 class="text-4xl lg-text-5xl font-heading font-bold mb-6">${escapeHtml(cs.header.heading)}</h1>
1491-
<p class="text-xl text-white-80 max-w-2xl">${escapeHtml(cs.header.description)}</p>
1492-
</div>
1493-
</div>
1494-
1495-
<div class="container section">
1496-
<div class="grid" style="gap:3rem">
1497-
${studies}
1498-
</div>
1499-
</div>
1500-
1501-
<section class="section-primary section">
1502-
<div class="container text-center">
1503-
<h2 class="text-3xl font-heading font-bold mb-6">Ready to Write Your Own Success Story?</h2>
1504-
<p class="text-white-80 max-w-2xl mx-auto mb-10 text-lg">Every engagement starts with a conversation. Let's talk about where you are and where you want to go.</p>
1505-
<div class="flex flex-col sm-flex-row gap-4 justify-center">
1506-
<a href="contact.html" class="btn-white btn-large">Get in Touch</a>
1507-
<a href="services-cfo.html" class="btn-outline-white btn-large">Explore Our Services</a>
1508-
</div>
1509-
</div>
1510-
</section>` + footer() + closePage();
1511-
}
1512-
15131451
function buildAbout() {
15141452
const a = content.about;
15151453
const cf = a.cofounder;
@@ -1916,7 +1854,7 @@ async function buildCard(person) {
19161854
<link rel="preconnect" href="https://fonts.googleapis.com">
19171855
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
19181856
<link href="https://fonts.googleapis.com/css2?family=Be+Vietnam+Pro:ital,wght@0,400;0,500;0,600;0,700;0,800;1,400&family=Inter+Tight:ital,wght@0,400;0,500;0,600;0,700;1,400&display=swap" rel="stylesheet">
1919-
<style>${css}</style>
1857+
<link rel="stylesheet" href="/styles.css">
19201858
</head>
19211859
<body>
19221860
<main class="card-page">

clean.js

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import { rmSync } from 'fs';
2+
import { join, dirname } from 'path';
3+
import { fileURLToPath } from 'url';
4+
5+
const __dirname = dirname(fileURLToPath(import.meta.url));
6+
const docsDir = join(__dirname, 'docs');
7+
8+
rmSync(docsDir, { recursive: true, force: true });
9+
console.log('Removed docs/');

docs/.nojekyll

Whitespace-only changes.

0 commit comments

Comments
 (0)