first commit

This commit is contained in:
amirhosein.ashourloo
2026-09-15 20:37:49 +03:30
commit dee487ad73
91 changed files with 7365 additions and 0 deletions
+32
View File
@@ -0,0 +1,32 @@
import { mkdir, readdir, writeFile } from 'node:fs/promises';
import { dirname, join, relative, resolve, sep } from 'node:path';
import { fileURLToPath } from 'node:url';
const projectRoot = resolve(dirname(fileURLToPath(import.meta.url)), '..');
const appRoot = join(projectRoot, 'app');
const outputFile = join(projectRoot, 'public', 'meshkee', 'sitemap-config.json');
const pageNames = new Set(['page.js','page.jsx','page.ts','page.tsx']);
async function findPages(directory) {
const entries = await readdir(directory,{withFileTypes:true});
const pages=[];
for(const entry of entries){
const full=join(directory,entry.name);
if(entry.isDirectory()) pages.push(...await findPages(full));
else if(pageNames.has(entry.name)) pages.push(full);
}
return pages;
}
function toRoute(file) {
const segments=relative(appRoot,dirname(file)).split(sep).filter(Boolean);
if(segments.some(segment=>segment.startsWith('[')||segment.startsWith('(')||segment==='api'))return null;
return segments.length?`/${segments.join('/')}`:'/';
}
const priorities={ '/':1, '/portfolios':.9, '/ourservices':.8, '/blog':.8, '/aboutus':.7, '/contactus':.6 };
const routes=(await findPages(appRoot)).map(toRoute).filter(Boolean).sort((a,b)=>a==='/'?-1:a.localeCompare(b));
const staticPages=[...new Set(routes)].map(path=>({path,...(path==='/'?{changefreq:'weekly'}:{}),priority:priorities[path]??.5}));
await mkdir(dirname(outputFile),{recursive:true});
await writeFile(outputFile,`${JSON.stringify({baseUrl:'https://sanazgoodarzi.ir',staticPages},null,2)}\n`,'utf8');
console.log(`Generated Meshkee sitemap config with ${staticPages.length} static routes.`);
+5
View File
@@ -0,0 +1,5 @@
import { spawn } from 'node:child_process';
import { nodeRuntime } from './runtime.mjs';
const child = spawn(nodeRuntime(), ['node_modules/eslint/bin/eslint.js', '.'], {stdio:'inherit'});
child.on('error', error => {console.error(error.message);process.exit(1)});
child.on('exit', code => process.exit(code ?? 1));
+6
View File
@@ -0,0 +1,6 @@
import { spawn } from 'node:child_process';
import { nodeRuntime } from './runtime.mjs';
if (process.argv[2] === 'build') await import('./generate-meshkee-config.mjs');
const child = spawn(nodeRuntime(), ['node_modules/next/dist/bin/next', ...process.argv.slice(2)], { stdio: 'inherit', env: process.env });
child.on('error', error => { console.error(error.message); process.exit(1); });
child.on('exit', code => process.exit(code ?? 1));
+13
View File
@@ -0,0 +1,13 @@
import { existsSync } from 'node:fs';
import { homedir } from 'node:os';
import { join } from 'node:path';
// Laragon may expose Node 18. Prefer the current runtime, with the desktop's
// already-installed Node as a local development fallback; never download one.
export function nodeRuntime() {
const [major, minor] = process.versions.node.split('.').map(Number);
if (major > 20 || (major === 20 && minor >= 9)) return process.execPath;
const bundled = join(homedir(), '.cache', 'codex-runtimes', 'codex-primary-runtime', 'dependencies', 'node', 'bin', 'node.exe');
if (process.platform === 'win32' && existsSync(bundled)) return bundled;
throw new Error('Next.js requires Node.js 20.9 or newer. Use Node.js 24 LTS.');
}
+21
View File
@@ -0,0 +1,21 @@
import { createServer } from 'node:http';
import { createReadStream, existsSync } from 'node:fs';
import { stat } from 'node:fs/promises';
import { resolve, sep, extname } from 'node:path';
const root=resolve('out');
if(!existsSync(root)){console.error('Run npm run build before npm start.');process.exit(1)}
const types={'.html':'text/html; charset=utf-8','.js':'text/javascript; charset=utf-8','.css':'text/css; charset=utf-8','.json':'application/json','.txt':'text/plain; charset=utf-8','.png':'image/png','.jpg':'image/jpeg','.jpeg':'image/jpeg','.svg':'image/svg+xml','.woff':'font/woff','.woff2':'font/woff2','.ico':'image/x-icon'};
createServer(async(req,res)=>{
if(!['GET','HEAD'].includes(req.method)){res.writeHead(405,{Allow:'GET, HEAD'});res.end();return}
try{
const pathname=decodeURIComponent(new URL(req.url,'http://localhost').pathname);
let file=resolve(root,`.${pathname}`);
if(file!==root&&!file.startsWith(root+sep)){res.writeHead(403);res.end();return}
let info=await stat(file).catch(()=>null);
if(info?.isDirectory()){file=resolve(file,'index.html');info=await stat(file).catch(()=>null)}
const status=info?.isFile()?200:404;
if(status===404){file=resolve(root,'404.html');info=await stat(file)}
res.writeHead(status,{'Content-Type':types[extname(file)]??'application/octet-stream','Content-Length':info.size});
if(req.method==='HEAD')res.end();else createReadStream(file).pipe(res);
}catch{res.writeHead(400);res.end('Bad request')}
}).listen(Number(process.env.PORT??3000),'127.0.0.1',()=>console.log(`Static site: http://localhost:${process.env.PORT??3000}`));
+30
View File
@@ -0,0 +1,30 @@
import { readFile, stat } from 'node:fs/promises';
import { resolve, join } from 'node:path';
import assert from 'node:assert/strict';
const root=resolve('out');
const routes=['/','/aboutus/','/contactus/','/ourservices/','/portfolios/','/blog/',...['residential','beauty','commercial','landscape','facade'].map(slug=>`/portfolios/${slug}/`)];
const assets=new Set();
for(const route of routes){
const html=await readFile(join(root,route,'index.html'),'utf8');
assert.match(html,/<html[^>]*lang="fa"/);
assert.match(html,/<html[^>]*dir="rtl"/);
assert.ok(html.includes('گودرزی'),`Missing brand: ${route}`);
if (route === '/') assert.ok(!html.includes('class="site-footer"'), 'Home must not render a footer');
else assert.ok(html.includes('https://meshkee.com'),`Missing Meshkee credit: ${route}`);
for(const match of html.matchAll(/(?:src|href)="(\/[^"?#]*)/g)){
const url=decodeURIComponent(match[1]);
if(url.startsWith('//'))continue;
assets.add(url.endsWith('/')?`${url}index.html`:url);
}
const response=await fetch(`http://localhost:3000${route}`);
assert.equal(response.status,200,route);
}
for(const asset of assets){assert.ok((await stat(join(root,asset))).isFile(),asset)}
for(const font of ['iranyekanweblightfanum.woff','iranyekanwebboldfanum.woff']){
assert.equal((await readFile(join(root,'fonts',font))).subarray(0,4).toString(),'wOFF');
}
const missing=await fetch('http://localhost:3000/this-page-does-not-exist/');
assert.equal(missing.status,404);
assert.ok((await missing.text()).includes('این صفحه پیدا نشد'));
console.log(`PASS: ${routes.length} routes, ${assets.size} linked local files, both IRANYekan fonts, Persian RTL, Meshkee credit, and real HTTP 404.`);
+23
View File
@@ -0,0 +1,23 @@
import assert from 'node:assert/strict';
import { access, readFile } from 'node:fs/promises';
import { join, resolve } from 'node:path';
const output=resolve('out');
const routes=['/','/aboutus/','/blog/','/contactus/','/ourservices/','/portfolios/'];
const titles=new Set(); const descriptions=new Set();
for(const route of routes){
const html=await readFile(join(output,route,'index.html'),'utf8');
const title=html.match(/<title>([^<]+)<\/title>/)?.[1];
const description=html.match(/<meta name="description" content="([^"]+)"/)?.[1];
assert.ok(title,`Missing title: ${route}`); assert.ok(description,`Missing description: ${route}`);
assert.ok(!titles.has(title),`Duplicate title: ${title}`); assert.ok(!descriptions.has(description),`Duplicate description: ${description}`);
titles.add(title); descriptions.add(description);
assert.equal((html.match(/<h1(?:\s|>)/g)||[]).length,1,`Expected one H1: ${route}`);
assert.match(html,/<link rel="canonical" href="https:\/\/sanazgoodarzi\.ir\//,`Missing canonical: ${route}`);
for(const image of html.matchAll(/<img\b[^>]*>/g)) assert.match(image[0],/\salt="[^"]+"/,`Missing meaningful image alt in ${route}`);
}
const sitemap=JSON.parse(await readFile(join(output,'meshkee','sitemap-config.json'),'utf8'));
assert.equal(sitemap.baseUrl,'https://sanazgoodarzi.ir');
assert.deepEqual(sitemap.staticPages.map(page=>page.path),['/','/aboutus','/blog','/contactus','/ourservices','/portfolios']);
for(const forbidden of ['sitemap.xml','robots.txt']){try{await access(join(output,forbidden));assert.fail(`${forbidden} must be proxied, not shipped`)}catch(error){if(error.code!=='ENOENT')throw error}}
console.log(`PASS: SEO metadata, one H1, canonical URLs and image alt text on ${routes.length} crawlable static pages; sitemap config contains ${sitemap.staticPages.length} routes.`);