clone
This commit is contained in:
175
scripts/clone-page.ts
Normal file
175
scripts/clone-page.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import scrape from 'website-scraper';
|
||||
// @ts-ignore
|
||||
import PuppeteerPlugin from 'website-scraper-puppeteer';
|
||||
import path from 'node:path';
|
||||
import { fileURLToPath } from 'node:url';
|
||||
import fs from 'node:fs';
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
|
||||
async function run() {
|
||||
let rawUrl = process.argv[2];
|
||||
if (!rawUrl) {
|
||||
console.error('Usage: npm run clone-page <URL>');
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// CLEANUP: Aggressively strip shell noise like ; or trailing quotes
|
||||
const targetUrl = rawUrl.trim().replace(/[;'"]+$/, '');
|
||||
const urlObj = new URL(targetUrl);
|
||||
const domain = urlObj.hostname;
|
||||
const safeDomain = domain.replace(/[^a-z0-9-]/gi, '_');
|
||||
|
||||
const domainDir = path.resolve(__dirname, '../cloned-websites', safeDomain);
|
||||
if (!fs.existsSync(domainDir)) fs.mkdirSync(domainDir, { recursive: true });
|
||||
|
||||
// Determine slug for filename
|
||||
let slug = urlObj.pathname.replace(/^\/|\/$/g, '').replace(/\//g, '-');
|
||||
if (!slug) slug = 'index';
|
||||
const htmlFilename = `${slug}.html`;
|
||||
|
||||
console.log(`🚀 CLONING PAGE: ${targetUrl}`);
|
||||
console.log(`📂 SAVING AS: ${htmlFilename} in ${domainDir}`);
|
||||
|
||||
// website-scraper needs an empty directory for each 'scrape' call if we use its defaults,
|
||||
// but we want to MERGE assets. So we scrape to a temp dir and then move.
|
||||
const tempDir = path.join(domainDir, `_temp_${Date.now()}`);
|
||||
|
||||
const options = {
|
||||
urls: [targetUrl],
|
||||
directory: tempDir,
|
||||
recursive: false,
|
||||
plugins: [
|
||||
new PuppeteerPlugin({
|
||||
launchOptions: { headless: true, args: ['--no-sandbox'] },
|
||||
scrollToBottom: { timeout: 15000, viewportN: 10 },
|
||||
blockNavigation: false
|
||||
})
|
||||
],
|
||||
// Sources list covering Salient/WooCommerce lazy assets
|
||||
sources: [
|
||||
{ selector: 'img', attr: 'src' },
|
||||
{ selector: 'img', attr: 'srcset' },
|
||||
{ selector: 'img', attr: 'data-src' },
|
||||
{ selector: 'img', attr: 'data-lazy-src' },
|
||||
{ selector: 'link[rel="stylesheet"]', attr: 'href' },
|
||||
{ selector: 'link[rel="preload"]', attr: 'href' },
|
||||
{ selector: 'script', attr: 'src' },
|
||||
{ selector: 'video', attr: 'src' },
|
||||
{ selector: 'video', attr: 'poster' },
|
||||
{ selector: 'source', attr: 'src' },
|
||||
{ selector: 'source', attr: 'srcset' },
|
||||
{ selector: 'iframe', attr: 'src' },
|
||||
{ selector: 'meta[property="og:image"]', attr: 'content' },
|
||||
{ selector: '[style*="background-image"]', attr: 'style' }
|
||||
],
|
||||
// Shared directory for assets
|
||||
subdirectories: [
|
||||
{ directory: 'assets/img', extensions: ['.jpg', '.png', '.svg', '.webp', '.gif', '.ico'] },
|
||||
{ directory: 'assets/js', extensions: ['.js'] },
|
||||
{ directory: 'assets/css', extensions: ['.css'] },
|
||||
{ directory: 'assets/fonts', extensions: ['.woff', '.woff2', '.ttf', '.eot'] },
|
||||
{ directory: 'assets/media', extensions: ['.mp4', '.webm'] }
|
||||
],
|
||||
request: {
|
||||
headers: {
|
||||
'User-Agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/122.0.0.0 Safari/537.36'
|
||||
}
|
||||
},
|
||||
urlFilter: (url: string) => {
|
||||
const u = new URL(url);
|
||||
// Allow domain assets, google fonts, and common cdn/upload patterns
|
||||
return u.hostname === domain ||
|
||||
u.hostname.includes('fonts.googleapis.com') ||
|
||||
u.hostname.includes('fonts.gstatic.com') ||
|
||||
url.includes('wp-content') ||
|
||||
url.includes('wp-includes');
|
||||
}
|
||||
};
|
||||
|
||||
try {
|
||||
await scrape(options);
|
||||
|
||||
// Rename the downloaded index.html to our slug.html
|
||||
const downloadedHtml = path.join(tempDir, 'index.html');
|
||||
const targetHtmlPath = path.join(tempDir, htmlFilename);
|
||||
if (fs.existsSync(downloadedHtml)) {
|
||||
fs.renameSync(downloadedHtml, targetHtmlPath);
|
||||
}
|
||||
|
||||
// POST-PROCESS: Inject Fonts and fix paths in the HTML
|
||||
if (fs.existsSync(targetHtmlPath)) {
|
||||
let content = fs.readFileSync(targetHtmlPath, 'utf8');
|
||||
|
||||
// NUKE TYPOGRAPHY: Strong overrides for Salient theme
|
||||
const fontInjection = `
|
||||
<!-- INDUSTRIAL TYPOGRAPHY OVERRIDE -->
|
||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700;800;900&display=swap" rel="stylesheet">
|
||||
<style>
|
||||
:root {
|
||||
--main-font: 'Inter', sans-serif !important;
|
||||
--heading-font: 'Montserrat', sans-serif !important;
|
||||
--font-family-body: 'Inter', sans-serif !important;
|
||||
--font-family-heading: 'Montserrat', sans-serif !important;
|
||||
}
|
||||
body, p, li, a, span, label, input, textarea, .body-font {
|
||||
font-family: 'Inter', sans-serif !important;
|
||||
}
|
||||
h1, h2, h3, h4, h5, h6, .title-font, .heading-font, [class*="heading"] {
|
||||
font-family: 'Montserrat', sans-serif !important;
|
||||
font-weight: 700 !important;
|
||||
}
|
||||
/* Salient Specific Heading Classes */
|
||||
.nectar-milestone .number, .nectar-milestone .subject, .nectar-heading {
|
||||
font-family: 'Montserrat' !important;
|
||||
}
|
||||
</style>
|
||||
`;
|
||||
content = content.replace('</head>', `${fontInjection}</head>`);
|
||||
|
||||
// Link fix: Replace all anchor hrefs with # to prevent unintentional navigation/leaks
|
||||
content = content.replace(/<a\b([^>]*)\bhref=["'][^"']*["']/gi, '<a$1href="#"');
|
||||
|
||||
// Fix Breeze dynamic scripts (Salient optimization)
|
||||
content = content.replace(/<div[^>]+class="breeze-scripts-load"[^>]*>([^<]+)<\/div>/gi, (match, url) => {
|
||||
const u = url.trim();
|
||||
const cleanUrl = u.split('?')[0];
|
||||
if (cleanUrl.endsWith('.css')) return `<link rel="stylesheet" href="${u}">`;
|
||||
return `<script src="${u}"></script>`;
|
||||
});
|
||||
|
||||
fs.writeFileSync(targetHtmlPath, content);
|
||||
}
|
||||
|
||||
// MOVE AND MERGE into domainDir
|
||||
const merge = (src: string, dest: string) => {
|
||||
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
||||
fs.readdirSync(src).forEach(item => {
|
||||
const s = path.join(src, item);
|
||||
const d = path.join(dest, item);
|
||||
if (fs.statSync(s).isDirectory()) {
|
||||
merge(s, d);
|
||||
} else {
|
||||
// Copy file (overwrite if HTML, skip assets to avoid duplicate downloads)
|
||||
if (item.endsWith('.html') || !fs.existsSync(d)) {
|
||||
fs.copyFileSync(s, d);
|
||||
}
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
merge(tempDir, domainDir);
|
||||
fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
|
||||
console.log(`\n✅ SUCCESS: ${path.join(domainDir, htmlFilename)}`);
|
||||
|
||||
} catch (e) {
|
||||
console.error('❌ CLONE FAILED:', e);
|
||||
if (fs.existsSync(tempDir)) fs.rmSync(tempDir, { recursive: true, force: true });
|
||||
}
|
||||
}
|
||||
|
||||
run();
|
||||
@@ -77,15 +77,8 @@ async function run() {
|
||||
urls: [targetUrl],
|
||||
directory: outputDir,
|
||||
recursive: true,
|
||||
maxDepth: 10,
|
||||
maxDepth: 5,
|
||||
// Custom filename generation to avoid "https:/" folders
|
||||
// We use 'bySiteStructure' behavior but manually controlled via plugin
|
||||
// to forcefully strip protocol/domain issues if any.
|
||||
// Actually, let's just use 'bySiteStructure' but strictly configured?
|
||||
// No, the user saw garbage. Let's use 'byType' combined with preserving structure for HTML.
|
||||
|
||||
// BETTER STRATEGY:
|
||||
// Use a custom plugin to control filenames EXACTLY how we want.
|
||||
plugins: [
|
||||
new PuppeteerPlugin({
|
||||
launchOptions: {
|
||||
@@ -95,6 +88,16 @@ async function run() {
|
||||
scrollToBottom: { timeout: 10000, viewportN: 10 },
|
||||
blockNavigation: false
|
||||
}),
|
||||
new class LoggerPlugin {
|
||||
apply(registerAction: any) {
|
||||
registerAction('onResourceSaved', ({ resource }: any) => {
|
||||
console.log(` 💾 Saved: ${resource.url} -> ${resource.filename}`);
|
||||
});
|
||||
registerAction('onResourceError', ({ resource, error }: any) => {
|
||||
console.error(` ❌ Error: ${resource.url} - ${error.message}`);
|
||||
});
|
||||
}
|
||||
},
|
||||
new class FilenamePlugin {
|
||||
apply(registerAction: any) {
|
||||
registerAction('generateFilename', ({ resource }: any) => {
|
||||
@@ -130,9 +133,9 @@ async function run() {
|
||||
const isTargetDomain = u.hostname === domain;
|
||||
const isGoogleFonts = u.hostname.includes('fonts.googleapis.com') || u.hostname.includes('fonts.gstatic.com');
|
||||
// Allow assets from anywhere
|
||||
const isAsset = /\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|mp4|webm|ico|json)$/i.test(u.pathname);
|
||||
const isAsset = /\.(css|js|png|jpg|jpeg|gif|svg|woff|woff2|ttf|eot|mp4|webm|ico|json|webp)$/i.test(u.pathname);
|
||||
// Allow fonts/css from common CDNs if standard extension check fails
|
||||
const isCommonAsset = u.pathname.includes('/css/') || u.pathname.includes('/js/') || u.pathname.includes('/static/') || u.pathname.includes('/assets/');
|
||||
const isCommonAsset = u.pathname.includes('/css/') || u.pathname.includes('/js/') || u.pathname.includes('/static/') || u.pathname.includes('/assets/') || u.pathname.includes('/uploads/');
|
||||
|
||||
return isTargetDomain || isAsset || isCommonAsset || isGoogleFonts;
|
||||
},
|
||||
@@ -144,6 +147,8 @@ async function run() {
|
||||
{ selector: 'source', attr: 'src' },
|
||||
{ selector: 'source', attr: 'srcset' },
|
||||
{ selector: 'link[rel="stylesheet"]', attr: 'href' },
|
||||
{ selector: 'link[rel="preload"]', attr: 'href' },
|
||||
{ selector: 'link[rel="prefetch"]', attr: 'href' },
|
||||
{ selector: 'script', attr: 'src' },
|
||||
{ selector: 'video', attr: 'src' },
|
||||
{ selector: 'video', attr: 'poster' },
|
||||
@@ -194,14 +199,43 @@ function sanitizeHtmlFiles(dir: string) {
|
||||
content = content.replace(/<script[^>]+src="[^"]*\/_next\/static\/chunks\/[^"]*"[^>]*><\/script>/gi, '');
|
||||
content = content.replace(/<script[^>]+src="[^"]*\/_next\/static\/[^"]*Manifest\.js"[^>]*><\/script>/gi, '');
|
||||
|
||||
// Convert Breeze dynamic script/styles into actual tags if possible
|
||||
// match <div class="breeze-scripts-load" ...>URL</div>
|
||||
content = content.replace(/<div[^>]+class="breeze-scripts-load"[^>]*>([^<]+)<\/div>/gi, (match, url) => {
|
||||
if (url.endsWith('.css')) return `<link rel="stylesheet" href="${url}">`;
|
||||
return `<script src="${url}"></script>`;
|
||||
});
|
||||
|
||||
// Inject Fonts (Fix for missing dynamic fonts)
|
||||
// We inject Inter as a safe default for modern Next.js/Tailwind sites if strictly missing
|
||||
if (!content.includes('fonts.googleapis.com')) {
|
||||
const fontLink = `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap">`;
|
||||
const styleBlock = `<style>.body-font{font-family:'Inter',sans-serif;}.title-font{font-family:'Inter',sans-serif;}</style>`;
|
||||
// We inject Inter and Montserrat as safe defaults for industrial/modern sites
|
||||
// Check specifically for a stylesheet link to google fonts
|
||||
const hasGoogleFontStylesheet = /<link[^>]+rel="stylesheet"[^>]+href="[^"]*fonts\.googleapis\.com/i.test(content);
|
||||
if (!hasGoogleFontStylesheet) {
|
||||
const fontLink = `<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&family=Montserrat:wght@300;400;500;600;700&display=swap">`;
|
||||
const styleBlock = `<style>
|
||||
:root { --main-font: 'Inter', sans-serif; --heading-font: 'Montserrat', sans-serif; }
|
||||
body, .body-font, p, span, li, a { font-family: var(--main-font) !important; }
|
||||
h1, h2, h3, h4, h5, h6, .title-font, .heading-font { font-family: var(--heading-font) !important; }
|
||||
</style>`;
|
||||
content = content.replace('</head>', `${fontLink}${styleBlock}</head>`);
|
||||
}
|
||||
|
||||
// Force column layout on product pages
|
||||
if (content.includes('class="products')) {
|
||||
const layoutScript = `
|
||||
<script>
|
||||
document.addEventListener('DOMContentLoaded', function() {
|
||||
const products = document.querySelector('.products');
|
||||
if (products) {
|
||||
products.classList.remove(...Array.from(products.classList).filter(c => c.startsWith('columns-')));
|
||||
products.classList.add('columns-1');
|
||||
products.setAttribute('data-n-desktop-columns', '1');
|
||||
}
|
||||
});
|
||||
</script>`;
|
||||
content = content.replace('</body>', `${layoutScript}</body>`);
|
||||
}
|
||||
|
||||
fs.writeFileSync(fullPath, content);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,98 +0,0 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Final verification test for embed components
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
console.log('🔍 Final Embed Component Verification\n');
|
||||
|
||||
const tests = [
|
||||
{
|
||||
name: 'Components Exist',
|
||||
test: () => {
|
||||
const files = ['YouTubeEmbed.astro', 'TwitterEmbed.astro', 'GenericEmbed.astro'];
|
||||
const allExist = files.every(file => existsSync(join(process.cwd(), 'src', 'components', file)));
|
||||
return allExist ? '✅ All components created' : '❌ Missing components';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Demo Post Exists',
|
||||
test: () => {
|
||||
const exists = existsSync(join(process.cwd(), 'src', 'pages', 'blog', 'embed-demo.astro'));
|
||||
return exists ? '✅ Demo post created' : '❌ Demo post missing';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Blog Posts Updated',
|
||||
test: () => {
|
||||
const content = readFileSync(join(process.cwd(), 'src', 'data', 'blogPosts.ts'), 'utf-8');
|
||||
return content.includes('embed-demo') ? '✅ embed-demo added to blogPosts' : '❌ embed-demo not in blogPosts';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'YouTube Component Valid',
|
||||
test: () => {
|
||||
const content = readFileSync(join(process.cwd(), 'src', 'components', 'YouTubeEmbed.astro'), 'utf-8');
|
||||
const hasIssues = content.includes('IntersectionObserver') || content.includes('timeout:');
|
||||
return !hasIssues ? '✅ YouTube component clean' : '⚠️ YouTube has potential issues';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Twitter Component Valid',
|
||||
test: () => {
|
||||
const content = readFileSync(join(process.cwd(), 'src', 'components', 'TwitterEmbed.astro'), 'utf-8');
|
||||
const hasFallback = content.includes('fallbackHtml');
|
||||
const hasErrorHandling = content.includes('try') && content.includes('catch');
|
||||
return hasFallback && hasErrorHandling ? '✅ Twitter component robust' : '❌ Twitter needs fixes';
|
||||
}
|
||||
},
|
||||
{
|
||||
name: 'Generic Component Valid',
|
||||
test: () => {
|
||||
const content = readFileSync(join(process.cwd(), 'src', 'components', 'GenericEmbed.astro'), 'utf-8');
|
||||
const hasEndpoints = content.includes('oEmbedEndpoints');
|
||||
const hasFallback = content.includes('fallbackHtml');
|
||||
return hasEndpoints && hasFallback ? '✅ Generic component robust' : '❌ Generic needs fixes';
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
let allPassed = true;
|
||||
tests.forEach(test => {
|
||||
const result = test.test();
|
||||
console.log(result);
|
||||
if (result.includes('❌')) allPassed = false;
|
||||
});
|
||||
|
||||
console.log('\n' + '='.repeat(70));
|
||||
|
||||
if (allPassed) {
|
||||
console.log('🎉 SUCCESS! All components are ready to use.');
|
||||
console.log('\n📋 WHAT WAS CREATED:');
|
||||
console.log('• YouTubeEmbed.astro - YouTube videos with full styling control');
|
||||
console.log('• TwitterEmbed.astro - Twitter/X tweets with oEmbed API');
|
||||
console.log('• GenericEmbed.astro - Universal oEmbed support');
|
||||
console.log('• embed-demo.astro - Working example post');
|
||||
console.log('• Updated blogPosts.ts - Added demo to blog list');
|
||||
|
||||
console.log('\n🚀 HOW TO TEST:');
|
||||
console.log('1. Start dev server: npm run dev');
|
||||
console.log('2. Visit: http://localhost:4321/blog/embed-demo');
|
||||
console.log('3. Or visit: http://localhost:4321/ and click "Rich Content Embedding Demo"');
|
||||
|
||||
console.log('\n💡 USAGE EXAMPLES:');
|
||||
console.log('YouTube: <YouTubeEmbed videoId="abc123" style="minimal" />');
|
||||
console.log('Twitter: <TwitterEmbed tweetId="123456789" theme="dark" />');
|
||||
console.log('Generic: <GenericEmbed url="https://vimeo.com/123" type="video" />');
|
||||
|
||||
console.log('\n🎨 STYLING CONTROL:');
|
||||
console.log('Use CSS variables or data attributes for full customization');
|
||||
console.log('All components match your blog\'s Tailwind aesthetic');
|
||||
|
||||
} else {
|
||||
console.log('❌ Some issues detected. Check the results above.');
|
||||
}
|
||||
|
||||
process.exit(allPassed ? 0 : 1);
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,214 +0,0 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Comprehensive test for embed components
|
||||
*/
|
||||
|
||||
import { readFileSync, existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const componentsDir = join(process.cwd(), 'src', 'components');
|
||||
|
||||
interface TestResult {
|
||||
name: string;
|
||||
passed: boolean;
|
||||
details: string;
|
||||
}
|
||||
|
||||
const results: TestResult[] = [];
|
||||
|
||||
// Test 1: Check component files exist
|
||||
function testComponentFiles(): TestResult {
|
||||
const files = [
|
||||
'YouTubeEmbed.astro',
|
||||
'TwitterEmbed.astro',
|
||||
'GenericEmbed.astro'
|
||||
];
|
||||
|
||||
const missing = files.filter(file => !existsSync(join(componentsDir, file)));
|
||||
|
||||
if (missing.length === 0) {
|
||||
return {
|
||||
name: 'Component Files Exist',
|
||||
passed: true,
|
||||
details: 'All embed components found'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Component Files Exist',
|
||||
passed: false,
|
||||
details: `Missing: ${missing.join(', ')}`
|
||||
};
|
||||
}
|
||||
|
||||
// Test 2: Check component structure
|
||||
function testComponentStructure(): TestResult {
|
||||
const youtubeContent = readFileSync(join(componentsDir, 'YouTubeEmbed.astro'), 'utf-8');
|
||||
const twitterContent = readFileSync(join(componentsDir, 'TwitterEmbed.astro'), 'utf-8');
|
||||
const genericContent = readFileSync(join(componentsDir, 'GenericEmbed.astro'), 'utf-8');
|
||||
|
||||
const issues: string[] = [];
|
||||
|
||||
// Check YouTube structure
|
||||
if (!youtubeContent.includes('interface Props')) issues.push('YouTube: Missing Props interface');
|
||||
if (!youtubeContent.includes('embedUrl')) issues.push('YouTube: Missing embed URL');
|
||||
if (!youtubeContent.includes('<iframe')) issues.push('YouTube: Missing iframe');
|
||||
|
||||
// Check Twitter structure
|
||||
if (!twitterContent.includes('oEmbed')) issues.push('Twitter: Missing oEmbed logic');
|
||||
if (!twitterContent.includes('fallbackHtml')) issues.push('Twitter: Missing fallback');
|
||||
if (!twitterContent.includes('set:html')) issues.push('Twitter: Missing HTML injection');
|
||||
|
||||
// Check Generic structure
|
||||
if (!genericContent.includes('oEmbedEndpoints')) issues.push('Generic: Missing oEmbed endpoints');
|
||||
if (!genericContent.includes('provider')) issues.push('Generic: Missing provider detection');
|
||||
|
||||
if (issues.length === 0) {
|
||||
return {
|
||||
name: 'Component Structure',
|
||||
passed: true,
|
||||
details: 'All components have correct structure'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Component Structure',
|
||||
passed: false,
|
||||
details: issues.join('; ')
|
||||
};
|
||||
}
|
||||
|
||||
// Test 3: Check for common issues
|
||||
function testCommonIssues(): TestResult {
|
||||
const youtubeContent = readFileSync(join(componentsDir, 'YouTubeEmbed.astro'), 'utf-8');
|
||||
const twitterContent = readFileSync(join(componentsDir, 'TwitterEmbed.astro'), 'utf-8');
|
||||
|
||||
const issues: string[] = [];
|
||||
|
||||
// Check for problematic Intersection Observer
|
||||
if (youtubeContent.includes('IntersectionObserver')) {
|
||||
issues.push('YouTube: Still using IntersectionObserver (may cause blinking)');
|
||||
}
|
||||
|
||||
// Check for timeout issues
|
||||
if (twitterContent.includes('timeout:')) {
|
||||
issues.push('Twitter: Has timeout property (not supported in fetch)');
|
||||
}
|
||||
|
||||
// Check for proper error handling
|
||||
if (!twitterContent.includes('try') || !twitterContent.includes('catch')) {
|
||||
issues.push('Twitter: Missing try/catch error handling');
|
||||
}
|
||||
|
||||
if (issues.length === 0) {
|
||||
return {
|
||||
name: 'Common Issues Check',
|
||||
passed: true,
|
||||
details: 'No common issues detected'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Common Issues Check',
|
||||
passed: false,
|
||||
details: issues.join('; ')
|
||||
};
|
||||
}
|
||||
|
||||
// Test 4: Check demo post exists
|
||||
function testDemoPost(): TestResult {
|
||||
const demoPath = join(process.cwd(), 'src', 'pages', 'blog', 'embed-demo.astro');
|
||||
|
||||
if (existsSync(demoPath)) {
|
||||
const content = readFileSync(demoPath, 'utf-8');
|
||||
|
||||
const hasYouTube = content.includes('YouTubeEmbed');
|
||||
const hasTwitter = content.includes('TwitterEmbed');
|
||||
const hasGeneric = content.includes('GenericEmbed');
|
||||
|
||||
if (hasYouTube && hasTwitter && hasGeneric) {
|
||||
return {
|
||||
name: 'Demo Post',
|
||||
passed: true,
|
||||
details: 'Demo post exists with all components'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Demo Post',
|
||||
passed: false,
|
||||
details: `Missing components: ${!hasYouTube ? 'YouTube' : ''} ${!hasTwitter ? 'Twitter' : ''} ${!hasGeneric ? 'Generic' : ''}`
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Demo Post',
|
||||
passed: false,
|
||||
details: 'Demo post file not found'
|
||||
};
|
||||
}
|
||||
|
||||
// Test 5: Check blogPosts array
|
||||
function testBlogPostsArray(): TestResult {
|
||||
const blogPostsPath = join(process.cwd(), 'src', 'data', 'blogPosts.ts');
|
||||
|
||||
if (!existsSync(blogPostsPath)) {
|
||||
return {
|
||||
name: 'Blog Posts Array',
|
||||
passed: false,
|
||||
details: 'blogPosts.ts not found'
|
||||
};
|
||||
}
|
||||
|
||||
const content = readFileSync(blogPostsPath, 'utf-8');
|
||||
|
||||
if (content.includes('embed-demo')) {
|
||||
return {
|
||||
name: 'Blog Posts Array',
|
||||
passed: true,
|
||||
details: 'embed-demo found in blogPosts array'
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
name: 'Blog Posts Array',
|
||||
passed: false,
|
||||
details: 'embed-demo not found in blogPosts array'
|
||||
};
|
||||
}
|
||||
|
||||
// Run all tests
|
||||
console.log('🔍 Running Comprehensive Embed Component Tests...\n');
|
||||
|
||||
results.push(testComponentFiles());
|
||||
results.push(testComponentStructure());
|
||||
results.push(testCommonIssues());
|
||||
results.push(testDemoPost());
|
||||
results.push(testBlogPostsArray());
|
||||
|
||||
// Display results
|
||||
let allPassed = true;
|
||||
results.forEach(result => {
|
||||
const icon = result.passed ? '✅' : '❌';
|
||||
console.log(`${icon} ${result.name}`);
|
||||
console.log(` ${result.details}`);
|
||||
if (!result.passed) allPassed = false;
|
||||
});
|
||||
|
||||
console.log('\n' + '='.repeat(60));
|
||||
|
||||
if (allPassed) {
|
||||
console.log('🎉 All tests passed! Components should work correctly.');
|
||||
console.log('\nNext steps:');
|
||||
console.log('1. Visit http://localhost:4321/blog/embed-demo');
|
||||
console.log('2. Check browser console for any errors');
|
||||
console.log('3. If Twitter fails, it needs internet during build');
|
||||
} else {
|
||||
console.log('❌ Some tests failed. Check the details above.');
|
||||
console.log('\nCommon fixes:');
|
||||
console.log('- Restart dev server: npm run dev');
|
||||
console.log('- Check internet connection for Twitter oEmbed');
|
||||
console.log('- Verify video IDs and tweet IDs are valid');
|
||||
}
|
||||
|
||||
process.exit(allPassed ? 0 : 1);
|
||||
@@ -1,77 +0,0 @@
|
||||
#!/usr/bin/env tsx
|
||||
/**
|
||||
* Test script to verify embed components are properly structured
|
||||
*/
|
||||
|
||||
import { existsSync } from 'fs';
|
||||
import { join } from 'path';
|
||||
|
||||
const componentsDir = join(process.cwd(), 'src', 'components');
|
||||
const embedsDir = join(componentsDir, 'Embeds');
|
||||
|
||||
const requiredFiles = [
|
||||
'YouTubeEmbed.astro',
|
||||
'TwitterEmbed.astro',
|
||||
'GenericEmbed.astro',
|
||||
'Embeds/index.ts'
|
||||
];
|
||||
|
||||
const optionalFiles = [
|
||||
'embedDemoPost.ts'
|
||||
];
|
||||
|
||||
console.log('🔍 Testing Embed Components...\n');
|
||||
|
||||
let allPassed = true;
|
||||
|
||||
// Check component files exist
|
||||
requiredFiles.forEach(file => {
|
||||
const filePath = join(process.cwd(), 'src', 'components', file.replace('Embeds/', ''));
|
||||
const exists = existsSync(filePath);
|
||||
|
||||
if (exists) {
|
||||
console.log(`✅ ${file}`);
|
||||
} else {
|
||||
console.log(`❌ ${file} - MISSING`);
|
||||
allPassed = false;
|
||||
}
|
||||
});
|
||||
|
||||
// Check optional files
|
||||
optionalFiles.forEach(file => {
|
||||
const filePath = join(process.cwd(), 'src', 'data', file);
|
||||
const exists = existsSync(filePath);
|
||||
|
||||
if (exists) {
|
||||
console.log(`✅ ${file} (optional)`);
|
||||
} else {
|
||||
console.log(`⚠️ ${file} - not found (optional)`);
|
||||
}
|
||||
});
|
||||
|
||||
// Check embeds directory
|
||||
if (existsSync(embedsDir)) {
|
||||
console.log(`✅ Embeds directory exists`);
|
||||
} else {
|
||||
console.log(`❌ Embeds directory - MISSING`);
|
||||
allPassed = false;
|
||||
}
|
||||
|
||||
console.log('\n📋 Component Summary:');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
console.log('YouTubeEmbed - Build-time YouTube embeds with full styling control');
|
||||
console.log('TwitterEmbed - Build-time Twitter embeds with oEmbed API');
|
||||
console.log('GenericEmbed - Universal oEmbed support for any provider');
|
||||
console.log('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━');
|
||||
|
||||
if (allPassed) {
|
||||
console.log('\n🎉 All required components are in place!');
|
||||
console.log('\nNext steps:');
|
||||
console.log('1. Add embeds to your blog posts');
|
||||
console.log('2. Customize styling with CSS variables');
|
||||
console.log('3. Check EMBED_USAGE_GUIDE.md for examples');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n❌ Some components are missing. Please check the files above.');
|
||||
process.exit(1);
|
||||
}
|
||||
@@ -1,37 +0,0 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Comprehensive test suite for file examples system
|
||||
* Run this separately: npm run test:file-examples
|
||||
*/
|
||||
|
||||
import { runFileExamplesTests } from '../src/utils/test-file-examples';
|
||||
import { testBlogPostIntegration } from '../src/utils/test-component-integration';
|
||||
|
||||
async function runComprehensiveTests() {
|
||||
console.log('🧪 Running Comprehensive File Examples Test Suite...\n');
|
||||
|
||||
console.log('1️⃣ Testing File Examples Data System');
|
||||
console.log('─'.repeat(50));
|
||||
await runFileExamplesTests();
|
||||
|
||||
console.log('\n2️⃣ Testing Blog Post Integration');
|
||||
console.log('─'.repeat(50));
|
||||
await testBlogPostIntegration();
|
||||
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log('🎉 ALL COMPREHENSIVE TESTS PASSED!');
|
||||
console.log('='.repeat(50));
|
||||
console.log('\nThe file examples system is fully functional:');
|
||||
console.log(' ✅ Data structure and manager');
|
||||
console.log(' ✅ Filtering by postSlug, groupId, and tags');
|
||||
console.log(' ✅ Copy and download functionality');
|
||||
console.log(' ✅ ZIP download for multiple files');
|
||||
console.log(' ✅ Integration with blog posts');
|
||||
console.log(' ✅ All blog posts have correct file examples');
|
||||
}
|
||||
|
||||
runComprehensiveTests().catch(err => {
|
||||
console.error('❌ Test failed:', err);
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,247 +0,0 @@
|
||||
#!/usr/bin/env tsx
|
||||
|
||||
/**
|
||||
* Updated link test for the blog with file examples
|
||||
* Tests: All references are valid, files exist
|
||||
*/
|
||||
|
||||
import fs from 'fs';
|
||||
import path from 'path';
|
||||
|
||||
console.log('🔗 Checking links and references...\n');
|
||||
|
||||
let passed = 0;
|
||||
let failed = 0;
|
||||
|
||||
function test(name: string, fn: () => void): void {
|
||||
try {
|
||||
fn();
|
||||
console.log(`✅ ${name}`);
|
||||
passed++;
|
||||
} catch (error) {
|
||||
console.log(`❌ ${name}`);
|
||||
if (error instanceof Error) {
|
||||
console.log(` Error: ${error.message}`);
|
||||
}
|
||||
failed++;
|
||||
}
|
||||
}
|
||||
|
||||
// Test 1: Check that blog posts reference valid data
|
||||
test('Blog posts reference valid data', () => {
|
||||
const blogPostsPath = path.join(process.cwd(), 'src/data/blogPosts.ts');
|
||||
const content = fs.readFileSync(blogPostsPath, 'utf8');
|
||||
|
||||
// Extract all slugs
|
||||
const slugMatches = content.match(/slug:\s*['"]([^'"]+)['"]/g) || [];
|
||||
const slugs = slugMatches.map(m => m.match(/['"]([^'"]+)['"]/)?.[1]);
|
||||
|
||||
if (slugs.length === 0) {
|
||||
throw new Error('No slugs found in blogPosts.ts');
|
||||
}
|
||||
|
||||
// Verify [slug].astro exists for dynamic routing
|
||||
const slugPagePath = path.join(process.cwd(), 'src/pages/blog/[slug].astro');
|
||||
if (!fs.existsSync(slugPagePath)) {
|
||||
throw new Error('Dynamic slug page [slug].astro does not exist');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 2: Verify tag references are valid
|
||||
test('Tag references are valid', () => {
|
||||
const blogPostsPath = path.join(process.cwd(), 'src/data/blogPosts.ts');
|
||||
const content = fs.readFileSync(blogPostsPath, 'utf8');
|
||||
|
||||
// Extract all tags
|
||||
const tagMatches = content.match(/tags:\s*\[([^\]]+)\]/g) || [];
|
||||
|
||||
if (tagMatches.length === 0) {
|
||||
throw new Error('No tags found in blogPosts.ts');
|
||||
}
|
||||
|
||||
// Verify tag page exists
|
||||
const tagPagePath = path.join(process.cwd(), 'src/pages/tags/[tag].astro');
|
||||
if (!fs.existsSync(tagPagePath)) {
|
||||
throw new Error('Tag page [tag].astro does not exist');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 3: Verify all component imports are valid
|
||||
test('All component imports are valid', () => {
|
||||
const components = [
|
||||
'src/components/MediumCard.astro',
|
||||
'src/components/SearchBar.tsx',
|
||||
'src/components/ArticleBlockquote.tsx',
|
||||
'src/components/ArticleHeading.tsx',
|
||||
'src/components/ArticleParagraph.tsx',
|
||||
'src/components/ArticleList.tsx',
|
||||
'src/components/Footer.tsx',
|
||||
'src/components/Hero.tsx',
|
||||
'src/components/Tag.astro',
|
||||
'src/components/FileExample.astro',
|
||||
'src/components/FileExamplesList.astro'
|
||||
];
|
||||
|
||||
for (const component of components) {
|
||||
const componentPath = path.join(process.cwd(), component);
|
||||
if (!fs.existsSync(componentPath)) {
|
||||
throw new Error(`Component missing: ${component}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 4: Verify all required pages exist
|
||||
test('All required pages exist', () => {
|
||||
const requiredPages = [
|
||||
'src/pages/index.astro',
|
||||
'src/pages/blog/[slug].astro',
|
||||
'src/pages/tags/[tag].astro',
|
||||
'src/pages/api/download-zip.ts'
|
||||
];
|
||||
|
||||
for (const page of requiredPages) {
|
||||
const pagePath = path.join(process.cwd(), page);
|
||||
if (!fs.existsSync(pagePath)) {
|
||||
throw new Error(`Required page missing: ${page}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 5: Verify layout files are valid
|
||||
test('Layout files are valid', () => {
|
||||
const layoutPath = path.join(process.cwd(), 'src/layouts/BaseLayout.astro');
|
||||
|
||||
if (!fs.existsSync(layoutPath)) {
|
||||
throw new Error('Layout missing: src/layouts/BaseLayout.astro');
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(layoutPath, 'utf8');
|
||||
|
||||
if (!content.includes('<html') || !content.includes('</html>')) {
|
||||
throw new Error('BaseLayout does not contain proper HTML structure');
|
||||
}
|
||||
|
||||
if (!content.includes('<head') || !content.includes('</head>')) {
|
||||
throw new Error('BaseLayout missing head section');
|
||||
}
|
||||
|
||||
if (!content.includes('<body') || !content.includes('</body>')) {
|
||||
throw new Error('BaseLayout missing body section');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 6: Verify global styles are properly imported
|
||||
test('Global styles are properly imported', () => {
|
||||
const stylesPath = path.join(process.cwd(), 'src/styles/global.css');
|
||||
|
||||
if (!fs.existsSync(stylesPath)) {
|
||||
throw new Error('Global styles file missing');
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(stylesPath, 'utf8');
|
||||
|
||||
// Check for Tailwind imports
|
||||
if (!content.includes('@tailwind base') || !content.includes('@tailwind components') || !content.includes('@tailwind utilities')) {
|
||||
throw new Error('Global styles missing Tailwind imports');
|
||||
}
|
||||
|
||||
// Check for required classes
|
||||
const requiredClasses = ['.container', '.post-card', '.highlighter-tag'];
|
||||
for (const className of requiredClasses) {
|
||||
if (!content.includes(className)) {
|
||||
throw new Error(`Global styles missing required class: ${className}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 7: Verify file examples data structure
|
||||
test('File examples data structure is valid', () => {
|
||||
const fileExamplesPath = path.join(process.cwd(), 'src/data/fileExamples.ts');
|
||||
|
||||
if (!fs.existsSync(fileExamplesPath)) {
|
||||
throw new Error('File examples data file missing');
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(fileExamplesPath, 'utf8');
|
||||
|
||||
if (!content.includes('export interface FileExample')) {
|
||||
throw new Error('FileExample interface not exported');
|
||||
}
|
||||
|
||||
if (!content.includes('export const sampleFileExamples')) {
|
||||
throw new Error('sampleFileExamples not exported');
|
||||
}
|
||||
|
||||
// Check for required fields in interface
|
||||
const requiredFields = ['id', 'filename', 'content', 'language'];
|
||||
for (const field of requiredFields) {
|
||||
if (!content.includes(field)) {
|
||||
throw new Error(`FileExample interface missing field: ${field}`);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Test 8: Verify API endpoint structure
|
||||
test('API endpoint structure is valid', () => {
|
||||
const apiPath = path.join(process.cwd(), 'src/pages/api/download-zip.ts');
|
||||
|
||||
if (!fs.existsSync(apiPath)) {
|
||||
throw new Error('API endpoint missing');
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(apiPath, 'utf8');
|
||||
|
||||
if (!content.includes('export const POST')) {
|
||||
throw new Error('API missing POST handler');
|
||||
}
|
||||
|
||||
if (!content.includes('export const GET')) {
|
||||
throw new Error('API missing GET handler');
|
||||
}
|
||||
|
||||
if (!content.includes('FileExampleManager')) {
|
||||
throw new Error('API does not use FileExampleManager');
|
||||
}
|
||||
});
|
||||
|
||||
// Test 9: Verify blog template imports file examples
|
||||
test('Blog template imports file examples', () => {
|
||||
const blogTemplatePath = path.join(process.cwd(), 'src/pages/blog/[slug].astro');
|
||||
|
||||
if (!fs.existsSync(blogTemplatePath)) {
|
||||
throw new Error('Blog template missing');
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(blogTemplatePath, 'utf8');
|
||||
|
||||
if (!content.includes('FileExamplesList')) {
|
||||
throw new Error('Blog template does not import FileExamplesList');
|
||||
}
|
||||
|
||||
if (!content.includes('FileExamplesList')) {
|
||||
throw new Error('Blog template does not reference file examples');
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
// Summary
|
||||
console.log('\n' + '='.repeat(50));
|
||||
console.log(`Tests passed: ${passed}`);
|
||||
console.log(`Tests failed: ${failed}`);
|
||||
console.log('='.repeat(50));
|
||||
|
||||
if (failed === 0) {
|
||||
console.log('\n🎉 All link checks passed! All references are valid.');
|
||||
console.log('\nVerified:');
|
||||
console.log(' ✅ Blog posts data and routing');
|
||||
console.log(' ✅ Tag filtering system');
|
||||
console.log(' ✅ All components exist');
|
||||
console.log(' ✅ All pages exist');
|
||||
console.log(' ✅ Layout structure');
|
||||
console.log(' ✅ File examples functionality');
|
||||
console.log(' ✅ API endpoints');
|
||||
process.exit(0);
|
||||
} else {
|
||||
console.log('\n❌ Some checks failed. Please fix the errors above.');
|
||||
process.exit(1);
|
||||
}
|
||||
Reference in New Issue
Block a user