@techninja/clearstack 0.3.41 → 0.3.43

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.
@@ -17,6 +17,8 @@ import { resolveTemplate, renderTemplate, buildContext, loadTokens, WIDTH, HEIGH
17
17
  * @property {string} [logo] - Path or URL to logo
18
18
  * @property {string} [siteName] - Site name for badge
19
19
  * @property {Record<string, unknown>} [context] - Extra interpolation data
20
+ * @property {(slug: string) => boolean} [filter] - Only render matching slugs
21
+ * @property {(slug: string, n: number, total: number) => void} [onProgress]
20
22
  */
21
23
 
22
24
  /**
@@ -25,7 +27,7 @@ import { resolveTemplate, renderTemplate, buildContext, loadTokens, WIDTH, HEIGH
25
27
  * @returns {Promise<{ images: number, routes: number }>}
26
28
  */
27
29
  export async function buildOGImages(opts) {
28
- const { projectDir, outDir = 'dist', logo, siteName, context = {} } = opts;
30
+ const { projectDir, outDir = 'dist', logo, siteName, context = {}, filter, onProgress } = opts;
29
31
  const routes = loadRoutes(projectDir);
30
32
  if (!routes) {
31
33
  console.log('⚠ No routes config found. Create clearstack.routes.json');
@@ -37,7 +39,8 @@ export async function buildOGImages(opts) {
37
39
  const start = performance.now();
38
40
 
39
41
  const { chromium } = await import('playwright');
40
- const browser = await chromium.launch();
42
+ const browser = await chromium.launch({ executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH });
43
+ console.log(' Browser ready, rendering...');
41
44
  const page = await browser.newPage({ viewport: { width: WIDTH, height: HEIGHT } });
42
45
  const out = resolve(projectDir, outDir);
43
46
  let images = 0;
@@ -47,6 +50,7 @@ export async function buildOGImages(opts) {
47
50
  const isDynamic = pattern.includes(':');
48
51
 
49
52
  if (!isDynamic) {
53
+ if (filter) continue;
50
54
  const data = { app: { name: siteName }, ...context };
51
55
  const ctx = buildContext(config, data, site);
52
56
  await screenshot(page, renderTemplate(template, ctx, projectDir), out, pattern);
@@ -57,11 +61,13 @@ export async function buildOGImages(opts) {
57
61
  for (const item of items) {
58
62
  const slug = item.slug || item.id || item.sku || item[paramName];
59
63
  if (!slug) continue;
64
+ if (filter && !filter(String(slug))) continue;
60
65
  const path = pattern.replace(`:${paramName}`, String(slug));
61
66
  const data = { [paramName]: item, item, app: { name: siteName }, ...context };
62
67
  const ctx = buildContext(config, data, site);
63
68
  await screenshot(page, renderTemplate(template, ctx, projectDir), out, path);
64
69
  images++;
70
+ if (onProgress) onProgress(String(slug), images, -1);
65
71
  }
66
72
  }
67
73
  }
@@ -85,7 +91,7 @@ export async function buildOneOGImage(opts) {
85
91
  const tokens = loadTokens(projectDir);
86
92
  const site = { tokens, logo, siteName };
87
93
  const { chromium } = await import('playwright');
88
- const browser = await chromium.launch();
94
+ const browser = await chromium.launch({ executablePath: process.env.PLAYWRIGHT_EXECUTABLE_PATH });
89
95
  const page = await browser.newPage({ viewport: { width: WIDTH, height: HEIGHT } });
90
96
  const out = resolve(projectDir, outDir);
91
97
  let result = '';
@@ -113,7 +119,8 @@ export async function buildOneOGImage(opts) {
113
119
 
114
120
  /** Render HTML content and save screenshot to disk. */
115
121
  async function screenshot(page, html, outDir, routePath) {
116
- await page.setContent(html, { waitUntil: 'networkidle' });
122
+ await page.setContent(html, { waitUntil: 'domcontentloaded' });
123
+ await page.waitForTimeout(html.includes('fonts.googleapis') ? 2500 : 800);
117
124
  const imgPath = routePath === '/'
118
125
  ? resolve(outDir, 'og-image.png')
119
126
  : resolve(outDir, routePath.slice(1) + '.png');
package/lib/build-og.js CHANGED
@@ -114,7 +114,7 @@ function resolveMeta(config, data, baseUrl, path) {
114
114
  function writePage(outDir, routePath, shell, meta) {
115
115
  const filePath = routePath === '/'
116
116
  ? resolve(outDir, 'index.html')
117
- : resolve(outDir, routePath.slice(1), 'index.html');
117
+ : resolve(outDir, routePath.slice(1) + '.html');
118
118
  mkdirSync(dirname(filePath), { recursive: true });
119
119
  writeFileSync(filePath, injectMeta(shell, meta));
120
120
  }
@@ -75,6 +75,14 @@ export function buildContext(config, itemData, site) {
75
75
  const resolvedImage = image && !image.startsWith('http') ? `${baseUrl}${image}` : image;
76
76
  const item = itemData.item || {};
77
77
  const emoji = item.emoji || itemData.emoji || '';
78
+ const tags = item.tags || itemData.tags || [];
79
+ const tagsHtml = tags.map((t) => `<div class="tag">#${t}</div>`).join('');
80
+ const postTitle = item.title || item.caption || item.name || '';
81
+ const logoSrc = item.logo?.wordmark || item.logo?.mark || '';
82
+ const itemLogoHtml = logoSrc ? `<img src="${logoSrc}" alt="${postTitle}">` : '';
83
+ const dateFormatted = item.date ? new Date(item.date).toLocaleDateString('en-US', { year: 'numeric', month: 'long', day: 'numeric' }) : '';
84
+ const mediaUrl = resolvedImage || (item.files?.[0] ? `https://data.tn42.com/assets-media/${item.files[0]}` : '');
85
+ const isVideo = item.type === 'video' || (item.files?.[0] || '').endsWith('.mp4');
78
86
 
79
87
  return {
80
88
  ...itemData, ...item,
@@ -87,12 +95,18 @@ export function buildContext(config, itemData, site) {
87
95
  text: t['color-text'] || '#e2e8f0',
88
96
  textMuted: t['color-text-muted'] || '#a8b8cc',
89
97
  emoji, image: resolvedImage,
98
+ tagsHtml, postTitle, dateFormatted, mediaUrl,
99
+ logoHtml: itemLogoHtml,
100
+ noLogoMark: itemLogoHtml ? '' : '<div class="mark">42</div>',
101
+ mediaHtml: isVideo
102
+ ? `<video src="${mediaUrl}" autoplay muted playsinline style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover"></video>`
103
+ : mediaUrl ? `<img src="${mediaUrl}" style="position:absolute;inset:0;width:100%;height:100%;object-fit:cover">` : '',
90
104
  variantsFormatted: formatNum(item.expected_variants),
91
105
  uniqueFormatted: formatNum(item.estimated_unique_variants),
92
106
  priceFormatted: item.price ? `$${(item.price / 100).toFixed(2)}` : '',
93
107
  imageHtml: resolvedImage ? `<img class="hero" src="${resolvedImage}">` : '',
94
108
  cardClass: resolvedImage ? 'card-with-image' : '',
95
- logoHtml: site.logo ? `<img class="logo" src="${site.logo}">` : '',
109
+ siteLogo: site.logo ? `<img class="logo" src="${site.logo}">` : '',
96
110
  badgeHtml: site.siteName ? `<div class="badge">${site.siteName}</div>` : '',
97
111
  emojiHtml: emoji ? `<div class="emoji">${emoji}</div>` : '',
98
112
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@techninja/clearstack",
3
- "version": "0.3.41",
3
+ "version": "0.3.43",
4
4
  "type": "module",
5
5
  "description": "A no-build web component framework specification — scaffold, validate, and evolve spec-compliant projects",
6
6
  "bin": {