Repository navigation
Expand file tree
/
Copy pathbuild.js
More file actions
173 lines (158 loc) · 6.19 KB
/
Copy pathbuild.js
File metadata and controls
173 lines (158 loc) · 6.19 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
#!/usr/bin/env node
/**
* Build task for the design concepts index.
*
* Scans every top-level folder for an index.html, extracts the concept
* name from its <title> (and an optional <meta name="description">),
* then writes a root index.html linking to each concept.
*
* Run with: pnpm build (or node build.js)
* No dependencies. The served site lives in public/ so the repo tooling
* (package.json, build.js, README) is never deployed. On Cloudflare Pages,
* set the build command to "pnpm build" and the output directory to "public".
*/
const fs = require('fs');
const path = require('path');
const ROOT = path.join(__dirname, 'public');
// Folders that are never design concepts.
const IGNORED_DIRS = new Set(['node_modules', 'assets', 'scripts']);
function listConceptDirs() {
return fs.readdirSync(ROOT, { withFileTypes: true })
.filter((entry) =>
entry.isDirectory() &&
!entry.name.startsWith('.') &&
!IGNORED_DIRS.has(entry.name) &&
fs.existsSync(path.join(ROOT, entry.name, 'index.html')))
.map((entry) => entry.name)
.sort();
}
function extractMeta(dir) {
const html = fs.readFileSync(path.join(ROOT, dir, 'index.html'), 'utf8');
const titleMatch = html.match(/<title>([^<]*)<\/title>/i);
const name = (titleMatch ? titleMatch[1].trim() : '') || dir;
const descMatch = html.match(
/<meta\s+name=["']description["']\s+content=["']([^"']*)["']/i);
const description = descMatch ? descMatch[1].trim() : '';
return { dir, name, description };
}
function escapeHtml(text) {
return text
.replace(/&/g, '&')
.replace(/</g, '<')
.replace(/>/g, '>')
.replace(/"/g, '"');
}
function renderIndex(concepts) {
const cards = concepts.map(({ dir, name, description }) => `
<li>
<a class="card" href="${escapeHtml(dir)}/">
<h2>${escapeHtml(name)}</h2>
${description ? `<p>${escapeHtml(description)}</p>` : ''}
<span class="path">${escapeHtml(dir)}/</span>
</a>
</li>`).join('');
// Colors come from the Open Home Foundation site stylesheet:
// dark blue #09293b, light blue #18bcf2, light grey #edeced,
// dark grey #595d66, red #dc5956. Display font is Black Han Sans.
return `<!doctype html>
<!-- Generated by build.js. Do not edit by hand, run "node build.js" instead. -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width,initial-scale=1">
<meta name="robots" content="noindex, nofollow">
<title>Design concepts · Open Home Foundation</title>
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Black+Han+Sans&display=swap">
<style>
:root {
--dark-blue: #09293b;
--light-blue: #18bcf2;
--light-grey: #edeced;
--dark-grey: #595d66;
--red: #dc5956;
--white: #fff;
}
* { box-sizing: border-box; }
body {
margin: 0; background: var(--white); color: var(--dark-blue);
font: 17px/1.6 -apple-system, BlinkMacSystemFont, "Segoe UI", "Helvetica Neue", sans-serif;
}
header {
background: var(--white); padding: 28px 24px;
border-bottom: 1px solid var(--light-grey);
}
header .inner { max-width: 760px; margin: 0 auto; }
header img { height: 40px; width: auto; display: block; }
main { max-width: 760px; margin: 0 auto; padding: 48px 24px 64px; }
h1 {
font-family: "Black Han Sans", -apple-system, sans-serif;
font-weight: 400; font-size: 40px; line-height: 1.15;
margin: 0 0 12px; color: var(--dark-blue);
}
.intro { margin: 0 0 24px; color: var(--dark-grey); max-width: 60ch; }
.warning {
border-left: 6px solid var(--red);
background: #fdf1f0;
color: var(--dark-blue);
padding: 16px 20px; border-radius: 0 8px 8px 0;
margin: 0 0 40px;
}
.warning strong { color: var(--red); display: block; margin-bottom: 4px; }
ul { list-style: none; margin: 0; padding: 0; display: grid; gap: 16px; }
.card {
display: block; padding: 24px; border-radius: 12px;
background: var(--light-grey); border: 2px solid transparent;
text-decoration: none; color: var(--dark-blue);
transition: border-color 120ms ease, transform 120ms ease;
}
.card:hover { border-color: var(--light-blue); transform: translateY(-2px); }
.card h2 {
font-family: "Black Han Sans", -apple-system, sans-serif;
font-weight: 400; font-size: 22px; margin: 0 0 6px;
}
.card p { margin: 0 0 10px; color: var(--dark-grey); }
.path {
font: 13px/1 "SF Mono", Monaco, Inconsolata, "Fira Mono", monospace;
color: var(--light-blue);
}
footer {
background: var(--dark-blue); color: var(--light-grey);
padding: 32px 24px; font-size: 14px;
}
footer .inner { max-width: 760px; margin: 0 auto; }
footer a { color: var(--light-blue); text-decoration: none; }
</style>
</head>
<body>
<header>
<div class="inner">
<img src="assets/open-home-foundation.svg" alt="Open Home Foundation">
</div>
</header>
<main>
<h1>Design concepts</h1>
<p class="intro">This is the home of design concepts for the Open Home Foundation. Each folder is a self contained exploration of an idea.</p>
<div class="warning">
<strong>These are design concepts, not products</strong>
Nothing here is production software. Data is fictional, features are mockups and nothing you see is final or committed. Do not rely on anything on this site.
</div>
<ul>${cards}
</ul>
</main>
<footer>
<div class="inner">Internal design explorations by the <a href="https://www.openhomefoundation.org/">Open Home Foundation</a>. Not indexed by search engines.</div>
</footer>
</body>
</html>
`;
}
const concepts = listConceptDirs().map(extractMeta);
if (concepts.length === 0) {
console.error('No concept folders found (a concept is a folder in public/ with an index.html).');
process.exit(1);
}
fs.writeFileSync(path.join(ROOT, 'index.html'), renderIndex(concepts));
console.log(`Generated index.html with ${concepts.length} concept(s):`);
for (const c of concepts) console.log(` ${c.dir}/ -> ${c.name}`);