-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathbuild.js
More file actions
361 lines (346 loc) · 20.4 KB
/
Copy pathbuild.js
File metadata and controls
361 lines (346 loc) · 20.4 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
#!/usr/bin/env node
/* ============================================================================
Integrated Software Technologies — static site generator
Reads assets/apps.js + assets/notes.js and emits:
apps/<slug>/index.html per-app landing pages (+ live rating via iTunes)
notes/<slug>/index.html journal / build-note pages
press/index.html press kit
assets/og/<slug>.png per-app share cards (via qlmanage + sips)
sitemap.xml, robots.txt, 404.html
Run: node build.js
Homepage (index.html) is hand-maintained and NOT touched here.
============================================================================ */
'use strict';
const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');
const ROOT = 'https://integratedsw.tech';
const dir = __dirname;
const { APPS } = require('./assets/apps.js');
const { NOTES } = require('./assets/notes.js');
const today = new Date().toISOString().slice(0, 10);
const esc = s => String(s || '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
const idOf = url => (String(url || '').match(/id(\d+)/) || [])[1];
const mkdir = p => fs.mkdirSync(p, { recursive: true });
const write = (rel, html) => { const f = path.join(dir, rel); mkdir(path.dirname(f)); fs.writeFileSync(f, html); };
/* -------------------------------------------------------------- shared head */
function head({ title, desc, canonical, og }) {
return `<!doctype html><html lang="en"><head>
<meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
<title>${esc(title)}</title>
<meta name="description" content="${esc(desc)}">
<link rel="canonical" href="${canonical}">
<meta name="theme-color" content="#06070d">
<meta property="og:type" content="website"><meta property="og:site_name" content="Integrated Software Technologies">
<meta property="og:title" content="${esc(title)}"><meta property="og:description" content="${esc(desc)}">
<meta property="og:url" content="${canonical}"><meta property="og:image" content="${og}">
<meta property="og:image:width" content="1200"><meta property="og:image:height" content="630">
<meta name="twitter:card" content="summary_large_image">
<meta name="twitter:title" content="${esc(title)}"><meta name="twitter:image" content="${og}">
<link rel="icon" href="/assets/favicon.svg" type="image/svg+xml">
<link rel="apple-touch-icon" href="/assets/icon-192.png">
<link rel="stylesheet" href="/assets/fonts.css"><link rel="stylesheet" href="/assets/style.css">
</head><body>
<div id="bg"><div class="blob a"></div><div class="blob b"></div><div class="blob c"></div></div>
<nav id="nav">
<a class="brand" href="/"><span class="mark">i</span><span>Integrated Software<br><small>integratedsw.tech</small></span></a>
<div class="navlinks"><a href="/#apps">Apps</a><a href="/#journal">Journal</a><a href="/press/">Press</a><a class="cta" href="/#contact">Get in touch</a></div>
</nav>`;
}
const foot = `<footer><a class="brand" href="/"><span class="mark">i</span> Integrated Software Technologies Inc.</a>
<span>© 2026 · Built on-device, in Glendale, CA · integratedsw.tech · <a href="/privacy/" style="color:inherit">Privacy</a></span></footer>
<script>addEventListener('scroll',function(){document.getElementById('nav').classList.toggle('scrolled',scrollY>30)})</script>
</body></html>`;
const svgApple = '<svg viewBox="0 0 24 24" fill="currentColor" width="15" height="15"><path d="M16 1c.1 1.2-.4 2.4-1.1 3.2-.8.9-2 1.6-3.2 1.5-.1-1.2.5-2.4 1.2-3.1C13.7 1.6 15 1 16 1zm3.6 16.4c-.6 1.4-.9 2-1.7 3.2-1.1 1.7-2.6 3.8-4.5 3.8-1.7 0-2.1-1.1-4.4-1.1s-2.8 1.1-4.4 1.1c-1.9 0-3.3-1.9-4.4-3.6C-1 16.4-.4 9.9 3 8c1.3-.7 2.6-.6 3.7-.6 1.2 0 2 .7 3.4.7 1.3 0 2.1-.7 3.6-.7 1.1 0 2.3.1 3.4.9-3 1.6-2.5 5.9.5 7.1z"/></svg>';
/* --------------------------------------------------------------- app pages */
function appPage(a) {
const id = idOf(a.store);
const canonical = `${ROOT}/apps/${a.slug}/`;
const og = `${ROOT}/assets/og/${a.slug}.png`;
const icon = a.icon
? `<img class="micon" src="/assets/icons/${a.icon}.png" alt="${esc(a.name)} icon">`
: `<div class="micon ph">📱</div>`;
const store = a.store
? `<a class="chip app" href="${a.store}" target="_blank" rel="noopener">${svgApple} View on the App Store</a>`
: `<span class="chip soon">${a.status === 'soon' ? 'In development' : 'In review'}</span>`;
const shots = a.key
? `<div class="mshots">` + Array.from({ length: a.shots || 5 }, (_, i) => i + 1).map(k =>
`<div class="device"><img class="shot" src="/assets/shots/${a.key}/${String(k).padStart(2, '0')}.png" alt="${esc(a.name)} screenshot ${k}" loading="lazy"></div>`).join('') + `</div>`
: '';
const meta = id
? `<div class="mmeta" id="meta"></div>
<script>(function(){window.__m=function(d){var r=(d.results||[])[0];if(!r)return;var b=[];
if(r.formattedPrice)b.push(r.formattedPrice);
if(r.userRatingCount>0)b.push('★ '+(r.averageUserRating||0).toFixed(1)+' ('+r.userRatingCount.toLocaleString()+')');
if(r.version)b.push('Version '+r.version);
if(r.currentVersionReleaseDate)b.push('Updated '+new Date(r.currentVersionReleaseDate).toLocaleDateString(undefined,{month:'short',day:'numeric',year:'numeric'}));
if(r.fileSizeBytes)b.push(Math.round(r.fileSizeBytes/1048576)+' MB');
if(r.minimumOsVersion)b.push('iOS '+r.minimumOsVersion+'+');
document.getElementById('meta').innerHTML=b.map(function(x){return '<span>'+x+'</span>'}).join('<i>·</i>');};
var s=document.createElement('script');s.src='https://itunes.apple.com/lookup?id=${id}&country=us&callback=__m';s.onerror=function(){};document.head.appendChild(s);})();</script>`
: '';
const ld = {
'@context': 'https://schema.org', '@type': 'SoftwareApplication', name: a.name,
applicationCategory: 'MobileApplication', operatingSystem: 'iOS', description: a.desc,
url: a.store || canonical, image: `${ROOT}/assets/icons/${a.icon}.png`,
offers: { '@type': 'Offer', price: '0', priceCurrency: 'USD' },
author: { '@type': 'Organization', name: 'Integrated Software Technologies Inc.', url: ROOT }
};
return head({ title: `${a.name} — ${a.cat} for iOS`, desc: a.desc, canonical, og }) +
`<main class="subpage" style="--a:${a.accent}">
<a class="backlink" href="/#apps">← All apps</a>
<div class="subhead"><span class="micwrap" style="--a:${a.accent}">${icon}</span>
<div><h1>${esc(a.name)}</h1><div class="cat" style="color:${a.accent}">${esc(a.cat)}</div></div></div>
<p class="mlong">${esc(a.long || a.desc)}</p>
<div class="mlinks">${store} <a class="chip" href="/apps/${a.slug}/privacy/">Privacy Policy</a></div>
${meta}
${shots}
</main>
<script type="application/ld+json">${JSON.stringify(ld)}</script>` + foot;
}
/* ----------------------------------------------------------- privacy pages */
/* Every App Store listing needs a working privacy policy URL, and only two
apps had one on this domain. One template, generated per app — the studio
stance (no accounts, no analytics, on-device) is the policy; apps that DO
have a third-party surface declare it via flags in apps.js (`ads:true`
for AdMob, as in Spinnit). The two original hand-written pages
(deck-privacy.html, spinnit-privacy.html) stay untouched so any listing
already pointing at them keeps working. */
function privacyPage(a) {
const canonical = `${ROOT}/apps/${a.slug}/privacy/`;
const ads = a.ads ? `
<h2>Advertising (Google AdMob)</h2>
<p>${esc(a.name)} is free and supported by ads served through <strong>Google AdMob</strong>.
To show ads, AdMob may collect information such as a device advertising identifier,
approximate location (derived from IP), and app-usage and interaction data, used to serve
and measure ads and to limit ad fraud.</p>
<ul>
<li>On first launch the app asks for permission to track (Apple's App Tracking
Transparency prompt). If you decline, ads still show — just less personalized.</li>
<li>Google's handling of this data is governed by the
<a href="https://policies.google.com/privacy">Google Privacy Policy</a>.</li>
<li>The in-app <strong>Remove Ads</strong> purchase removes ads and stops ad-related
data collection entirely.</li>
</ul>
<h2>Purchases</h2>
<p>Purchases are processed by Apple. We receive no payment details — Apple only tells the
app that the purchase succeeded.</p>` : '';
const shortLine = a.ads
? `${esc(a.name)} has no accounts and we run no servers. Your data stays on your device.
The only data collected comes from the Google AdMob ads that keep the app free —
and you can remove ads to stop that entirely.`
: `${esc(a.name)} collects nothing. It has no accounts, no analytics, and no
third-party trackers. Everything happens on your device.`;
return head({ title: `${a.name} — Privacy Policy`,
desc: `Privacy policy for ${a.name} by Integrated Software Technologies.`,
canonical, og: `${ROOT}/assets/og/${a.slug}.png` }) +
`<main class="subpage" style="--a:${a.accent}">
<a class="backlink" href="/apps/${a.slug}/">← ${esc(a.name)}</a>
<h1>${esc(a.name)} — Privacy Policy</h1>
<p class="mlong"><strong>Short version:</strong> ${shortLine}</p>
<h2>What ${esc(a.name)} stores on your device</h2>
<p>Your content and settings are stored <strong>on your device</strong> (and, if you use
iCloud, in your own private iCloud account through Apple). We never receive this
information and operate no servers that could collect it.</p>
<h2>What we don't do</h2>
<ul>
<li>We do <strong>not</strong> collect your name, email, contacts, or photos.</li>
<li>We do <strong>not</strong> run analytics or third-party trackers${a.ads ? ' beyond the advertising described below' : ''}.</li>
<li>We do <strong>not</strong> sell personal data.</li>
</ul>
<h2>Permissions</h2>
<p>If the app asks for a system permission (camera, local network, Face ID, photos…),
it is used only to power the feature in front of you, processed on-device, and never
transmitted to us.</p>
${ads}
<h2>Contact</h2>
<p>Questions? Email <a href="mailto:[email protected]">[email protected]</a>.</p>
<p class="mlong" style="opacity:.6">Last updated ${today}.</p>
</main>` + foot;
}
function privacyIndex() {
const canonical = `${ROOT}/privacy/`;
return head({ title: 'Privacy — Integrated Software Technologies',
desc: 'Privacy policies for every Integrated Software Technologies app: no accounts, no analytics, on-device.',
canonical, og: `${ROOT}/assets/og.png` }) +
`<main class="subpage">
<a class="backlink" href="/">← Home</a>
<h1>Privacy</h1>
<p class="mlong">Every app we ship is built the same way: no accounts, no analytics, no
third-party trackers, everything on your device. Each app's policy:</p>
<ul>
${APPS.map(a => ` <li><a href="/apps/${a.slug}/privacy/">${esc(a.name)}</a></li>`).join('\n')}
</ul>
</main>` + foot;
}
/* -------------------------------------------------------------- note pages */
function notePage(n) {
const canonical = `${ROOT}/notes/${n.slug}/`;
const dt = new Date(n.date).toLocaleDateString(undefined, { month: 'long', day: 'numeric', year: 'numeric' });
const ld = {
'@context': 'https://schema.org', '@type': 'BlogPosting', headline: n.title,
datePublished: n.date, description: n.dek, url: canonical, keywords: (n.tags || []).join(', '),
author: { '@type': 'Person', name: 'Matthew Mesropian' },
publisher: { '@type': 'Organization', name: 'Integrated Software Technologies Inc.' }
};
return head({ title: `${n.title} — Integrated Software Technologies`, desc: n.dek, canonical, og: `${ROOT}/assets/og.png` }) +
`<main class="subpage">
<a class="backlink" href="/#journal">← Journal</a>
<article class="prose"><h1>${esc(n.title)}</h1>
<div class="meta">${dt} · ${(n.tags || []).join(' · ')}</div>
${n.body}</article>
</main>
<script type="application/ld+json">${JSON.stringify(ld)}</script>` + foot;
}
/* -------------------------------------------------------------- press page */
function pressPage() {
const canonical = `${ROOT}/press/`;
const assets = [
{ n: 'App mark (512px)', img: '/assets/icon-512.png', dl: '/assets/icon-512.png' },
{ n: 'App mark (192px)', img: '/assets/icon-192.png', dl: '/assets/icon-192.png' },
{ n: 'Share card', img: '/assets/og.png', dl: '/assets/og.png' }
];
const swatches = [['Cyan', '#33e6d1'], ['Amber', '#fab84d'], ['Teal', '#248c85'], ['Emerald', '#34d399'], ['Ink', '#06070d']];
return head({ title: 'Press kit — Integrated Software Technologies', desc: 'Brand assets, logos, colors, and boilerplate for Integrated Software Technologies.', canonical, og: `${ROOT}/assets/og.png` }) +
`<main class="subpage">
<a class="backlink" href="/">← Home</a>
<article class="prose"><h1>Press kit</h1>
<div class="meta">Logos, colors, and boilerplate — free to use when writing about the studio or its apps.</div>
<p><b>Integrated Software Technologies Inc.</b> is a one-person iOS studio run by Matthew Mesropian in Glendale, CA. It designs and ships native, on-device apps — developer tools, network utilities, LiDAR measuring, and audio — with no accounts and no tracking.</p>
<p>Contact: <a href="mailto:[email protected]" style="color:var(--c2);font-weight:600">[email protected]</a> · (818) 671-9866</p>
</article>
<div class="pressgrid">
${assets.map(x => `<div class="asset"><img src="${x.img}" alt="${x.n}"><div class="an">${x.n}</div><a class="chip gh" href="${x.dl}" download>Download</a></div>`).join('')}
</div>
<div class="swatches">
${swatches.map(([n, c]) => `<span class="swatch"><i style="background:${c}"></i>${n} · ${c}</span>`).join('')}
</div>
</main>` + foot;
}
/* ---------------------------------------------------------------- 404 page */
function notFound() {
return head({ title: 'Not found — Integrated Software Technologies', desc: 'That page does not exist.', canonical: `${ROOT}/404`, og: `${ROOT}/assets/og.png` }) +
`<main class="subpage" style="text-align:center;padding-top:24vh">
<div class="prose" style="margin:0 auto"><h1>404</h1>
<div class="meta">That page wandered off-device.</div>
<p><a class="chip app" href="/" style="margin-top:6px">← Back home</a></p></div>
</main>` + foot;
}
/* ---------------------------------------------------- per-app OG images ---- */
function ogFor(a) {
const iconPath = a.icon && path.join(dir, 'assets', 'icons', a.icon + '.png');
let iconTag = '';
if (iconPath && fs.existsSync(iconPath)) {
const b64 = fs.readFileSync(iconPath).toString('base64');
iconTag = `<image x="96" y="86" width="150" height="150" href="data:image/png;base64,${b64}"/>`;
}
const full = (a.desc || '');
const desc = (full.length > 64 ? full.slice(0, 62).trimEnd() + '…' : full).replace(/&/g, '&');
const svg = `<svg xmlns="http://www.w3.org/2000/svg" width="1200" height="1200" viewBox="0 0 1200 630" preserveAspectRatio="xMidYMid meet">
<defs>
<linearGradient id="g" x1="0" y1="0" x2="1" y2="1"><stop offset="0" stop-color="${a.accent}"/><stop offset="1" stop-color="#ffffff"/></linearGradient>
<radialGradient id="glow" cx="0.9" cy="0.05" r="0.8"><stop offset="0" stop-color="${a.accent}" stop-opacity="0.32"/><stop offset="1" stop-color="${a.accent}" stop-opacity="0"/></radialGradient>
</defs>
<rect width="1200" height="630" fill="#06070d"/><rect width="1200" height="630" fill="url(#glow)"/>
${iconTag}
<text x="270" y="150" font-family="Helvetica Neue, Arial, sans-serif" font-size="30" font-weight="600" fill="#98a0b8" letter-spacing="2">${(a.cat || '').toUpperCase()}</text>
<text x="270" y="215" font-family="Helvetica Neue, Arial, sans-serif" font-size="66" font-weight="700" fill="#eaeefb">${esc(a.name)}</text>
<text x="96" y="360" font-family="Helvetica Neue, Arial, sans-serif" font-size="32" font-weight="500" fill="#cdd3e6">${esc(desc)}</text>
<rect x="96" y="470" width="${Math.min(560, 40 + a.name.length * 4)}" height="4" rx="2" fill="url(#g)"/>
<text x="96" y="545" font-family="Helvetica Neue, Arial, sans-serif" font-size="28" font-weight="700" fill="url(#g)">integratedsw.tech</text>
</svg>`;
const tmp = path.join('/tmp', 'ist_og_' + a.slug + '.svg');
fs.writeFileSync(tmp, svg);
const outDir = path.join(dir, 'assets', 'og'); mkdir(outDir);
const out = path.join(outDir, a.slug + '.png');
try {
execSync(`qlmanage -t -s 1200 -o /tmp "${tmp}" >/dev/null 2>&1`);
fs.copyFileSync(path.join('/tmp', path.basename(tmp) + '.png'), out);
execSync(`sips -c 630 1200 "${out}" >/dev/null 2>&1`);
return true;
} catch (e) { console.warn(' OG failed for', a.slug, e.message); return false; }
}
/* --------------------------------------------------------------- sitemap -- */
function sitemap() {
const urls = [`${ROOT}/`, `${ROOT}/press/`, `${ROOT}/privacy/`,
...APPS.map(a => `${ROOT}/apps/${a.slug}/`),
...APPS.map(a => `${ROOT}/apps/${a.slug}/privacy/`),
...NOTES.map(n => `${ROOT}/notes/${n.slug}/`)];
return `<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
${urls.map(u => ` <url><loc>${u}</loc><lastmod>${today}</lastmod></url>`).join('\n')}
</urlset>
`;
}
/* -------------------------------------------------------- OG compression -- */
/* qlmanage emits ~370 KB truecolor PNGs of what is mostly flat gradient — and
link-preview crawlers fetch these on every share. Palette-quantize with
dithering via Pillow (~4-6x smaller, no visible banding on this art). Silent
no-op if the Homebrew Python/Pillow pair isn't there. */
const PYTHON = '/opt/homebrew/opt/[email protected]/bin/python3.12';
function compressPNGs(files) {
const existing = files.filter(f => fs.existsSync(f));
if (!existing.length || !fs.existsSync(PYTHON)) return;
const script = `
import sys
from PIL import Image
for p in sys.argv[1:]:
im = Image.open(p).convert("RGBA")
q = im.quantize(colors=256, method=2, dither=Image.Dither.FLOYDSTEINBERG)
q.save(p, optimize=True)
`;
const tmp = path.join('/tmp', 'ist_pngq.py');
fs.writeFileSync(tmp, script);
try {
execSync(`"${PYTHON}" "${tmp}" ${existing.map(f => `"${f}"`).join(' ')}`,
{ stdio: 'ignore' });
console.log(` compressed ${existing.length} OG images`);
} catch (e) { console.warn(' OG compression skipped:', e.message); }
}
/* ------------------------------------------------------- ratings snapshot -- */
/* Bake the App Store ratings into assets/ratings.js at build time. The browser
still asks the live lookup API (and overwrites this), but if Apple retires
that endpoint the way it retired the reviews feed, the site keeps showing
the last-built numbers instead of silently showing none. Offline builds
keep the previous snapshot. */
async function ratingsSnapshot() {
const ids = APPS.map(a => idOf(a.store)).filter(Boolean);
if (!ids.length) return;
try {
const res = await fetch(`https://itunes.apple.com/lookup?id=${ids.join(',')}&country=us`);
if (!res.ok) throw new Error('HTTP ' + res.status);
const data = await res.json();
if (!data.results || !data.results.length) throw new Error('empty result');
write('assets/ratings.js',
`/* Build-time App Store snapshot (${today}) — regenerated by build.js.\n`
+ ` Seeds the rating badges; the live lookup call overwrites it. */\n`
+ `window.__RATINGS_SNAPSHOT = ${JSON.stringify({ results: data.results })};\n`);
console.log(` ratings snapshot: ${data.results.length} apps`);
} catch (e) {
console.warn(' ratings snapshot skipped (' + e.message + ') — keeping the previous one');
if (!fs.existsSync(path.join(dir, 'assets', 'ratings.js'))) {
write('assets/ratings.js', 'window.__RATINGS_SNAPSHOT = null;\n');
}
}
}
/* ------------------------------------------------------------------- main -- */
(async () => {
console.log('Generating…');
let n = 0;
APPS.forEach(a => { write(`apps/${a.slug}/index.html`, appPage(a)); ogFor(a); n++; });
console.log(` ${n} app pages + OG images`);
APPS.forEach(a => write(`apps/${a.slug}/privacy/index.html`, privacyPage(a)));
write('privacy/index.html', privacyIndex());
console.log(` ${n} privacy pages + /privacy/ hub`);
NOTES.forEach(x => write(`notes/${x.slug}/index.html`, notePage(x)));
console.log(` ${NOTES.length} note pages`);
write('press/index.html', pressPage());
write('404.html', notFound());
write('sitemap.xml', sitemap());
write('robots.txt', `User-agent: *\nAllow: /\n\nSitemap: ${ROOT}/sitemap.xml\n`);
console.log(' press, 404, sitemap.xml, robots.txt');
compressPNGs([...APPS.map(a => path.join(dir, 'assets', 'og', a.slug + '.png')),
path.join(dir, 'assets', 'og.png')]);
await ratingsSnapshot();
console.log('Done.');
})();