@micropage-sh/cli 2.0.2 → 2.0.8
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/commands/builds.js +18 -0
- package/src/commands/links.js +4 -10
- package/src/commands/projects.js +57 -16
- package/src/config.js +27 -1
- package/src/index.js +2 -1
- package/src/parser.js +3 -7
- package/src/supabase.js +73 -12
- package/templates/assets/favicons/favicon-a.svg +7 -0
- package/templates/assets/favicons/favicon-b.svg +12 -0
- package/templates/assets/favicons/favicon-c.svg +8 -0
- package/templates/assets/favicons/favicon-d.svg +6 -0
- package/templates/assets/logos/logo-a.svg +13 -0
- package/templates/assets/logos/logo-b.svg +16 -0
- package/templates/assets/logos/logo-c.svg +9 -0
- package/templates/assets/logos/logo-d.svg +7 -0
- package/templates/examples/agency-portfolio.page +130 -0
- package/templates/examples/blog-publication.page +158 -0
- package/templates/examples/devtool-docs.page +181 -0
- package/templates/examples/event-conference.page +171 -0
- package/templates/examples/restaurant-landing.page +124 -0
package/package.json
CHANGED
package/src/commands/builds.js
CHANGED
|
@@ -11,6 +11,7 @@ const {
|
|
|
11
11
|
getValidAccessToken,
|
|
12
12
|
getMaxDeployEventIdForBuild,
|
|
13
13
|
streamDeployEventsUntilDone,
|
|
14
|
+
uploadAssetsWithToken,
|
|
14
15
|
} = require('../supabase');
|
|
15
16
|
const { getProjectConfig, setProjectConfig } = require('../auth');
|
|
16
17
|
const { readPageFilesFromDir } = require('../parser');
|
|
@@ -139,6 +140,23 @@ async function push(options = {}) {
|
|
|
139
140
|
process.exit(1);
|
|
140
141
|
}
|
|
141
142
|
|
|
143
|
+
// Upload new files in assets/ to project Files before parsing,
|
|
144
|
+
// so `img: <- name` lookups can resolve against just-uploaded files.
|
|
145
|
+
try {
|
|
146
|
+
const accessToken = await getValidAccessToken();
|
|
147
|
+
const uploaded = await uploadAssetsWithToken(
|
|
148
|
+
accessToken,
|
|
149
|
+
config.projectId,
|
|
150
|
+
cwd,
|
|
151
|
+
(filename) => console.log(`Uploaded asset: ${filename}`),
|
|
152
|
+
);
|
|
153
|
+
if (uploaded > 0) console.log(`Uploaded ${uploaded} asset(s).`);
|
|
154
|
+
} catch (err) {
|
|
155
|
+
handleAuthError(err);
|
|
156
|
+
console.error('Asset upload failed:', err.message);
|
|
157
|
+
process.exit(1);
|
|
158
|
+
}
|
|
159
|
+
|
|
142
160
|
// Parse content via parser API
|
|
143
161
|
console.log('Parsing content…');
|
|
144
162
|
let jsonContent;
|
package/src/commands/links.js
CHANGED
|
@@ -3,13 +3,7 @@
|
|
|
3
3
|
const { execSync } = require('child_process');
|
|
4
4
|
const { db, handleAuthError } = require('../supabase');
|
|
5
5
|
const { getProjectConfig } = require('../auth');
|
|
6
|
-
const { APP_URL } = require('../config');
|
|
7
|
-
|
|
8
|
-
function projectUrl(domain) {
|
|
9
|
-
if (!domain) return null;
|
|
10
|
-
if (domain.startsWith('http')) return domain;
|
|
11
|
-
return `https://${domain}.pages.dev`;
|
|
12
|
-
}
|
|
6
|
+
const { APP_URL, projectUrl } = require('../config');
|
|
13
7
|
|
|
14
8
|
function copyToClipboard(text) {
|
|
15
9
|
const cmds = [
|
|
@@ -41,7 +35,7 @@ async function getProject(cwd) {
|
|
|
41
35
|
try {
|
|
42
36
|
const project = await db
|
|
43
37
|
.from('projects')
|
|
44
|
-
.select('id,name,domain')
|
|
38
|
+
.select('id,name,domain,custom_domain')
|
|
45
39
|
.eq('id', config.projectId)
|
|
46
40
|
.single();
|
|
47
41
|
return project || config;
|
|
@@ -56,7 +50,7 @@ async function getProject(cwd) {
|
|
|
56
50
|
async function preview() {
|
|
57
51
|
const cwd = process.cwd();
|
|
58
52
|
const project = await getProject(cwd);
|
|
59
|
-
const url = projectUrl(project.domain);
|
|
53
|
+
const url = projectUrl(project.domain, project.custom_domain);
|
|
60
54
|
|
|
61
55
|
if (!url) {
|
|
62
56
|
console.error('No domain set for this project. Publish first.');
|
|
@@ -76,7 +70,7 @@ async function preview() {
|
|
|
76
70
|
async function copyLink() {
|
|
77
71
|
const cwd = process.cwd();
|
|
78
72
|
const project = await getProject(cwd);
|
|
79
|
-
const url = projectUrl(project.domain);
|
|
73
|
+
const url = projectUrl(project.domain, project.custom_domain);
|
|
80
74
|
|
|
81
75
|
if (!url) {
|
|
82
76
|
console.error('No domain set for this project. Publish first.');
|
package/src/commands/projects.js
CHANGED
|
@@ -23,11 +23,9 @@ const { syncAssets } = require('./files');
|
|
|
23
23
|
// Helpers
|
|
24
24
|
// ---------------------------------------------------------------------------
|
|
25
25
|
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
return `https://${host}`;
|
|
30
|
-
}
|
|
26
|
+
const { projectUrl: buildProjectUrl } = require('../config');
|
|
27
|
+
const projectUrl = (slug, customDomain) =>
|
|
28
|
+
buildProjectUrl(slug, customDomain) || '-';
|
|
31
29
|
|
|
32
30
|
function copyIfExists(src, dest) {
|
|
33
31
|
try {
|
|
@@ -49,26 +47,69 @@ function copyExamplesAndAssetsIntoProject(dir) {
|
|
|
49
47
|
fs.mkdirSync(examplesDest, { recursive: true });
|
|
50
48
|
}
|
|
51
49
|
|
|
52
|
-
//
|
|
50
|
+
// Collect full-page templates (exclude components-* demo files) for the starter pick.
|
|
51
|
+
// All examples are still copied into examples/ for the user to browse.
|
|
52
|
+
const allExampleFiles = [];
|
|
53
|
+
const starterCandidates = [];
|
|
53
54
|
try {
|
|
54
55
|
const files = fs.readdirSync(examplesSrc);
|
|
55
56
|
for (const file of files) {
|
|
56
57
|
if (!file.endsWith('.page')) continue;
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
58
|
+
allExampleFiles.push(file);
|
|
59
|
+
if (!file.startsWith('components-')) {
|
|
60
|
+
starterCandidates.push(file);
|
|
61
|
+
}
|
|
60
62
|
}
|
|
61
63
|
} catch {
|
|
62
64
|
// ignore if templates/examples missing in this build
|
|
63
65
|
}
|
|
64
66
|
|
|
65
|
-
|
|
67
|
+
for (const file of allExampleFiles) {
|
|
68
|
+
const src = path.join(examplesSrc, file);
|
|
69
|
+
const dest = path.join(examplesDest, file);
|
|
70
|
+
copyIfExists(src, dest);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Write a randomized starter landing.page at the project root.
|
|
74
|
+
if (starterCandidates.length > 0) {
|
|
75
|
+
const pick = starterCandidates[Math.floor(Math.random() * starterCandidates.length)];
|
|
76
|
+
copyIfExists(path.join(examplesSrc, pick), path.join(dir, 'landing.page'));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Logo / favicon in assets/ — pick a random variant pair from logos/ and favicons/.
|
|
80
|
+
// Falls back to the root logo.svg / favicon.svg if the variant directories are absent or empty.
|
|
66
81
|
const destAssets = path.join(dir, 'assets');
|
|
67
82
|
if (!fs.existsSync(destAssets)) {
|
|
68
83
|
fs.mkdirSync(destAssets, { recursive: true });
|
|
69
84
|
}
|
|
70
|
-
|
|
71
|
-
|
|
85
|
+
|
|
86
|
+
const logosDir = path.join(assetsSrc, 'logos');
|
|
87
|
+
const faviconsDir = path.join(assetsSrc, 'favicons');
|
|
88
|
+
let usedVariant = false;
|
|
89
|
+
|
|
90
|
+
try {
|
|
91
|
+
if (fs.existsSync(logosDir)) {
|
|
92
|
+
const logoFiles = fs.readdirSync(logosDir).filter((f) => /^logo-[^.]+\.svg$/.test(f));
|
|
93
|
+
if (logoFiles.length > 0) {
|
|
94
|
+
const chosen = logoFiles[Math.floor(Math.random() * logoFiles.length)];
|
|
95
|
+
// logo-x.svg -> favicon-x.svg
|
|
96
|
+
const variantId = chosen.replace(/^logo-/, '').replace(/\.svg$/, '');
|
|
97
|
+
const matchingFavicon = path.join(faviconsDir, `favicon-${variantId}.svg`);
|
|
98
|
+
if (fs.existsSync(matchingFavicon)) {
|
|
99
|
+
copyIfExists(path.join(logosDir, chosen), path.join(destAssets, 'logo.svg'));
|
|
100
|
+
copyIfExists(matchingFavicon, path.join(destAssets, 'favicon.svg'));
|
|
101
|
+
usedVariant = true;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
// fall through to default below
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (!usedVariant) {
|
|
110
|
+
copyIfExists(path.join(assetsSrc, 'logo.svg'), path.join(destAssets, 'logo.svg'));
|
|
111
|
+
copyIfExists(path.join(assetsSrc, 'favicon.svg'), path.join(destAssets, 'favicon.svg'));
|
|
112
|
+
}
|
|
72
113
|
}
|
|
73
114
|
|
|
74
115
|
async function getLatestBuild(projectId) {
|
|
@@ -145,7 +186,7 @@ async function list(options = {}) {
|
|
|
145
186
|
|
|
146
187
|
const rows = projects.map((p) => {
|
|
147
188
|
const build = latestBuild[p.id];
|
|
148
|
-
const url = projectUrl(p.
|
|
189
|
+
const url = projectUrl(p.domain, p.custom_domain);
|
|
149
190
|
return [
|
|
150
191
|
p.uuid || '-',
|
|
151
192
|
p.name || '-',
|
|
@@ -195,7 +236,7 @@ async function show(uuidOrDomain, options = {}) {
|
|
|
195
236
|
return;
|
|
196
237
|
}
|
|
197
238
|
|
|
198
|
-
const url = projectUrl(project.
|
|
239
|
+
const url = projectUrl(project.domain, project.custom_domain);
|
|
199
240
|
|
|
200
241
|
console.log('UUID: ', project.uuid || '-');
|
|
201
242
|
console.log('Name: ', project.name || '-');
|
|
@@ -261,9 +302,9 @@ async function create(name, options = {}) {
|
|
|
261
302
|
|
|
262
303
|
console.log('Created project:', project.name || name);
|
|
263
304
|
if (project.uuid) console.log('UUID:', project.uuid);
|
|
264
|
-
console.log('URL:', projectUrl(project.domain));
|
|
305
|
+
console.log('URL:', projectUrl(project.domain, project.custom_domain));
|
|
265
306
|
console.log('Folder:', dir);
|
|
266
|
-
console.log(`\nNext: cd ${name} &&
|
|
307
|
+
console.log(`\nNext: cd ${name} && micropage push`);
|
|
267
308
|
}
|
|
268
309
|
|
|
269
310
|
// ---------------------------------------------------------------------------
|
package/src/config.js
CHANGED
|
@@ -19,4 +19,30 @@ const BUILD_COMPILER_URL =
|
|
|
19
19
|
process.env.MICROPAGE_PARSER_URL ||
|
|
20
20
|
'https://build-compiler.micropage.sh';
|
|
21
21
|
|
|
22
|
-
|
|
22
|
+
const BASE_DOMAIN = process.env.MICROPAGE_BASE_DOMAIN || 'micropage.sh';
|
|
23
|
+
|
|
24
|
+
// Live URL for a project. Prefers the customer's custom domain (a full FQDN
|
|
25
|
+
// like `www.example.com`) when set; falls back to `https://<slug>.<BASE_DOMAIN>`.
|
|
26
|
+
// Returns null when there's no usable identifier.
|
|
27
|
+
function projectUrl(slug, customDomain) {
|
|
28
|
+
const cd = customDomain && String(customDomain).trim();
|
|
29
|
+
if (cd) {
|
|
30
|
+
return cd.startsWith('http://') || cd.startsWith('https://')
|
|
31
|
+
? cd
|
|
32
|
+
: `https://${cd}`;
|
|
33
|
+
}
|
|
34
|
+
if (!slug) return null;
|
|
35
|
+
const s = String(slug).trim();
|
|
36
|
+
if (!s) return null;
|
|
37
|
+
if (s.startsWith('http://') || s.startsWith('https://')) return s;
|
|
38
|
+
return `https://${s}.${BASE_DOMAIN}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
module.exports = {
|
|
42
|
+
SUPABASE_URL,
|
|
43
|
+
SUPABASE_ANON_KEY,
|
|
44
|
+
APP_URL,
|
|
45
|
+
BUILD_COMPILER_URL,
|
|
46
|
+
BASE_DOMAIN,
|
|
47
|
+
projectUrl,
|
|
48
|
+
};
|
package/src/index.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
4
|
const { Command } = require('commander');
|
|
5
|
+
const { version } = require('../package.json');
|
|
5
6
|
|
|
6
7
|
const login = require('./commands/login');
|
|
7
8
|
const logout = require('./commands/logout');
|
|
@@ -18,7 +19,7 @@ const program = new Command();
|
|
|
18
19
|
program
|
|
19
20
|
.name('micropage')
|
|
20
21
|
.description('CLI for micropage.sh – create, sync, and publish microsites')
|
|
21
|
-
.version(
|
|
22
|
+
.version(version);
|
|
22
23
|
|
|
23
24
|
// ---------------------------------------------------------------------------
|
|
24
25
|
// Authentication
|
package/src/parser.js
CHANGED
|
@@ -48,18 +48,15 @@ function writePageFile(dir, content) {
|
|
|
48
48
|
return dest;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
-
const ASSET_EXTENSIONS = ['.png', '.jpg', '.jpeg', '.gif', '.webp', '.ico', '.svg'];
|
|
52
|
-
|
|
53
51
|
function listAssetsFromDir(dir) {
|
|
54
52
|
const assetsDir = path.join(dir, 'assets');
|
|
55
53
|
if (!fs.existsSync(assetsDir)) return [];
|
|
56
54
|
const entries = fs.readdirSync(assetsDir, { withFileTypes: true });
|
|
57
55
|
const files = [];
|
|
58
56
|
for (const e of entries) {
|
|
59
|
-
if (e.isFile())
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
57
|
+
if (!e.isFile()) continue;
|
|
58
|
+
if (e.name.startsWith('.')) continue;
|
|
59
|
+
files.push(e.name);
|
|
63
60
|
}
|
|
64
61
|
return files.sort();
|
|
65
62
|
}
|
|
@@ -68,5 +65,4 @@ module.exports = {
|
|
|
68
65
|
readPageFilesFromDir,
|
|
69
66
|
writePageFile,
|
|
70
67
|
listAssetsFromDir,
|
|
71
|
-
ASSET_EXTENSIONS,
|
|
72
68
|
};
|
package/src/supabase.js
CHANGED
|
@@ -257,13 +257,23 @@ const db = {
|
|
|
257
257
|
// ---------------------------------------------------------------------------
|
|
258
258
|
|
|
259
259
|
async function getUserInfo(accessToken) {
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
260
|
+
let res;
|
|
261
|
+
try {
|
|
262
|
+
res = await fetch(`${SUPABASE_URL}/auth/v1/user`, {
|
|
263
|
+
headers: {
|
|
264
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
265
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
266
|
+
},
|
|
267
|
+
});
|
|
268
|
+
} catch (err) {
|
|
269
|
+
console.error(`[micropage] getUserInfo network error: ${err.message}`);
|
|
270
|
+
return null;
|
|
271
|
+
}
|
|
272
|
+
if (!res.ok) {
|
|
273
|
+
const body = await res.text().catch(() => '');
|
|
274
|
+
console.error(`[micropage] getUserInfo failed (HTTP ${res.status} from ${SUPABASE_URL}/auth/v1/user): ${body}`);
|
|
275
|
+
return null;
|
|
276
|
+
}
|
|
267
277
|
return res.json();
|
|
268
278
|
}
|
|
269
279
|
|
|
@@ -427,6 +437,12 @@ async function streamDeployEventsUntilDone(bearerToken, projectId, buildId, opti
|
|
|
427
437
|
return { terminalEvent };
|
|
428
438
|
}
|
|
429
439
|
|
|
440
|
+
function hashFile(filePath) {
|
|
441
|
+
const crypto = require('crypto');
|
|
442
|
+
const fs = require('fs');
|
|
443
|
+
return crypto.createHash('sha256').update(fs.readFileSync(filePath)).digest('hex');
|
|
444
|
+
}
|
|
445
|
+
|
|
430
446
|
/**
|
|
431
447
|
* Upload a single asset file to the project via the upload-file Edge function.
|
|
432
448
|
* Uses a pre-obtained Supabase access token.
|
|
@@ -437,10 +453,12 @@ async function uploadAssetWithToken(accessToken, projectId, filePath, filename)
|
|
|
437
453
|
|
|
438
454
|
const bytes = fs.readFileSync(filePath);
|
|
439
455
|
const mimeType = mime.fromFilename(filename);
|
|
456
|
+
const sha = hashFile(filePath);
|
|
440
457
|
|
|
441
458
|
const formData = new FormData();
|
|
442
459
|
formData.append('file', new Blob([bytes], { type: mimeType }), filename);
|
|
443
460
|
formData.append('project_id', String(projectId));
|
|
461
|
+
formData.append('content_hash', sha);
|
|
444
462
|
|
|
445
463
|
const res = await fetch(`${SUPABASE_URL}/functions/v1/upload-file`, {
|
|
446
464
|
method: 'POST',
|
|
@@ -463,9 +481,33 @@ async function uploadAssetWithToken(accessToken, projectId, filePath, filename)
|
|
|
463
481
|
return data;
|
|
464
482
|
}
|
|
465
483
|
|
|
484
|
+
async function deleteFileWithToken(accessToken, fileId) {
|
|
485
|
+
const res = await fetch(`${SUPABASE_URL}/functions/v1/delete-file`, {
|
|
486
|
+
method: 'POST',
|
|
487
|
+
headers: {
|
|
488
|
+
'apikey': SUPABASE_ANON_KEY,
|
|
489
|
+
'Authorization': `Bearer ${accessToken}`,
|
|
490
|
+
'Content-Type': 'application/json',
|
|
491
|
+
},
|
|
492
|
+
body: JSON.stringify({ file_id: fileId }),
|
|
493
|
+
});
|
|
494
|
+
const text = await res.text();
|
|
495
|
+
let data = null;
|
|
496
|
+
try { data = text ? JSON.parse(text) : null; } catch { data = { raw: text }; }
|
|
497
|
+
if (!res.ok) {
|
|
498
|
+
const msg = (data && (data.error || data.message)) || `HTTP ${res.status}`;
|
|
499
|
+
const err = new Error(msg);
|
|
500
|
+
err.status = res.status;
|
|
501
|
+
throw err;
|
|
502
|
+
}
|
|
503
|
+
return data;
|
|
504
|
+
}
|
|
505
|
+
|
|
466
506
|
/**
|
|
467
507
|
* Upload all assets from the local `assets/` directory for a project.
|
|
468
|
-
*
|
|
508
|
+
* Compares each local file's SHA-256 hash against the server's stored hash.
|
|
509
|
+
* Skips files whose content is unchanged. Deletes and re-uploads files whose
|
|
510
|
+
* content has changed (or whose server hash is NULL from before hashing was added).
|
|
469
511
|
* Returns the count of files uploaded.
|
|
470
512
|
*/
|
|
471
513
|
async function uploadAssetsWithToken(accessToken, projectId, cwd, onProgress) {
|
|
@@ -475,7 +517,9 @@ async function uploadAssetsWithToken(accessToken, projectId, cwd, onProgress) {
|
|
|
475
517
|
const assetFiles = listAssetsFromDir(cwd);
|
|
476
518
|
if (assetFiles.length === 0) return 0;
|
|
477
519
|
|
|
478
|
-
// Fetch existing
|
|
520
|
+
// Fetch existing files so we can skip unchanged ones. We MUST treat a
|
|
521
|
+
// non-OK response as fatal: continuing with an empty list would re-upload
|
|
522
|
+
// every asset and (when filenames collide) leave duplicate user_files rows.
|
|
479
523
|
const listRes = await fetch(
|
|
480
524
|
`${SUPABASE_URL}/functions/v1/list-files?project_id=${encodeURIComponent(projectId)}`,
|
|
481
525
|
{
|
|
@@ -485,13 +529,28 @@ async function uploadAssetsWithToken(accessToken, projectId, cwd, onProgress) {
|
|
|
485
529
|
},
|
|
486
530
|
},
|
|
487
531
|
);
|
|
488
|
-
|
|
489
|
-
|
|
532
|
+
if (!listRes.ok) {
|
|
533
|
+
const text = await listRes.text().catch(() => '');
|
|
534
|
+
const err = new Error(`Failed to list project files: HTTP ${listRes.status}${text ? ` — ${text}` : ''}`);
|
|
535
|
+
err.status = listRes.status;
|
|
536
|
+
throw err;
|
|
537
|
+
}
|
|
538
|
+
const listData = await listRes.json();
|
|
539
|
+
const existingByName = new Map(
|
|
540
|
+
(listData?.files || []).map((f) => [f.filename, { id: f.id, content_hash: f.content_hash }]),
|
|
541
|
+
);
|
|
490
542
|
|
|
491
543
|
let uploaded = 0;
|
|
492
544
|
for (const filename of assetFiles) {
|
|
493
|
-
if (existing.has(filename)) continue;
|
|
494
545
|
const filePath = path.join(cwd, 'assets', filename);
|
|
546
|
+
const localHash = hashFile(filePath);
|
|
547
|
+
const existing = existingByName.get(filename);
|
|
548
|
+
|
|
549
|
+
if (existing && existing.content_hash === localHash) continue;
|
|
550
|
+
|
|
551
|
+
if (existing) {
|
|
552
|
+
await deleteFileWithToken(accessToken, existing.id);
|
|
553
|
+
}
|
|
495
554
|
await uploadAssetWithToken(accessToken, projectId, filePath, filename);
|
|
496
555
|
if (onProgress) onProgress(filename);
|
|
497
556
|
uploaded++;
|
|
@@ -634,4 +693,6 @@ module.exports = {
|
|
|
634
693
|
exchangeDeployTokenForAccessToken,
|
|
635
694
|
pushWithToken,
|
|
636
695
|
invokePublishBuild,
|
|
696
|
+
uploadAssetsWithToken,
|
|
697
|
+
hashFile,
|
|
637
698
|
};
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage favicon — variant A</title>
|
|
3
|
+
<desc>Minimal stacked bar favicon for starter projects</desc>
|
|
4
|
+
<rect x="4" y="7" width="24" height="4" rx="2" fill="#4f46e5"/>
|
|
5
|
+
<rect x="6" y="14" width="20" height="4" rx="2" fill="#0ea5e9"/>
|
|
6
|
+
<rect x="8" y="21" width="16" height="4" rx="2" fill="#22c55e"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage favicon — variant B</title>
|
|
3
|
+
<desc>Minimal sunrise arc favicon</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="g" x1="0%" y1="100%" x2="100%" y2="0%">
|
|
6
|
+
<stop offset="0%" stop-color="#dc2626"/>
|
|
7
|
+
<stop offset="100%" stop-color="#f59e0b"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<path d="M 4 22 A 12 12 0 0 1 28 22" stroke="url(#g)" stroke-width="5" fill="none" stroke-linecap="round"/>
|
|
11
|
+
<circle cx="16" cy="22" r="3" fill="#dc2626"/>
|
|
12
|
+
</svg>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage favicon — variant C</title>
|
|
3
|
+
<desc>Minimal concentric circles favicon in forest green</desc>
|
|
4
|
+
<circle cx="16" cy="14" r="10" fill="#166534"/>
|
|
5
|
+
<circle cx="16" cy="14" r="5" fill="#14532d"/>
|
|
6
|
+
<circle cx="16" cy="14" r="2" fill="#bbf7d0"/>
|
|
7
|
+
<rect x="14" y="24" width="4" height="6" rx="2" fill="#713f12"/>
|
|
8
|
+
</svg>
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 32 32" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage favicon — variant D</title>
|
|
3
|
+
<desc>Minimal diamond favicon in charcoal with magenta center</desc>
|
|
4
|
+
<polygon points="16,4 28,16 16,28 4,16" fill="#1c1c1c"/>
|
|
5
|
+
<polygon points="16,11 21,16 16,21 11,16" fill="#e91e8c"/>
|
|
6
|
+
</svg>
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage starter logo — variant A</title>
|
|
3
|
+
<desc>Abstract stacked rectangles suggesting pages</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="g" x1="0%" y1="0%" x2="100%" y2="100%">
|
|
6
|
+
<stop offset="0%" stop-color="#4f46e5"/>
|
|
7
|
+
<stop offset="100%" stop-color="#06b6d4"/>
|
|
8
|
+
</linearGradient>
|
|
9
|
+
</defs>
|
|
10
|
+
<rect x="10" y="14" width="44" height="12" rx="3" fill="url(#g)"/>
|
|
11
|
+
<rect x="14" y="28" width="36" height="10" rx="3" fill="#111827" opacity="0.9"/>
|
|
12
|
+
<rect x="18" y="40" width="28" height="8" rx="3" fill="#111827" opacity="0.7"/>
|
|
13
|
+
</svg>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage starter logo — variant B</title>
|
|
3
|
+
<desc>Abstract sunrise arc suggesting warmth and momentum</desc>
|
|
4
|
+
<defs>
|
|
5
|
+
<linearGradient id="g" x1="0%" y1="100%" x2="100%" y2="0%">
|
|
6
|
+
<stop offset="0%" stop-color="#dc2626"/>
|
|
7
|
+
<stop offset="50%" stop-color="#ea580c"/>
|
|
8
|
+
<stop offset="100%" stop-color="#f59e0b"/>
|
|
9
|
+
</linearGradient>
|
|
10
|
+
</defs>
|
|
11
|
+
<path d="M 8 44 A 24 24 0 0 1 56 44" stroke="url(#g)" stroke-width="8" fill="none" stroke-linecap="round"/>
|
|
12
|
+
<circle cx="32" cy="44" r="5" fill="#dc2626"/>
|
|
13
|
+
<line x1="32" y1="14" x2="32" y2="22" stroke="#f59e0b" stroke-width="4" stroke-linecap="round"/>
|
|
14
|
+
<line x1="14" y1="26" x2="20" y2="30" stroke="#ea580c" stroke-width="3" stroke-linecap="round"/>
|
|
15
|
+
<line x1="50" y1="26" x2="44" y2="30" stroke="#ea580c" stroke-width="3" stroke-linecap="round"/>
|
|
16
|
+
</svg>
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage starter logo — variant C</title>
|
|
3
|
+
<desc>Abstract leaf and root mark suggesting growth and earth</desc>
|
|
4
|
+
<circle cx="32" cy="20" r="14" fill="#166534"/>
|
|
5
|
+
<circle cx="32" cy="20" r="8" fill="#14532d"/>
|
|
6
|
+
<circle cx="32" cy="20" r="4" fill="#bbf7d0"/>
|
|
7
|
+
<rect x="30" y="34" width="4" height="14" rx="2" fill="#713f12"/>
|
|
8
|
+
<path d="M 20 48 Q 32 38 44 48" stroke="#713f12" stroke-width="3" fill="none" stroke-linecap="round"/>
|
|
9
|
+
</svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64" role="img" aria-labelledby="title desc">
|
|
2
|
+
<title>Micropage starter logo — variant D</title>
|
|
3
|
+
<desc>Bold geometric diamond mark in charcoal with magenta accent</desc>
|
|
4
|
+
<polygon points="32,8 52,32 32,56 12,32" fill="#1c1c1c"/>
|
|
5
|
+
<polygon points="32,18 44,32 32,46 20,32" fill="#2e2e2e"/>
|
|
6
|
+
<polygon points="32,26 38,32 32,38 26,32" fill="#e91e8c"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
[site]
|
|
2
|
+
title: Slab Studio
|
|
3
|
+
description: Brand, digital, and motion design for companies that want to stand out
|
|
4
|
+
logo: <- logo.svg
|
|
5
|
+
favicon: <- favicon.svg
|
|
6
|
+
colors:
|
|
7
|
+
primary: #111111
|
|
8
|
+
secondary: #333333
|
|
9
|
+
danger: #ff6b35
|
|
10
|
+
|
|
11
|
+
[site.dark]
|
|
12
|
+
colors:
|
|
13
|
+
primary: #f5f5f5
|
|
14
|
+
secondary: #cccccc
|
|
15
|
+
|
|
16
|
+
[nav]
|
|
17
|
+
Work -> /
|
|
18
|
+
Studio -> /studio
|
|
19
|
+
Contact -> /contact
|
|
20
|
+
|
|
21
|
+
[footer]
|
|
22
|
+
/// section bg:dark text:light
|
|
23
|
+
|
|
24
|
+
col:
|
|
25
|
+
h5: SLAB STUDIO
|
|
26
|
+
p: Brand — Digital — Motion
|
|
27
|
+
p: Brooklyn, New York
|
|
28
|
+
|
|
29
|
+
col:
|
|
30
|
+
link: LinkedIn -> https://linkedin.com
|
|
31
|
+
link: Dribbble -> https://dribbble.com
|
|
32
|
+
link: Instagram -> https://instagram.com
|
|
33
|
+
|
|
34
|
+
col:
|
|
35
|
+
p: New projects: hello@slabstudio.co
|
|
36
|
+
p: Press: press@slabstudio.co
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
[Work -> /]
|
|
40
|
+
|
|
41
|
+
/// section align:center
|
|
42
|
+
h1: We make things look like they mean it.
|
|
43
|
+
p: Slab Studio is a seven-person brand and digital studio. We work with founders and creative directors who know what they want and need a team that can build it.
|
|
44
|
+
|
|
45
|
+
/// section
|
|
46
|
+
h2: Selected projects
|
|
47
|
+
|
|
48
|
+
col:
|
|
49
|
+
h3: Cinder — Brand Identity
|
|
50
|
+
p: End-to-end identity for a B2B fintech: wordmark, color system, motion guidelines, and a component library used across three product surfaces.
|
|
51
|
+
small: Fintech · 2024
|
|
52
|
+
|
|
53
|
+
col:
|
|
54
|
+
h3: Waypoint — Campaign
|
|
55
|
+
p: Product launch campaign for a logistics SaaS — visual language, video direction, and a microsite that converted at 4.2%.
|
|
56
|
+
small: Logistics · 2024
|
|
57
|
+
|
|
58
|
+
col:
|
|
59
|
+
h3: Olive & Thread — Packaging
|
|
60
|
+
p: Rebranded a sustainable fashion label from logo to hangtag to e-commerce photography direction.
|
|
61
|
+
small: Fashion · 2023
|
|
62
|
+
|
|
63
|
+
/// section bg:muted
|
|
64
|
+
h2: More work
|
|
65
|
+
|
|
66
|
+
col:
|
|
67
|
+
h3: Nova Health — Digital Product
|
|
68
|
+
p: Design system and patient-facing app UI for a telehealth platform. Accessibility-first, WCAG 2.1 AA.
|
|
69
|
+
small: Health tech · 2023
|
|
70
|
+
|
|
71
|
+
col:
|
|
72
|
+
h3: Paloma Records — Art Direction
|
|
73
|
+
p: Album artwork, lyric video direction, and social templates for four releases.
|
|
74
|
+
small: Music · 2023
|
|
75
|
+
|
|
76
|
+
col:
|
|
77
|
+
h3: Fieldwork Brewing — Web
|
|
78
|
+
p: Responsive marketing site with custom animations, tap room finder, and e-commerce integration.
|
|
79
|
+
small: Food & beverage · 2022
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
[Studio -> /studio]
|
|
83
|
+
|
|
84
|
+
/// section
|
|
85
|
+
h1: About Slab
|
|
86
|
+
|
|
87
|
+
col:
|
|
88
|
+
p: We started Slab in 2018 after leaving larger agencies because we wanted to do fewer projects, better. Seven people, no junior-to-senior ratio games, no account managers between you and the work.
|
|
89
|
+
|
|
90
|
+
col:
|
|
91
|
+
p: Everyone at Slab touches client work. Our lead designer also codes. Our strategist also writes. That's on purpose.
|
|
92
|
+
|
|
93
|
+
/// section align:center
|
|
94
|
+
h2: How we work
|
|
95
|
+
|
|
96
|
+
col:
|
|
97
|
+
icon: bi bi-chat-square-text
|
|
98
|
+
h3: Discovery
|
|
99
|
+
p: Two-week engagement to align on objectives, audiences, and success criteria before any creative begins.
|
|
100
|
+
|
|
101
|
+
col:
|
|
102
|
+
icon: bi bi-easel
|
|
103
|
+
h3: Creative development
|
|
104
|
+
p: Three rounds of feedback with clear decision points. No endless-revision contracts.
|
|
105
|
+
|
|
106
|
+
col:
|
|
107
|
+
icon: bi bi-box-arrow-up-right
|
|
108
|
+
h3: Handoff
|
|
109
|
+
p: Full source files, a brand guidelines document, and a 30-day support window after launch.
|
|
110
|
+
|
|
111
|
+
/// section bg:dark text:light align:center
|
|
112
|
+
h2: We work with clients who ship.
|
|
113
|
+
p: Not committees. Not approvals-by-committee. Founders, CPOs, and creative directors with the authority to make decisions and the taste to know a good one.
|
|
114
|
+
button: Start a conversation -> /contact
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
[Contact -> /contact]
|
|
118
|
+
|
|
119
|
+
/// section
|
|
120
|
+
h1: Start a project
|
|
121
|
+
p: Tell us what you're building and why it matters. We'll come back within 48 hours.
|
|
122
|
+
|
|
123
|
+
form: intake
|
|
124
|
+
input: Name*
|
|
125
|
+
input: Email*
|
|
126
|
+
input: Company
|
|
127
|
+
select: Project type [Brand identity, Web design, Campaign, Motion, Product design, Other]
|
|
128
|
+
select: Approximate budget [$20k–$50k, $50k–$100k, $100k+, Not sure yet]
|
|
129
|
+
text: Tell us about the project*
|
|
130
|
+
submit: Send brief
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
[site]
|
|
2
|
+
title: The Long Read
|
|
3
|
+
description: Reporting and essays on technology, cities, and the people who build them
|
|
4
|
+
logo: <- logo.svg
|
|
5
|
+
favicon: <- favicon.svg
|
|
6
|
+
colors:
|
|
7
|
+
primary: #1f2937
|
|
8
|
+
secondary: #374151
|
|
9
|
+
danger: #b91c1c
|
|
10
|
+
|
|
11
|
+
[site.dark]
|
|
12
|
+
colors:
|
|
13
|
+
primary: #f9fafb
|
|
14
|
+
secondary: #e5e7eb
|
|
15
|
+
danger: #ef4444
|
|
16
|
+
|
|
17
|
+
[nav]
|
|
18
|
+
Latest -> /
|
|
19
|
+
Archive -> /archive
|
|
20
|
+
About -> /about
|
|
21
|
+
Subscribe -> /subscribe
|
|
22
|
+
|
|
23
|
+
[footer]
|
|
24
|
+
/// section
|
|
25
|
+
|
|
26
|
+
col:
|
|
27
|
+
h5: THE LONG READ
|
|
28
|
+
p: Published weekly. No ads. Reader-supported.
|
|
29
|
+
|
|
30
|
+
col:
|
|
31
|
+
link: Twitter -> https://twitter.com
|
|
32
|
+
link: RSS -> /feed.xml
|
|
33
|
+
link: Archive -> /archive
|
|
34
|
+
|
|
35
|
+
col:
|
|
36
|
+
p: Tips and story ideas: tips@thelongread.io
|
|
37
|
+
p: Licensing: rights@thelongread.io
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
[Latest -> /]
|
|
41
|
+
|
|
42
|
+
/// section
|
|
43
|
+
h1: Latest stories
|
|
44
|
+
|
|
45
|
+
col:
|
|
46
|
+
h3: The Fiber Wars
|
|
47
|
+
p: Why three companies are spending $80 billion to wire the same neighborhoods — and who loses when they all pull back.
|
|
48
|
+
small: Infrastructure · 22 min read
|
|
49
|
+
|
|
50
|
+
col:
|
|
51
|
+
h3: What Happened to the Promise of Smart Cities
|
|
52
|
+
p: A decade ago, Sidewalk Toronto was supposed to prove that data could make urban life better. Here's what we learned from its failure.
|
|
53
|
+
small: Cities · 18 min read
|
|
54
|
+
|
|
55
|
+
col:
|
|
56
|
+
h3: The Engineer Who Said No
|
|
57
|
+
p: When Priya Nair refused to ship a feature she thought was dangerous, she lost her job. Two years later, she's not sure she'd do it differently.
|
|
58
|
+
small: Profile · 14 min read
|
|
59
|
+
|
|
60
|
+
/// section bg:muted
|
|
61
|
+
h2: From the archive
|
|
62
|
+
|
|
63
|
+
col:
|
|
64
|
+
h3: The Last Payphone
|
|
65
|
+
p: New York's final payphone came down in May 2022. We spent a day with the crew that removed it.
|
|
66
|
+
small: Cities · 8 min read
|
|
67
|
+
|
|
68
|
+
col:
|
|
69
|
+
h3: Chasing the Algorithm
|
|
70
|
+
p: A content moderation contractor describes 18 months of reading the internet's worst content so you don't have to.
|
|
71
|
+
small: Technology · 26 min read
|
|
72
|
+
|
|
73
|
+
col:
|
|
74
|
+
h3: Where Did the Mid-Size City Go?
|
|
75
|
+
p: Population data tells one story. Walking around Youngstown, Ohio tells another.
|
|
76
|
+
small: Cities · 15 min read
|
|
77
|
+
|
|
78
|
+
/// section bg:danger text:light align:center
|
|
79
|
+
h2: Become a supporting reader.
|
|
80
|
+
p: The Long Read is fully reader-supported. No investors, no brand deals, no tracking. A subscription covers our editorial costs and keeps the site ad-free.
|
|
81
|
+
button: Subscribe — $10/month -> /subscribe
|
|
82
|
+
btn-secondary: Gift a subscription -> /gift
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
[Archive -> /archive]
|
|
86
|
+
|
|
87
|
+
/// section
|
|
88
|
+
h1: All stories
|
|
89
|
+
|
|
90
|
+
/// section
|
|
91
|
+
h2: Technology
|
|
92
|
+
|
|
93
|
+
col:
|
|
94
|
+
link: The Fiber Wars -> /stories/fiber-wars
|
|
95
|
+
link: Chasing the Algorithm -> /stories/chasing-algorithm
|
|
96
|
+
link: Who Owns the Sidewalk Data? -> /stories/sidewalk-data
|
|
97
|
+
link: The Quiet Monopoly -> /stories/quiet-monopoly
|
|
98
|
+
|
|
99
|
+
/// section
|
|
100
|
+
h2: Cities
|
|
101
|
+
|
|
102
|
+
col:
|
|
103
|
+
link: What Happened to the Promise of Smart Cities -> /stories/smart-cities
|
|
104
|
+
link: The Last Payphone -> /stories/last-payphone
|
|
105
|
+
link: Where Did the Mid-Size City Go? -> /stories/mid-size-city
|
|
106
|
+
link: Living on the Heat Map -> /stories/heat-map
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
[About -> /about]
|
|
110
|
+
|
|
111
|
+
/// section
|
|
112
|
+
h1: About The Long Read
|
|
113
|
+
|
|
114
|
+
col:
|
|
115
|
+
p: The Long Read was started in 2021 by two journalists who were tired of writing to SEO briefs. We publish one to two pieces a week — long-form reported stories and essays about technology and the built environment.
|
|
116
|
+
|
|
117
|
+
col:
|
|
118
|
+
p: We have no investors, no editorial board, and no algorithmic pressures. If something takes six months to report properly, we take six months.
|
|
119
|
+
|
|
120
|
+
/// section align:center
|
|
121
|
+
h2: Our editorial standards
|
|
122
|
+
|
|
123
|
+
col:
|
|
124
|
+
icon: bi bi-incognito
|
|
125
|
+
h3: No tracking
|
|
126
|
+
p: We collect no analytics, no behavioral data, no ad pixels. Your reading is your own.
|
|
127
|
+
|
|
128
|
+
col:
|
|
129
|
+
icon: bi bi-journal-check
|
|
130
|
+
h3: Named sources
|
|
131
|
+
p: We don't publish anonymously sourced claims without a documented reason. We always try to give subjects the chance to respond.
|
|
132
|
+
|
|
133
|
+
col:
|
|
134
|
+
icon: bi bi-archive
|
|
135
|
+
h3: Everything stays up
|
|
136
|
+
p: We don't delete stories. If we get something wrong, we publish a correction and leave both up.
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
[Subscribe -> /subscribe]
|
|
140
|
+
|
|
141
|
+
/// section
|
|
142
|
+
h1: Support independent journalism
|
|
143
|
+
p: Subscribers get all stories in their inbox each week, access to the full archive, and the knowledge that this publication exists because of them.
|
|
144
|
+
|
|
145
|
+
col:
|
|
146
|
+
h3: Monthly
|
|
147
|
+
p: $10 per month. Cancel any time.
|
|
148
|
+
button: Subscribe monthly -> /checkout/monthly
|
|
149
|
+
|
|
150
|
+
col:
|
|
151
|
+
h3: Annual
|
|
152
|
+
p: $90 per year — save $30.
|
|
153
|
+
button: Subscribe annually -> /checkout/annual
|
|
154
|
+
|
|
155
|
+
col:
|
|
156
|
+
h3: Founding reader
|
|
157
|
+
p: $200 per year. Your name in our annual supporters list.
|
|
158
|
+
button: Become a founding reader -> /checkout/founding
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
[site]
|
|
2
|
+
title: Fluxline
|
|
3
|
+
description: Open-source event streaming SDK for TypeScript and Go
|
|
4
|
+
logo: <- logo.svg
|
|
5
|
+
favicon: <- favicon.svg
|
|
6
|
+
colors:
|
|
7
|
+
primary: #0d9488
|
|
8
|
+
secondary: #0f766e
|
|
9
|
+
info: #0891b2
|
|
10
|
+
|
|
11
|
+
[site.dark]
|
|
12
|
+
colors:
|
|
13
|
+
primary: #2dd4bf
|
|
14
|
+
secondary: #14b8a6
|
|
15
|
+
|
|
16
|
+
[nav]
|
|
17
|
+
Home -> /
|
|
18
|
+
Docs -> /docs
|
|
19
|
+
Pricing -> /pricing
|
|
20
|
+
GitHub -> https://github.com
|
|
21
|
+
|
|
22
|
+
[footer]
|
|
23
|
+
/// section bg:dark text:light
|
|
24
|
+
|
|
25
|
+
col:
|
|
26
|
+
h5: FLUXLINE
|
|
27
|
+
p: Open source under Apache 2.0
|
|
28
|
+
link: GitHub -> https://github.com
|
|
29
|
+
|
|
30
|
+
col:
|
|
31
|
+
link: Documentation -> /docs
|
|
32
|
+
link: Changelog -> /changelog
|
|
33
|
+
link: Status -> https://status.fluxline.dev
|
|
34
|
+
|
|
35
|
+
col:
|
|
36
|
+
p: Fluxline is maintained by a small team and community contributors.
|
|
37
|
+
link: Contributing guide -> /contributing
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
[Home -> /]
|
|
41
|
+
|
|
42
|
+
/// section bg:dark text:light
|
|
43
|
+
h5: OPEN SOURCE · TYPESCRIPT · GO
|
|
44
|
+
h1: Stream events without the boilerplate.
|
|
45
|
+
p: Fluxline is a lightweight SDK for publishing, subscribing to, and replaying ordered event streams. Works with your existing message broker — no new infrastructure required.
|
|
46
|
+
|
|
47
|
+
////
|
|
48
|
+
html: <pre><code>import { Fluxline } from 'fluxline'
|
|
49
|
+
|
|
50
|
+
const client = new Fluxline({ brokerUrl: process.env.BROKER_URL })
|
|
51
|
+
await client.publish('orders', { id: 'ord_01', total: 49.00 })</code></pre>
|
|
52
|
+
|
|
53
|
+
col:
|
|
54
|
+
button: Get started -> /docs
|
|
55
|
+
btn-secondary: View on GitHub -> https://github.com
|
|
56
|
+
|
|
57
|
+
col:
|
|
58
|
+
p: v1.4.2 · 2.1 kB gzipped · Zero runtime dependencies
|
|
59
|
+
|
|
60
|
+
/// section
|
|
61
|
+
h2: Why Fluxline
|
|
62
|
+
|
|
63
|
+
col:
|
|
64
|
+
icon: bi bi-lightning-charge
|
|
65
|
+
h3: Under 3 kB
|
|
66
|
+
p: The TypeScript client ships at 2.1 kB gzipped. The Go client is a single file you can vendor.
|
|
67
|
+
|
|
68
|
+
col:
|
|
69
|
+
icon: bi bi-arrow-repeat
|
|
70
|
+
h3: Replay from any offset
|
|
71
|
+
p: Seek back to any sequence number and replay events forward. Works with Kafka, NATS, and Redis Streams.
|
|
72
|
+
|
|
73
|
+
col:
|
|
74
|
+
icon: bi bi-shield-check
|
|
75
|
+
h3: Exactly-once delivery
|
|
76
|
+
p: Idempotent producer IDs and consumer group offsets give you strong delivery guarantees out of the box.
|
|
77
|
+
|
|
78
|
+
/// section bg:primary-subtle
|
|
79
|
+
h2: Framework integrations
|
|
80
|
+
|
|
81
|
+
col:
|
|
82
|
+
h3: Next.js / Remix
|
|
83
|
+
p: Server action helpers and route handler bindings. Automatically infer event types from your Zod schemas.
|
|
84
|
+
|
|
85
|
+
col:
|
|
86
|
+
h3: Hono & Fastify
|
|
87
|
+
p: Middleware that turns any route into an event source. Streaming responses with backpressure handling.
|
|
88
|
+
|
|
89
|
+
col:
|
|
90
|
+
h3: Go net/http
|
|
91
|
+
p: Handler adapter for standard Go HTTP servers. No generics gymnastics — just plain functions.
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
[Docs -> /docs]
|
|
95
|
+
|
|
96
|
+
/// section
|
|
97
|
+
h1: Getting started
|
|
98
|
+
|
|
99
|
+
/// section bg:muted
|
|
100
|
+
h2: Installation
|
|
101
|
+
|
|
102
|
+
////
|
|
103
|
+
html: <pre><code># TypeScript / Node
|
|
104
|
+
npm install fluxline
|
|
105
|
+
|
|
106
|
+
# Go
|
|
107
|
+
go get github.com/fluxline/fluxline-go</code></pre>
|
|
108
|
+
|
|
109
|
+
/// section
|
|
110
|
+
h2: Quick start
|
|
111
|
+
|
|
112
|
+
col:
|
|
113
|
+
h3: 1. Create a client
|
|
114
|
+
p: Initialize with your broker URL and an optional namespace prefix. Credentials are read from environment variables by default.
|
|
115
|
+
|
|
116
|
+
col:
|
|
117
|
+
h3: 2. Define your events
|
|
118
|
+
p: Use the TypeScript generic or Go struct tag to attach a schema. Fluxline validates before publishing and narrows the type on receive.
|
|
119
|
+
|
|
120
|
+
col:
|
|
121
|
+
h3: 3. Publish and subscribe
|
|
122
|
+
p: publish() is fire-and-forget with an optional ack callback. subscribe() returns an async iterator or a channel, depending on the language.
|
|
123
|
+
|
|
124
|
+
/// section
|
|
125
|
+
h2: Configuration reference
|
|
126
|
+
|
|
127
|
+
col:
|
|
128
|
+
h3: brokerUrl
|
|
129
|
+
p: Required. WebSocket or TCP URL for your message broker.
|
|
130
|
+
|
|
131
|
+
col:
|
|
132
|
+
h3: namespace
|
|
133
|
+
p: Optional string prefix applied to all stream names. Useful for multi-tenant deployments.
|
|
134
|
+
|
|
135
|
+
col:
|
|
136
|
+
h3: maxRetries
|
|
137
|
+
p: Default 3. How many times to retry a failed publish before raising an error.
|
|
138
|
+
|
|
139
|
+
col:
|
|
140
|
+
h3: ackTimeout
|
|
141
|
+
p: Default 5000 ms. Time to wait for a broker ack before treating the publish as failed.
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
[Pricing -> /pricing]
|
|
145
|
+
|
|
146
|
+
/// section align:center
|
|
147
|
+
h1: Free to start. Pay as you grow.
|
|
148
|
+
p: All plans include the full SDK feature set. Pricing is based on monthly event volume.
|
|
149
|
+
|
|
150
|
+
col:
|
|
151
|
+
h3: Open Source
|
|
152
|
+
p: Self-hosted, unlimited events, community support.
|
|
153
|
+
p: Always free.
|
|
154
|
+
button: Get started -> /docs
|
|
155
|
+
|
|
156
|
+
col:
|
|
157
|
+
h3: Cloud
|
|
158
|
+
p: Managed broker, 10M events/month included.
|
|
159
|
+
p: $49 / month
|
|
160
|
+
button: Start free trial -> /trial
|
|
161
|
+
|
|
162
|
+
col:
|
|
163
|
+
h3: Enterprise
|
|
164
|
+
p: Dedicated cluster, SLA, audit logs, SSO, custom retention.
|
|
165
|
+
p: Custom pricing
|
|
166
|
+
button: Talk to us -> /contact
|
|
167
|
+
|
|
168
|
+
/// section bg:muted
|
|
169
|
+
h2: Frequently asked questions
|
|
170
|
+
|
|
171
|
+
col:
|
|
172
|
+
h3: Can I use my own Kafka cluster?
|
|
173
|
+
p: Yes. The SDK is broker-agnostic. The managed Cloud tier runs NATS JetStream under the hood, but the OSS version works with any NATS, Kafka, or Redis Streams backend.
|
|
174
|
+
|
|
175
|
+
col:
|
|
176
|
+
h3: Is there a free trial for Cloud?
|
|
177
|
+
p: Cloud plans include a 14-day trial with no credit card required. You'll get 10M events to test with before you're charged.
|
|
178
|
+
|
|
179
|
+
col:
|
|
180
|
+
h3: What counts as an event?
|
|
181
|
+
p: Any publish() call that reaches the broker. Failed attempts before retry exhaustion are not counted.
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
[site]
|
|
2
|
+
title: Frontline Summit
|
|
3
|
+
description: The annual conference for product, design, and engineering leaders
|
|
4
|
+
logo: <- logo.svg
|
|
5
|
+
favicon: <- favicon.svg
|
|
6
|
+
colors:
|
|
7
|
+
primary: #e3236d
|
|
8
|
+
secondary: #b01a55
|
|
9
|
+
info: #7c3aed
|
|
10
|
+
|
|
11
|
+
[site.dark]
|
|
12
|
+
colors:
|
|
13
|
+
primary: #f0457e
|
|
14
|
+
secondary: #e3236d
|
|
15
|
+
|
|
16
|
+
[nav]
|
|
17
|
+
Home -> /
|
|
18
|
+
Schedule -> /schedule
|
|
19
|
+
Speakers -> /speakers
|
|
20
|
+
Register -> /register
|
|
21
|
+
|
|
22
|
+
[footer]
|
|
23
|
+
/// section bg:dark text:light
|
|
24
|
+
|
|
25
|
+
col:
|
|
26
|
+
h5: FRONTLINE SUMMIT 2025
|
|
27
|
+
p: March 12–14, 2025
|
|
28
|
+
p: Chicago, IL — The Civic Ballroom
|
|
29
|
+
|
|
30
|
+
col:
|
|
31
|
+
link: Code of conduct -> /coc
|
|
32
|
+
link: Sponsorship -> /sponsors
|
|
33
|
+
link: Contact -> /contact
|
|
34
|
+
|
|
35
|
+
col:
|
|
36
|
+
p: Questions? Email us at hello@frontlinesummit.com
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
[Home -> /]
|
|
40
|
+
|
|
41
|
+
/// hero bg:primary text:light
|
|
42
|
+
h1: Build what comes next.
|
|
43
|
+
p: Frontline Summit brings together 1,200 product, design, and engineering leaders for three days of talks, workshops, and honest conversations about how software gets made.
|
|
44
|
+
p: March 12–14, 2025 · Chicago
|
|
45
|
+
button: Register now -> /register
|
|
46
|
+
btn-secondary: View schedule -> /schedule
|
|
47
|
+
|
|
48
|
+
/// section
|
|
49
|
+
h2: This year's tracks
|
|
50
|
+
|
|
51
|
+
col:
|
|
52
|
+
icon: bi bi-cpu
|
|
53
|
+
h3: Platform Engineering
|
|
54
|
+
p: Developer experience, internal platforms, and the golden path between speed and stability.
|
|
55
|
+
|
|
56
|
+
col:
|
|
57
|
+
icon: bi bi-palette
|
|
58
|
+
h3: Product Design at Scale
|
|
59
|
+
p: Design systems, accessibility debt, and leading creative work in a fast-moving org.
|
|
60
|
+
|
|
61
|
+
col:
|
|
62
|
+
icon: bi bi-people
|
|
63
|
+
h3: Leadership & Culture
|
|
64
|
+
p: Hiring, retention, psychological safety, and building teams that survive hypergrowth.
|
|
65
|
+
|
|
66
|
+
/// section bg:muted
|
|
67
|
+
h2: Formats
|
|
68
|
+
|
|
69
|
+
col:
|
|
70
|
+
h3: Keynotes
|
|
71
|
+
p: Six 45-minute talks from practitioners who've shipped things that mattered.
|
|
72
|
+
|
|
73
|
+
col:
|
|
74
|
+
h3: Deep-dive workshops
|
|
75
|
+
p: Half-day sessions capped at 30 attendees. Hands-on, no slides, all practice.
|
|
76
|
+
|
|
77
|
+
col:
|
|
78
|
+
h3: Open hallway track
|
|
79
|
+
p: Unscheduled space for spontaneous conversations. Some of the best talks happen here.
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
[Schedule -> /schedule]
|
|
83
|
+
|
|
84
|
+
/// section
|
|
85
|
+
h1: Schedule at a glance
|
|
86
|
+
|
|
87
|
+
/// section bg:primary-subtle
|
|
88
|
+
h2: Day 1 — March 12
|
|
89
|
+
|
|
90
|
+
col:
|
|
91
|
+
h3: 9:00 am — Opening keynote
|
|
92
|
+
p: "The Decade of the Platform" — keynote by Mira Okonkwo, VP Engineering at Lattice.
|
|
93
|
+
|
|
94
|
+
col:
|
|
95
|
+
h3: 11:00 am — Workshop block A
|
|
96
|
+
p: Choose from: Incident culture, Design sprints for complex features, Staff eng career paths.
|
|
97
|
+
|
|
98
|
+
col:
|
|
99
|
+
h3: 2:00 pm — Lightning talks
|
|
100
|
+
p: Eight 10-minute talks from speakers selected from the open CFP.
|
|
101
|
+
|
|
102
|
+
/// section
|
|
103
|
+
h2: Day 2 — March 13
|
|
104
|
+
|
|
105
|
+
col:
|
|
106
|
+
h3: 9:30 am — Keynote
|
|
107
|
+
p: "Shipping with Confidence" — how Vercel restructured their deploy pipeline and reduced rollbacks by 60%.
|
|
108
|
+
|
|
109
|
+
col:
|
|
110
|
+
h3: 1:00 pm — Panel
|
|
111
|
+
p: "AI in the dev workflow: what's actually working" — four engineering leaders, unfiltered.
|
|
112
|
+
|
|
113
|
+
col:
|
|
114
|
+
h3: 4:00 pm — Unconference
|
|
115
|
+
p: Participant-led sessions. Vote on topics in the morning, run sessions in the afternoon.
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
[Speakers -> /speakers]
|
|
119
|
+
|
|
120
|
+
/// section align:center
|
|
121
|
+
h1: Featured speakers
|
|
122
|
+
|
|
123
|
+
col:
|
|
124
|
+
h3: Mira Okonkwo
|
|
125
|
+
p: VP Engineering, Lattice
|
|
126
|
+
p: Scaling platform teams from 12 to 120 engineers without losing the culture.
|
|
127
|
+
|
|
128
|
+
col:
|
|
129
|
+
h3: Dev Patel
|
|
130
|
+
p: Staff Engineer, Linear
|
|
131
|
+
p: Real-time sync architecture and the tradeoffs nobody talks about.
|
|
132
|
+
|
|
133
|
+
col:
|
|
134
|
+
h3: Carmen Estrada
|
|
135
|
+
p: Head of Design, Figma
|
|
136
|
+
p: Building a design system that 4,000 designers actually use.
|
|
137
|
+
|
|
138
|
+
/// section
|
|
139
|
+
h2: Call for proposals
|
|
140
|
+
|
|
141
|
+
col:
|
|
142
|
+
p: We select 30% of our program from open CFP submissions. If you've solved a hard problem and want to share it, we want to hear from you.
|
|
143
|
+
button: Submit a proposal -> /cfp
|
|
144
|
+
|
|
145
|
+
col:
|
|
146
|
+
p: CFP closes January 15, 2025. Decisions sent by February 1. Travel and hotel covered for selected speakers.
|
|
147
|
+
|
|
148
|
+
|
|
149
|
+
[Register -> /register]
|
|
150
|
+
|
|
151
|
+
/// section
|
|
152
|
+
h1: Get your ticket
|
|
153
|
+
p: Early-bird pricing ends December 1. Workshop seats are limited and sell out first.
|
|
154
|
+
|
|
155
|
+
col:
|
|
156
|
+
h3: Conference pass
|
|
157
|
+
p: Full three-day access, all keynotes and lightning talks, hallway track.
|
|
158
|
+
p: $799 early bird / $999 standard
|
|
159
|
+
button: Register -> /checkout
|
|
160
|
+
|
|
161
|
+
col:
|
|
162
|
+
h3: Conference + Workshop
|
|
163
|
+
p: Everything in the conference pass plus one half-day workshop of your choice.
|
|
164
|
+
p: $1,199 early bird / $1,499 standard
|
|
165
|
+
button: Register -> /checkout
|
|
166
|
+
|
|
167
|
+
col:
|
|
168
|
+
h3: Team of 5
|
|
169
|
+
p: Five conference passes at a 20% discount. Invoice billing available.
|
|
170
|
+
p: $3,196 (saves $800)
|
|
171
|
+
button: Contact us -> /contact
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
[site]
|
|
2
|
+
title: Ember & Salt
|
|
3
|
+
description: Wood-fired kitchen and craft cocktail bar in the heart of the city
|
|
4
|
+
logo: <- logo.svg
|
|
5
|
+
favicon: <- favicon.svg
|
|
6
|
+
colors:
|
|
7
|
+
primary: #c1432d
|
|
8
|
+
secondary: #8b2c17
|
|
9
|
+
warning: #e8a02a
|
|
10
|
+
|
|
11
|
+
[site.dark]
|
|
12
|
+
colors:
|
|
13
|
+
primary: #e05a40
|
|
14
|
+
secondary: #c1432d
|
|
15
|
+
|
|
16
|
+
[nav]
|
|
17
|
+
Menu -> /
|
|
18
|
+
About -> /about
|
|
19
|
+
Reservations -> /reservations
|
|
20
|
+
|
|
21
|
+
[footer]
|
|
22
|
+
/// section bg:dark text:light
|
|
23
|
+
|
|
24
|
+
col:
|
|
25
|
+
h5: EMBER & SALT
|
|
26
|
+
p: 142 North Market Street
|
|
27
|
+
p: Open Tuesday–Sunday, 5 pm – 11 pm
|
|
28
|
+
|
|
29
|
+
col:
|
|
30
|
+
link: Instagram -> https://instagram.com
|
|
31
|
+
link: Yelp -> https://yelp.com
|
|
32
|
+
link: Google Maps -> https://maps.google.com
|
|
33
|
+
|
|
34
|
+
col:
|
|
35
|
+
p: Reservations recommended on weekends.
|
|
36
|
+
link: Book a table -> /reservations
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
[Menu -> /]
|
|
40
|
+
|
|
41
|
+
/// hero bg:dark text:light
|
|
42
|
+
h1: Fire. Smoke. Salt.
|
|
43
|
+
p: We cook over an open wood hearth. Everything is made in-house — from the pasta to the pickles to the ice cream.
|
|
44
|
+
button: Book a table -> /reservations
|
|
45
|
+
btn-secondary: View full menu -> /menu
|
|
46
|
+
|
|
47
|
+
/// section
|
|
48
|
+
h2: From the kitchen
|
|
49
|
+
|
|
50
|
+
col:
|
|
51
|
+
h3: Burrata & Heirloom Tomato
|
|
52
|
+
p: Stone-fruit vinaigrette, basil oil, toasted hazelnuts. Seasonal.
|
|
53
|
+
small: Starter — $18
|
|
54
|
+
|
|
55
|
+
col:
|
|
56
|
+
h3: Hand-Rolled Pappardelle
|
|
57
|
+
p: Braised short rib, rosemary jus, Parmigiano Reggiano.
|
|
58
|
+
small: Pasta — $32
|
|
59
|
+
|
|
60
|
+
col:
|
|
61
|
+
h3: Wood-Fired Half Chicken
|
|
62
|
+
p: Spatchcocked over oak, herb butter, roasted garlic aioli, market greens.
|
|
63
|
+
small: Main — $38
|
|
64
|
+
|
|
65
|
+
/// section bg:primary-subtle
|
|
66
|
+
h2: Signature cocktails
|
|
67
|
+
|
|
68
|
+
col:
|
|
69
|
+
h3: Ember Old Fashioned
|
|
70
|
+
p: Smoked rye, demerara, orange peel, Angostura.
|
|
71
|
+
|
|
72
|
+
col:
|
|
73
|
+
h3: Salt & Citrus Sour
|
|
74
|
+
p: Mezcal, yuzu, egg white, black lava salt rim.
|
|
75
|
+
|
|
76
|
+
col:
|
|
77
|
+
h3: Garden Mule
|
|
78
|
+
p: House ginger beer, cucumber vodka, fresh mint, lime.
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
[About -> /about]
|
|
82
|
+
|
|
83
|
+
/// section align:center
|
|
84
|
+
h1: A kitchen built on fire
|
|
85
|
+
|
|
86
|
+
col:
|
|
87
|
+
p: Ember & Salt started in 2019 as a pop-up at the Fulton Farmers Market. We outgrew our tent. Now we have a wood-burning hearth, a walk-in full of heritage-breed pork and local vegetables, and a bar that takes its bitters seriously.
|
|
88
|
+
|
|
89
|
+
col:
|
|
90
|
+
p: Chef Rena Kowalski spent twelve years in Lyon before returning to open the restaurant she always wanted: unpretentious, ingredient-driven, and stubbornly seasonal.
|
|
91
|
+
|
|
92
|
+
/// section
|
|
93
|
+
h2: Awards & press
|
|
94
|
+
|
|
95
|
+
col:
|
|
96
|
+
icon: bi bi-award
|
|
97
|
+
h3: Best New Restaurant
|
|
98
|
+
p: City & State Dining Guide, 2022
|
|
99
|
+
|
|
100
|
+
col:
|
|
101
|
+
icon: bi bi-newspaper
|
|
102
|
+
h3: Featured in Eater
|
|
103
|
+
p: "One of the most exciting wood-fire openings of the year."
|
|
104
|
+
|
|
105
|
+
col:
|
|
106
|
+
icon: bi bi-star
|
|
107
|
+
h3: Michelin Bib Gourmand
|
|
108
|
+
p: Recognized for exceptional value and quality, 2023 guide.
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
[Reservations -> /reservations]
|
|
112
|
+
|
|
113
|
+
/// section
|
|
114
|
+
h1: Reserve a table
|
|
115
|
+
p: We hold tables up to 48 hours in advance. Walk-ins welcome at the bar.
|
|
116
|
+
|
|
117
|
+
form: reservation
|
|
118
|
+
input: Full name*
|
|
119
|
+
input: Email*
|
|
120
|
+
input: Phone
|
|
121
|
+
select: Party size [1, 2, 3–4, 5–6, 7+]
|
|
122
|
+
select: Preferred time [5:00 pm, 6:00 pm, 7:00 pm, 8:00 pm, 9:00 pm]
|
|
123
|
+
text: Special requests or dietary needs
|
|
124
|
+
submit: Request reservation
|