diff --git a/app/scripts/generate-web-stories.mjs b/app/scripts/generate-web-stories.mjs new file mode 100644 index 0000000..9bd6262 --- /dev/null +++ b/app/scripts/generate-web-stories.mjs @@ -0,0 +1,255 @@ +#!/usr/bin/env node +// Generates CSS scroll-snap Web Stories for each published article. +// Output: dist/stories/{slug}/index.html +// Zero-CDN: inline CSS only, uses local OG images and local fonts. +import { existsSync, mkdirSync, writeFileSync } from 'node:fs'; +import { resolve } from 'node:path'; +import { TeknavData } from '../teknav-data.js'; + +const SITE_URL = (process.env.TEKNAV_SITE_URL || process.env.VITE_SITE_URL || 'https://www.teknav.ir').replace(/\/$/, ''); +const today = new Date().toISOString().slice(0, 10); + +function esc(value) { + return String(value ?? '') + .replace(/&/g, '&') + .replace(/"/g, '"') + .replace(//g, '>'); +} + +function clean(value) { + return String(value ?? '').replace(/<[^>]+>/g, ' ').replace(/\s+/g, ' ').trim(); +} + +function truncate(value, max) { + const text = clean(value); + return text.length > max ? `${text.slice(0, max - 1).trim()}…` : text; +} + +function isPastOrTodayDate(date) { + const value = String(date ?? '').slice(0, 10); + return /^\d{4}-\d{2}-\d{2}$/.test(value) && value <= today; +} + +function isPublishedArticle(article) { + if (article.status === 'منتشرشده') return true; + if (article.status === 'پیش‌نویس' || article.status === 'در انتظار بررسی') return false; + if (['rust-systems', 'venture-capital-ai'].includes(article.slug)) return false; + return !!article.dateEn; +} + +function extractSlides(article) { + const content = String(article.content || ''); + const slides = []; + + // Lead paragraph + const leadMatch = content.match(/]*class="lead"[^>]*>([\s\S]*?)<\/p>/i) + || content.match(/

([\s\S]*?)<\/p>/i); + const lead = leadMatch ? truncate(clean(leadMatch[1]), 220) : truncate(article.summary || '', 220); + if (lead) slides.push({ type: 'lead', text: lead }); + + // h2 headings + the text right after them + const h2Pattern = /]*>([\s\S]*?)<\/h2>([\s\S]*?)(?=]*>([\s\S]*?)<\/p>/i); + const body = bodyMatch ? truncate(clean(bodyMatch[1]), 200) : ''; + if (heading) slides.push({ type: 'section', heading, body }); + } + + return slides; +} + +function slide(id, bgImage, content, slideNum, totalSlides) { + const progress = Math.round((slideNum / totalSlides) * 100); + return `

+
+
+
+
${content}
+
`; +} + +function buildStory(article) { + const slug = article.slug; + const title = esc(article.title); + const subtitle = esc(article.subtitle || article.summary || ''); + const canonical = `${SITE_URL}/article/${slug}`; + const storyUrl = `${SITE_URL}/stories/${slug}`; + const slugImg = `/images/og/${slug}.jpg`; + const hasSlugImg = existsSync(resolve(`dist${slugImg}`)); + const ogImage = hasSlugImg ? slugImg : (article.ogImage || '/images/og/default.jpg'); + const category = esc(article.categoryName || ''); + const author = esc(article.authorName || ''); + const date = esc(article.date || ''); + const readTime = article.readTime || 5; + + const contentSlides = extractSlides(article); + const totalSlides = 2 + contentSlides.length + 1; // cover + content + CTA + const slides = []; + let slideNum = 0; + + // Cover slide + slides.push(slide('cover', ogImage, ` +
${category}
+

${title}

+

${author} · ${date} · ${readTime} دقیقه

+
← بکشید
`, slideNum++, totalSlides)); + + // Content slides + for (const s of contentSlides) { + if (s.type === 'lead') { + slides.push(slide(`slide-${slideNum}`, ogImage, ` +

${esc(s.text)}

`, slideNum++, totalSlides)); + } else { + slides.push(slide(`slide-${slideNum}`, ogImage, ` +

${esc(s.heading)}

+ ${s.body ? `

${esc(s.body)}

` : ''}`, slideNum++, totalSlides)); + } + } + + // CTA slide + slides.push(slide('cta', ogImage, ` +
+ +

تحلیل کامل را در تکناو بخوانید

+

${subtitle}

+ خواندن مقاله → +
`, slideNum++, totalSlides)); + + const jsonLd = JSON.stringify({ + '@context': 'https://schema.org', + '@graph': [ + { + '@type': ['Organization', 'NewsMediaOrganization'], + '@id': `${SITE_URL}/#organization`, + name: 'تکناو', + url: SITE_URL, + logo: { '@type': 'ImageObject', url: `${SITE_URL}/favicon.png` }, + }, + { + '@type': ['NewsArticle', 'TechArticle'], + headline: clean(article.title).slice(0, 110), + description: clean(article.metaDescription || article.summary || '').slice(0, 160), + image: `${SITE_URL}${ogImage}`, + datePublished: article.dateEn, + author: { '@type': 'Person', name: article.authorName || 'تکناو' }, + publisher: { '@id': `${SITE_URL}/#organization` }, + mainEntityOfPage: { '@type': 'WebPage', '@id': canonical }, + url: storyUrl, + }, + ], + }); + + return ` + + + + + + ${title} | تکناو + + + + + + + + + + + + + + + + + + + + +
+${slides.join('\n')} +
+ + + +`; +} + +const publishedArticles = TeknavData.articles.filter( + (a) => isPublishedArticle(a) && isPastOrTodayDate(a.dateEn), +); + +let count = 0; +for (const article of publishedArticles) { + const dir = resolve(`dist/stories/${article.slug}`); + mkdirSync(dir, { recursive: true }); + writeFileSync(`${dir}/index.html`, buildStory(article), 'utf8'); + count++; +} + +console.log(`[web-stories] generated ${count} story pages in dist/stories/`);