ساخت وبسایت کاتالوگ محصولات فولاد ایمان بههمراه پنل مدیریت - #2
Open
parhamofski46-cyber wants to merge 52 commits into
Open
ساخت وبسایت کاتالوگ محصولات فولاد ایمان بههمراه پنل مدیریت#2parhamofski46-cyber wants to merge 52 commits into
parhamofski46-cyber wants to merge 52 commits into
Conversation
سایت کامل کاتالوگ محصولات برای گروه تولیدی صنعتی فولاد ایمان (علیآباد کتول و گرگان) با استک Node.js + Express + SQLite + EJS + sharp. صفحات عمومی: - صفحهی اصلی با هیرو، دستهبندیها، محصولات پرفروش، بخش ویژهی فرفورژه، «چرا ما» و منطقهی خدمات - فهرست محصولات با فیلتر دسته/زیردسته، جستوجو و فیلتر «فقط موجودها» - صفحهی جزئیات محصول با گالری، وضعیت موجودی و دکمهی استعلام قیمت واتساپ - گالری اختصاصی فرفورژه (طرح نرده / درب / پنجره)، تماس با ما و صفحهی ۴۰۴ برندشده پنل مدیریت (/admin): - ورود امن با رمز هششدهی bcrypt و تغییر اجباری رمز پیشفرض در اولین ورود - افزودن/ویرایش/حذف محصول، تغییر سریع موجودی، مدیریت دسته و زیردسته - آپلود عکس با فرم ساده؛ تولید خودکار سه سایز WebP بهینه با sharp - ویرایش متنهای صفحهی اصلی و کد نقشه امنیت: محافظت CSRF روی همهی فرمها، محدودیت تلاش ورود، هدرهای helmet با CSP و nonce، کوکی httpOnly، بازسازی نشست بعد از ورود و noindex روی کل پنل. سرعت: رندر سمت سرور، فشردهسازی، فونت سلفهاست Vazirmatn، lazy-loading و srcset، بدون هیچ کتابخانهی سنگین سمت کاربر. سئوی محلی: Schema.org LocalBusiness با areaServed، Product schema با وضعیت موجودی، BreadcrumbList، متای اختصاصی هر صفحه، sitemap.xml و robots.txt پویا. ساختار کانالهای ارتباطی طوری نوشته شده که افزودن تلگرام در آینده فقط با تغییر یک تنظیم در src/config/site.js انجام شود. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
راههای ارتباطی: - تلگرام با یوزرنیم @parham_plg فعال شد؛ دکمهاش خودکار در هدر، فوتر، منوی موبایل، نوار پایین صفحه، صفحهی محصول و همهی بخشهای CTA ظاهر میشود بخشهای جدید صفحهی اصلی برای جلب اعتماد مشتری: - نوار آمار (سابقه، سفارش، مشتری، درصد رضایت) با اعداد قابل ویرایش از پنل - معرفی مدیر با عکس در قاب حرفهای: بلوک زنگاری پشت عکس، سایه، هالهی گرم و مدال «سال تجربه» — عکس هم از پنل قابل تعویض است - نظرات مشتریان با امتیاز ستارهای، میانگین امتیاز و دادهی ساختاریافتهی AggregateRating برای نمایش ستاره در نتایج گوگل - بخش تضمینها (شش مورد) و مراحل سفارش در سه قدم پنل مدیریت: - صفحهی «نظرات مشتریان»: افزودن، ویرایش، مخفیسازی، ترتیبدهی و حذف یکجای نمونهها - صفحهی «متنها و آمار»: ویرایش اعداد آمار و کل بخش معرفی مدیر + آپلود عکس مدیر - محافظت CSRF روی مسیر خروج هم اضافه شد بهبود موبایل و چیدمان: - منوی موبایل با پردهی تیرهی تمامصفحه، آیکون متحرک همبرگری به ضربدر، قفل اسکرول پسزمینه و دکمههای تماس داخل منو - جایگزینی دکمههای شناور با نوار تماس ثابت پایین صفحه در موبایل تا روی متن نیفتند - اصلاح اندازهی آیکونهای درونخطی و چیدمان سهستونی کارتهای تضمین - محدودکردن اندازهی عکس مدیر و حذف نقش پسزمینهی هیرو در موبایل - تستشده روی ۳۷۵، ۳۹۰، ۴۱۲ و ۷۶۸ پیکسل بدون اسکرول افقی نظرها و آمار نمونه با برچسب «(نمونه)» وارد شدهاند و در README تأکید شده که باید با دادهی واقعی جایگزین شوند. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
اصلاح مهم — فرفورژه تولید نمیشود: همهی متنهایی که ادعای ساخت و نصب فرفورژه در کارگاه داشتند بازنویسی شدند. فرفورژه بهصورت «بیش از ۱۰۰۰ مدل آماده در انبار» معرفی میشود که مشتری تحویل میگیرد و جوشکار خودش نصب میکند. شامل صفحهی فرفورژه، صفحهی اصلی، فوتر، بخش تضمینها، سؤالات پرتکرار، متای صفحات و توضیح محصولات. دستهها و محصولات جدید (۹ دسته، ۳۶ محصول): - ورق گالوانیزه تولید خودمان: طرح سفال رنگی، گالوانیزه شفاف، برش سفارشی - پیچ و یراقآلات: پیچ سرمته، قفل درب، لولای درب - تور مرغی به دستهی «فنس و توری» - پشم شیشه و فوم به دستهی «ایزوگام و عایق» - مهاجرت یکبارهی کاتالوگ (catalog_version) تا دیتابیسهای موجود هم دستههای جدید را بگیرند بدون اینکه دستههای حذفشده برگردند اطلاعات کسبوکار: - نام مدیر: علیاکبر پلنگ سنگدوینی — زیر عکس، در جدول تماس و اسکیمای گوگل - نشانی حضوری: علیآباد کتول، خیابان مزرعه، روبهروی آهنفروشی دیلمی — در فوتر، صفحهی تماس (کارت اختصاصی)، منطقهی خدمات و دادهی ساختاریافته - بازنویسی داستان مدیر با روایت واقعی کسبوکار آمار قابلراستیآزمایی بهجای عددهای ساختگی: عددهای «تعداد فروش» و «درصد رضایت» حذف و جای آنها ۱۰۰۰+ مدل فرفورژه، سال سابقه، تعداد گروه کالا و تعداد شهرهای تحت پوشش نشسته که دو مورد آخر خودکار از دیتابیس و تنظیمات محاسبه میشوند. قابلیت جدید — لیست استعلام: مشتری با دکمهی «+ لیست» چند محصول را جمع میکند و کل لیست را در یک پیام واتساپ میفرستد. بدون سبد خرید و بدون سرور؛ لیست در localStorage مرورگر خودش میماند. شامل پنل شناور، شمارنده، حذف تکی، پاککردن کلی و پیام تأیید. اصلاحات جانبی: - قانون سراسری [hidden] تا عناصر flex با صفت hidden واقعاً مخفی شوند - کارت محصول با الگوی stretched-link بازسازی شد تا دکمهی لیست مستقل باشد - پاراگرافبندی متن معرفی مدیر حفظ میشود - آیکون اختصاصی برای دستههای جدید Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
نصب از مخزن nextlevelbuilder/ui-ux-pro-max-skill (نسخهی ۲.۱۳.۰، مجوز MIT)
در مسیر .claude/skills/ — شامل هفت اسکیل:
- ui-ux-pro-max: پایگاهدادهی جستوجوپذیر با ۸۴ استایل، ۱۹۲ پالت رنگی،
۷۴ ترکیب فونت، ۹۸ راهنمای تجربهی کاربری، ۱۹۲ نوع محصول و ۲۵ نوع نمودار
در ۲۲ استک — بههمراه اسکریپت پایتونی تولید دیزاینسیستم
- design, design-system, ui-styling, brand, banner-design, slides
نصب اولیه با CLI رسمی (npx ui-ux-pro-max-cli init --ai claude) انجام شد،
سپس اسکیل اصلی با نسخهی بهروز مخزن همگام شد که ساختار بهتری دارد:
SKILL.md کوتاهتر (۱۹۶ خط بهجای ۶۸۵) بههمراه فایلهای مرجع جداگانه که
فقط هنگام نیاز خوانده میشوند.
مسیر اسکریپتها از ${CLAUDE_PLUGIN_ROOT} به مسیر نسبی پروژه تغییر کرد،
چون نصب در سطح پروژه است نه بهصورت پلاگین. اجرای اسکریپت جستوجو با
پایتون ۳ تست و تأیید شد.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
سرور 21st (نوع http روی https://21st.dev/api/mcp) به فایل .mcp.json اضافه شد. تنظیمات با دستور رسمی خودشان تولید شده است: npx @21st-dev/cli@latest init --client claude --write کلید API از متغیر محیطی API_KEY_21ST خوانده میشود، پس هیچ کلیدی داخل مخزن ذخیره نمیشود. کلید از https://21st.dev/mcp گرفته میشود. توجه: پکیج قدیمی @21st-dev/magic منسوخ شده و خودشان به @21st-dev/cli ارجاع میدهند؛ همان مسیر جدید استفاده شد. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
ممیزی با اندازهگیری واقعی در مرورگر انجام شد (کنتراست WCAG، اندازهی هدف لمسی، اندازهی فونت، طول خط، لایهبندی) نه با حدس. نتیجهی قبل و بعد: کنتراست ناکافی ۲۷ → ۰ مورد واقعی، فونت زیر ۱۲px از ۴ → ۰، هدف لمسی کوچک در موبایل ۲۳ → ۰ مورد واقعی. تصویرسازی اختصاصی هر دسته: مهمترین ضعف بصری این بود که هر ۳۶ محصول یک placeholder خاکستری یکسان داشتند. ده تصویرسازی خطی در هویت رنگی سایت ساخته شد (قوطی، نبشی، رابیتس، شاخ گوزنی، فنس، ایزوگام، ورق گالوانیزه، پیچ و یراق، فرفورژه و پیشفرض)، مجموعاً ۱۹ کیلوبایت SVG. هر محصول تصویر دستهی خودش را میگیرد تا گرید طراحیشده بهنظر برسد — و بهمحض آپلود عکس واقعی، جای خودش را میدهد. هیرو و کارت دستهبندی: - ترکیب تصویری پلکانی چهار دستهی شاخص در هیرو، جایگزین نقش محو پسزمینه - کارتهای دسته با تصویر بزرگ بهجای آیکون تکراری - دستهی شاخص اول گرید میآید تا کارت دوستونهاش حفره ایجاد نکند دسترسیپذیری (WCAG AA): - ink-faint از #86868f به #6e6e77 (۳.۶:۱ → ۵.۱:۱) - سبز واتساپ و آبی تلگرام تیرهتر شدند تا متن سفید ۴.۹:۱ و ۴.۸:۱ شود - رفع باگ واقعی: قانون .nav a رنگ متن دکمههای منوی موبایل را تیره میکرد و روی سبز ۲.۴:۱ میشد - همهی فونتهای زیر ۱۲px بزرگ شدند، هدفهای لمسی به ۴۴px رسیدند - لینک کشیدهی کارت حالا کادر واقعی دارد، نه شبهعنصر حرکت (طبق تیر Subtle دیتابیس): - مدت انیمیشن ورود از ۵۰۰ به ۳۸۰ms و جابهجایی از ۱۶ به ۱۲px - ورود پلهای کارتهای گرید با فاصلهی ۴۰ms - بازخورد فشردن روی کارتها، توکنهای یکسان زمانبندی و easing ساختار و ریتم: - ادغام دو بخش «چرا ما» و «تضمینها» که ۸۰٪ محتوایشان تکراری بود؛ صفحه ۷۰۰ پیکسل کوتاهتر و پیامها بدون تکرار شدند - یکدرمیانشدن پسزمینهی بخشها (سفید/تیره/گرم) - جعبهی سفارش در صفحهی محصول در دسکتاپ چسبان شد - توکنهای فاصله، لایهبندی z-index و ارقام جدولی تستشده روی ۳۷۵، ۳۹۰، ۷۶۸، ۱۰۲۴ و ۱۴۴۰ پیکسل: بدون خطای مرورگر و بدون اسکرول افقی در هیچ صفحهای. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
باگ: شنوندههای سطح document و window (کلیک روی «+ لیست»، کلید Escape، تغییر اندازهی صفحه) با هر بار اجرای اسکریپت دوباره وصل میشدند. در سایت چندصفحهای مشکلی نداشت چون هر صفحه یکبار لود میشود، ولی در نسخهی پیشنمایش که همهی صفحات در یک فایل جمع شدهاند، یک کلیک روی «+ لیست» دوبار اجرا میشد: اول اضافه و بلافاصله حذف، یعنی اثرش خنثی میشد. اصلاح: - تابع bindOnce برای شنوندههای سراسری؛ هر کدام فقط یکبار وصل میشوند - آن شنوندهها عناصر را هنگام اجرا پیدا میکنند، نه هنگام وصلشدن، تا بعد از رندر دوباره به عناصر قدیمیِ حذفشده اشاره نکنند - ارجاعهای DOM لیست استعلام از حالت «یکبار گرفتهشده» به «هر بار تازهگرفتهشده» تغییر کرد - انتخابگر انیمیشن ورود به reveal:not(.in) محدود شد تا بخشهایی که قبلاً ظاهر شدهاند دوباره پردازش نشوند - کد به بخشهای نامدار (initNav، initReveal، initQuotePanel، initGallery) تفکیک شد تستشده روی دسکتاپ و موبایل: افزودن و ماندگاری و پاککردن لیست استعلام، باز و بسته شدن منو با کلید Escape، رنگ متن دکمههای منو، انیمیشن ورود همهی بخشها و نبود اسکرول افقی. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
…یاتی ایرادهای گزارششده: ۱) دکمههای واتساپ/تلگرام/تماس در پیشنمایش کار نمیکردند — صفحه داخل iframe محدود اجرا میشود و بازکردن پنجره ممکن است بلاک شود. حالا اول window.open امتحان میشود و اگر نشد آدرس مقصد نمایش داده میشود تا کاربر ببیند لینک درست است. (در سایت واقعی این لینکها همیشه کار میکردند.) ۲) «رابیتس» به «رابیس» تغییر کرد — املایی که مشتریهای خودشان به کار میبرند. در توضیح دسته هر دو املا آمده تا با هر دو جستوجو پیدا شود. ۳) لینکهای «چرا فولاد ایمان؟» و «منطقهی خدمات» در پیشنمایش بیاثر بودند — مسیریاب، لنگر را از مسیر جدا نمیکرد. حالا «مسیر + لنگر» را میفهمد و بعد از رندر به همان بخش اسکرول میکند. بازرسی کامل سایت واقعی: ۵۳ صفحه، ۲۶۵ لنگر، ۳۶ عکس — همه سالم. اسکریپت بازرس (scripts/linkcheck.js) در مخزن ماند تا در آینده هم اجرا شود. اصلاح محصولات طبق اطلاعات مالک: - ورق گالوانیزه فقط ضخامت ۰.۵ میلیمتر با دو طرح «سفال» و «گالوانیزه»؛ محصول ساختگی «شیروانی سفارشی» حذف شد - مهاجرت یکبارهی داده برای دیتابیسهای موجود، با ترتیب درست اجرا (fixContent قبل از migrateCatalog، وگرنه دسته تکراری ساخته میشد) تصویرسازی اختصاصی هر محصول: ۲۶ تصویر خطی که تفاوت واقعی محصولات را نشان میدهند — سایز مقطع قوطی، تراکم شبکهی رابیس، تعداد شاخهی شاخ گوزنی، پروفیل ورق سفال در برابر گالوانیزه. فرفورژه طبق خواستهی مالک تصویر دسته میگیرد. عکس واقعی دانلود نشد چون سیاست شبکهی محیط همهی میزبانهای عکس را بلاک میکند و عکس استوکِ محصولی که در انبار نیست، مشتری را گمراه میکند. سئو و ترافیک: - آدرس تمیز دستهها: /category/<slug> با ریدایرکت ۳۰۱ از آدرس قدیمی - اسکیمای ItemList و BreadcrumbList روی صفحات دسته - اسکیمای FAQPage روی صفحهی تماس، متصل به همان متنی که نمایش داده میشود - کارت اشتراکگذاری ۱۲۰۰×۶۳۰ برای واتساپ و تلگرام - عنوان و توضیح متای اختصاصیتر برای هر دسته عملیات و مدیریت آینده: - CLAUDE.md: معماری، واقعیتهای کسبوکار، تلههایی که قبلاً باگ ساختهاند - DEPLOY.md: راهنمای فارسی خرید دامنه .ir و انتشار، قدمبهقدم - SEO.md: چه کاری انجام شده و چه کاری با مالک است - scripts/deploy-setup.sh: نصب کامل روی اوبونتو با یک دستور - scripts/backup.sh: پشتیبانگیری امن از دیتابیس WAL و عکسها - endpoint سلامت /healthz برای مانیتورینگ و عیبیابی از راه دور تستشده روی ۳۶۰ تا ۱۹۲۰ پیکسل: بدون خطای مرورگر، بدون اسکرول افقی، بدون عکس خراب. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
کاربر گزارش کرد در منوی موبایل «نظر مشتریان» و «دربارهی ما» کار نمیکنند و صفحهی اصلی خیلی طولانی شده است. هر دو یک ریشه داشتند: این آیتمها لینک لنگری به بخشهایی از صفحهی اصلی بودند (/#reviews و /#about). لینک لنگری وقتی صفحه داخل قاب پیشنمایش باز میشود کار نمیکند، چون اسکرول را صفحهی میزبان کنترل میکند و scrollIntoView داخل قاب حرکتی ایجاد نمیکند. ساختار - صفحهی مستقل /about: معرفی مدیر، تضمینها، مراحل سفارش، منطقهی خدمات - صفحهی مستقل /reviews: همهی نظرها + دادهی ساختاریافتهی Review - صفحهی اصلی کوتاه شد: ۱۴۶۸۷ → ۸۵۴۸ پیکسل روی گوشی (۱۷ → ۱۰ صفحه) - همهی لینکهای لنگری منو و فوتر به آدرس صفحهی واقعی تبدیل شدند - پارشال breadcrumb با دادهی ساختاریافته + افزودن صفحات به sitemap باگهای واقعی که پیدا و رفع شدند - product-grid.big امتیاز بالاتری از product-grid داشت و مدیاکوئری موبایل را رد میکرد؛ بخش فرفورژه روی گوشی تکستونه و سه برابر بلندتر میشد - content-visibility: auto باعث پریدن اسکرول و نامرئیماندن دو بخش انتهای صفحه میشد؛ حذف شد و دلیلش در style.css مستند شد - «رابیتس» در فوتر، توضیح سایت و دادهی ساختاریافته به «رابیس» اصلاح شد - هدفهای لمسی کوچک فوتر و breadcrumb روی گوشی به ۴۴ پیکسل رسیدند امنیت - در حالت production اگر SESSION_SECRET نباشد یا کوتاه باشد، سایت بالا نمیآید - هدر Permissions-Policy برای بستن دوربین/میکروفون/موقعیت - امتیاز ستاره فقط از نظرهای واقعی ساخته میشود؛ نظرهای «(نمونه)» شمرده نمیشوند تا aggregateRating ساختگی به گوگل فرستاده نشود سرعت و نگهداری - نسخهی فایلهای استاتیک از هش محتوایشان ساخته میشود تا بعد از هر اصلاح، مرورگر مشتری نسخهی قدیمی کششده را نشان ندهد - scripts/clickcheck.js: روی هر لینک منو/فوتر در موبایل و دسکتاپ واقعاً کلیک میکند و مقصد را میسنجد — همان نوع باگی که linkcheck نمیگرفت - audit.js دیگر عناصر پنهان را گزارش نمیکند؛ هر سه اسکریپت آدرس پایه را بهعنوان آرگومان میگیرند Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
…ی JSON-LD کاربر خواست تا جایی که ممکن است موارد تستنشده را تست کنم و مطمئن شوم سایت زیر بار همزمان قوی است. سه یافتهی واقعی: ۱) ظرفیت همزمان تست بار (بدون کتابخانهی بیرونی، چون شبکه اجازهی نصب autocannon/wrk را نمیداد) نشان داد سرور تکپردازشی حدود ۴۶۰ درخواست بر ثانیه سقف دارد — چون better-sqlite3 همزمان (synchronous) است و هر درخواست رشتهی اصلی Node را میگیرد. در ۴۰۰ اتصال همزمان، ۸۲ خطای اتصال رخ میداد. افزودن cluster.js (به تعداد هستهی پردازنده، حداکثر ۴ پردازش فرزند): توان به ~۱۵۶۰ درخواست بر ثانیه رسید و در همان ۴۰۰ اتصال همزمان صفر خطا داد. seedAll() الگوی چک-سپس-درج دارد (نه atomic)، پس پردازش اصلی قبل از fork کردن فرزندان، دیتابیس را یکبار میسازد تا مسابقهی نوشتن رخ ندهد. مقاومت در برابر کرش هم تست شد: کشتن یک پردازش وسط بار سنگین فقط ۵ درخواست از ۶۵۳۲ را ناموفق کرد؛ پردازش اصلی خودکار جایگزینش ساخت. systemd (deploy-setup.sh) و package.json (npm run start:cluster) بهروز شدند. ۲) اسکرول افقی ناخواسته روی گوشیهای ۳۲۰px (iPhone SE، Galaxy S9+) دو علت جدا: - overflow-x:hidden فقط روی body بود، نه html؛ در صفحات راستبهچپ این باعث میشد مرورگر موقعیت اولیهی اسکرول را صفر نگذارد (scrollX=-31 بهجای ۰) و کل صفحه با شکاف سفید کج بیفتد. - .brand با white-space:nowrap هرگز کوچکتر از پهنترین خطش (زیرنویس) نمیشد؛ در ۳۲۰px عرض لازم برای برند+همبرگر+دکمهی تماس از عرض هدر بیشتر بود. زیرنویس زیر ۴۸۰px پنهان میشود. تست شد: ۶ نمایهی واقعی دستگاه (iPhone SE تا iPad Mini) × ۶ صفحه، صفر ایراد. ۳) امنسازی JSON-LD شش جای مختلف با JSON.stringify() خام داخل <script type="application/ld+json"> مینوشتند؛ اگر متن کاربر (نام محصول، نظر مشتری) بهطور اتفاقی «</script>» داشته باشد، مرورگر تگ را زودهنگام میبندد. h.jsonLd() اضافه شد که «<» را escape میکند. با تزریق واقعی </script><script>alert(1)</script> در نام محصول تست شد — صفر دیالوگ alert باز شد، صفر تگ script اضافه در صفحه. سازگاری مرورگر (بررسی ایستا، چون شبکه اجازهی نصب Firefox/WebKit واقعی را نمیداد): پیشوند -webkit-backdrop-filter برای سافاری، پشتیبان vh قبل از dvh، main.js از قبل ES5 خالص بود (بدون optional chaining/arrow function). Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
بررسی کامل سایت طبق چکلیست کاربر. اکثر موارد از قبل ساخته شده بودند (دکمهی واتساپ شناور، صفحهی ۴۰۴ اختصاصی، فشردهسازی و lazy loading عکسها، alt متن، امنیت پایهی پنل) — اینها فقط تأیید شدند، نه بازسازی. موارد واقعاً جدید یا اصلاحشده: نقشه (خواستهی ۷) از این به بعد صفحهی «دربارهی ما» و «تماس با ما» بدون هیچ تنظیمی از پنل، نقشهی جستوجوشدهی آدرس فروشگاه را نشان میدهند (defaultMapEmbed در src/config/site.js). عمداً از مختصات تقریبی مرکز شهر برای پین استفاده نشد چون میتوانست مشتری را بهجای اشتباه بفرستد؛ بهجایش از آدرس متنی کامل بهعنوان جستوجو استفاده شده. مالک هر وقت خواست از پنل میتواند کد iframe دقیقتری جایگزین کند. گالری بزرگنمایی فرفورژه (خواستهی ۸) دکمهی بزرگنمایی روی کارتهای صفحهی /forge اضافه شد؛ چون تصویرسازیها SVG (وکتور) هستند، در نمای بزرگ هم بدون افت کیفیت دیده میشوند. یک لایتباکس سبک بدون کتابخانه (main.js) با بستن از طریق Escape/کلیک پسزمینه/دکمهی بستن. سئو (خواستهی ۳) عنوان صفحهی اصلی، توضیح متا و زیرنویس هیرو با عبارتهای دقیقی که خواسته شده بود («آهن فروشی گرگان»، «فروش قوطی و پروفیل»، «فرفورژه گرگان») بهروز شد. تغییر زیرنویس هیرو با migration محافظتشده اعمال شد (فقط اگر مدیر آن را از پنل شخصیسازی نکرده باشد). دو باگ امنیتی واقعی که در تست پیدا شد ۱) ورود به پنل مدیریت کاملاً از کار افتاده بود. کوکی نشست با secure:isProd ثابت تنظیم شده بود. با تست واقعی (نه فقط خواندن کد) مشخص شد: در حالت production روی HTTP ساده (قبل از تنظیم HTTPS واقعی، دقیقاً وضعیتی که موقع تست اولیهی سرور یا اگر certbot هنوز موفق نشده پیش میآید)، مرورگر کوکی امن را نگه نمیدارد و هر ورود با خطای مبهم «فرم منقضی شده» شکست میخورد. رفع شد با secure:'auto' که بر اساس trust proxy درست تصمیم میگیرد — هم پشت نگینکس با HTTPS واقعی امن میماند، هم قبل از آمادهشدن HTTPS کار میکند. ۲) محدودیت تلاش ورود عملاً ۴ برابر ضعیفتر از چیزی بود که فکر میشد. express-rate-limit با فروشگاه پیشفرضش هر پردازش cluster.js را جدا میشمارد. با تست دقیق (اتصال جداگانه برای هر درخواست، دقیقاً رفتار یک مهاجم واقعی) اندازهگیری شد: بهجای مسدودشدن در تلاش دهم، تا تلاش ۴۱ ام مسدود نمیشد. جایگزین شد با loginLimiter در src/middleware/auth.js که از جدول login_attempts در همان دیتابیس مشترک استفاده میکند — سراسری، صرفنظر از تعداد پردازش. با تست تکراری تأیید شد که حالا دقیقاً در تلاش یازدهم مسدود میشود. هر دو باگ فقط با تست واقعی مرورگر (نه خواندن کد) پیدا شدند؛ کد بهتنهایی درست بهنظر میرسید. تستهای تکمیلی که انجام و تأیید شد - تزریق </script><script>alert(1)</script> در نام محصول: صفر alert باز شد - جریان کامل لیست استعلام: افزودن → toast تأیید → پنل → لینک واتساپ با متن کامل و شمارهگذاریشده → حذف آیتم → ماندگاری در localStorage بعد از رفرش - پنل مدیریت (ورود، تغییر رمز اجباری، فرم محصول، تنظیمات) روی ۶ نمایهی واقعی گوشی/تبلت — صفر سرریز افقی، صفر خطای جاوااسکریپت - ۵۵ صفحه linkcheck، ۱۳۲ کلیک clickcheck — همه سالم Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
عکس محصولات ۱۰ عکسی که فرستاده شد پردازش و به سایت اضافه شد: شاخ گوزنی، رابیس، سیم رابیس، پیچ سرمته، پشم شیشه، ورق گالوانیزه، ورق طرح سفال، ورق نما، قوطی و ایزوگام. هر عکس در چند عرض به WebP تبدیل شده تا مرورگر روی گوشی نسخهی کوچک و روی دسکتاپ نسخهی بزرگ را بردارد (مجموع ۵۰۰ کیلوبایت).⚠️ عکسها عمداً در public/img/photos/ ذخیره شدند نه public/uploads/. پوشهی uploads در .gitignore است، پس اگر عکس پایهی سایت آنجا میرفت با دیپلوی همراه نمیشد و سایت روی سرور بیعکس بالا میآمد. عکس آپلودی مدیر از پنل، همچنان بر اینها اولویت دارد. باگ واقعی که سر راه پیدا شد: imageSrcset عرضِ هدف را اعلام میکرد نه عرض واقعی فایل. چون تصاویر با withoutEnlargement ساخته میشوند، عکسی که اصلش ۵۰۰ پیکسل است در هر سه نسخه ۵۰۰ میماند؛ اعلام «۱۶۰۰w» باعث میشد مرورگر همان فایل کوچک را برای جای بزرگ بردارد و عکس تار دیده شود. حالا عرض واقعی اعلام میشود (image_width به پرسوجو اضافه شد) و نسخههای همعرض تکراری هم حذف میشوند. دستهی قوطی و منوی سایز قوطی به ۱۶ سایز رایج گسترش یافت (۹ مربع، ۷ مستطیل) با زیردستهی مربع/مستطیل. هر سایز یک صفحهی مستقل دارد تا جستوجوی «قوطی ۴۰×۴۰ گرگان» مستقیم به همان صفحه برسد. یک منوی بازشوی «انتخاب سایز» در صفحهی محصولات اضافه شد که با <details>/<summary> ساخته شده — بدون جاوااسکریپت، سازگار با CSP سختگیر سایت و درست با کیبورد و صفحهخوان. ضخامت عمداً در نام سایز نیامده: هر سایز در چند ضخامت فروخته میشود و موجودی روزانه عوض میشود؛ نوشتن یک عدد ثابت یعنی دادن اطلاعات نادرست. دو محصول جدید که عکسشان بود ولی در کاتالوگ نبودند: سیم رابیس و ورق نما. مرتبسازی صفحهی محصولات چیپهای دسته، زیردسته، منوی سایز و نوار جستوجو داخل یک پنل جمع شدند تا صفحه بهجای چند ردیف شناور، یک ناحیهی انتخاب و یک ناحیهی نتیجه داشته باشد. در صفحهی محصول، عکس واقعی بهصورت کامل (contain) نمایش داده میشود نه بریده — نسبت ابعاد عکسها یکسان نیست و برش، بخشی از خود کالا را از کادر بیرون میگذاشت (ورق شیروانی عکس ۲.۴:۱ دارد). پنل مدیریت اسکریپت set-admin برای تعیین نام کاربری و رمز اضافه شد (هش bcrypt هزینهی ۱۲، حذف کاربر پیشفرض تا راه ورود اضافه نماند). رمز عمداً در هیچ فایلی از مخزن نوشته نشده — این مخزن روی گیتهاب است و هر چیزی که commit شود برای همیشه در تاریخچه میماند. رمز موقع نصب یا با همین دستور داده میشود. بهروزرسانی خودکار بعد از دیپلوی scripts/auto-update.sh + تایمر systemd: سرور هر ۳ دقیقه گیتهاب را نگاه میکند، اگر نسخهی جدیدی بود آن را میگیرد، نصب میکند، ریاستارت میدهد و سلامت سایت را میسنجد؛ اگر بالا نیامد **خودکار به نسخهی قبلی برمیگردد**. مسیر بازگشت واقعاً تست شد: با یک کامیت سالم و پورت سلامت اشتباه، اسکریپت نسخهی جدید را اعمال کرد، شکست را تشخیص داد و دقیقاً به کامیت قبلی برگشت. تستها ۷۳ صفحه linkcheck، ۱۳۲ کلیک clickcheck، ۶ نمایهی دستگاه × ۹ صفحه بدون سرریز افقی و بدون خطای جاوااسکریپت، و تست کامل ورود/خروج پنل با اطلاعات درست و نادرست. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
…یمیرد علت خطا لیارا روی سرویسهای Node خودش NODE_ENV=production را تنظیم میکند. در کد، یک محافظ امنیتی بود که در حالت production اگر SESSION_SECRET تعریف نشده بود با process.exit(1) برنامه را میبست. نتیجه: کانتینر در کمتر از یک ثانیه میمرد و چون فرصت نمیکرد چیزی لاگ کند، کاربر فقط «Failure بدون هیچ لاگی» میدید. با شبیهسازی دقیق همان محیط بازتولید شد: env -u SESSION_SECRET NODE_ENV=production node server.js → exit 1 مرحلهی build مشکلی نداشت؛ npm ci با همین package-lock در ۵ ثانیه و بدون خطا کامل شد (better-sqlite3 و sharp هر دو نسخهی ازپیشساخته دارند). رفع src/config/session-secret.js اضافه شد با این ترتیب اولویت: ۱) متغیر محیطی SESSION_SECRET ۲) کلید تصادفی ۳۲ بایتی که در DATA_DIR ذخیره میشود (mode 0600) ۳) کلید فقط-در-حافظه اگر دیسک قابل نوشتن نبود هیچکدام از این مسیرها برنامه را نمیکشد و امنیت هم حفظ میشود؛ فقط اگر پوشهی داده پایدار نباشد، با هر دیپلوی باید دوباره وارد پنل شد. سایت هنگام راهاندازی همین را در لاگ توضیح میدهد. باگ مسابقهای که سر راه پیدا شد در حالت چندپردازشی، هر worker خودش کلید را حل میکرد. چهار پردازش تقریباً همزمان میدیدند فایل کلید نیست، هر کدام کلید متفاوتی میساختند و روی هم مینوشتند — یعنی کوکی امضاشده توسط یک پردازش، در پردازش بعدی نامعتبر شمرده میشد و مدیر بهصورت متناوب از پنل بیرون میافتاد. حالا کلید در پردازش اصلی و قبل از fork تعیین و از راه متغیر محیطی به فرزندان داده میشود. با ۲۰ درخواست پشتسرهم تأیید شد که نشست پایدار میماند. پایداری اطلاعات روی سرویس ابری UPLOAD_DIR هم مثل DATA_DIR از متغیر محیطی قابل تنظیم شد. بدون این دو، فایلسیستم لیارا با هر دیپلوی پاک میشود و همهی محصولات، عکسهای آپلودی و رمز پنل از بین میرود. با دیسک جداگانه تست شد و سالم بود. liara.json با پلتفرم node، پورت ۳۰۰۰ و Node نسخهی ۲۰ اضافه شد (نسخهای که هر دو کتابخانهی نیازمند کامپایل برایش باینری آماده دارند). LIARA.md: راهنمای قدمبهقدم شامل انتخاب شاخهی درست، ساخت دیسک، متغیرهای محیطی و تنظیم رمز پنل. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
…OAD_DIR unwritable Reproduces the Liara ENOENT mkdir '/app/data' crash locally (blocking the path with a file instead of chmod, since chmod is bypassed when running as root) and confirms the new try/catch paths trigger correctly: DATA_DIR failure now exits with an actionable Persian message pointing to LIARA.md instead of a bare Node stack trace, and UPLOAD_DIR failure logs a warning but lets the rest of the site start normally.
…nings The Liara deploy died with ENOENT mkdir '/app/data' because the app root is read-only there and no disk was attached yet. Failing hard meant nothing was visible at all — not the storefront, not the admin panel. DATA_DIR and UPLOAD_DIR now fall back to os.tmpdir() when the preferred path cannot be created or written, so the site boots and works. Because that state silently loses data on every redeploy, it is surfaced in three places: the startup log, /healthz (new "storage" block), and a red banner across every admin page. Only a genuinely unwritable tmpdir is still fatal, since SQLite cannot run without a writable file. Verified: unwritable dirs -> site HTTP 200 with banner and healthz persistent:false; writable dirs -> no warning anywhere; linkcheck clean across 73 pages.
Creating a disk in the Liara panel is not enough on its own — the mount path has to be declared in liara.json, otherwise the service starts without the disk and silently falls back to temporary storage. Note the ordering constraint, documented in LIARA.md: the disks must exist in the panel before the next deploy, or Liara fails with "disk not found".
…path Deploy v5 succeeded with no error yet the disks were never mounted: the app fell back to temporary storage with ENOENT mkdir '/var/lib/data'. Liara resolves mountTo relative to the app directory (/app), so an absolute /var/lib path is silently not mounted. Mount to "db" and "user-uploads" instead, which land at /app/db and /app/user-uploads. Neither path exists in the repo, so no application file is shadowed by the mount. DATA_DIR/UPLOAD_DIR in LIARA.md updated to match.
هدرهای Strict-Transport-Security و upgrade-insecure-requests روی هر درخواستی — حتی درخواست سادهی http — فرستاده میشدند. روی دامنهای که هنوز گواهی SSL نگرفته، نتیجه این است: - upgrade-insecure-requests: خودِ HTML میآید ولی CSS و JS و همهی عکسها به https ارتقا پیدا میکنند و شکست میخورند. کاربر یک صفحهی بیقالب و بهظاهر خراب میبیند. - HSTS با max-age یکساله: مرورگر را برای یک سال روی آن دامنه به https قفل میکند. حتی بعد از صدور گواهی هم مرورگرِ همان کاربر خطا نشان میدهد و پاککردنش فقط دستی از تنظیمات مرورگر ممکن است. حالا هر دو در helmet خاموشاند و در یک میانافزار جدا فقط وقتی ست میشوند که req.secure درست باشد — یعنی اتصال واقعاً HTTPS بوده. با trust proxy که از قبل تنظیم است، هدر X-Forwarded-Proto لیارا درست خوانده میشود. نتیجه: دامنهی تازه قبل از صدور گواهی هم کامل بالا میآید، و لحظهای که گواهی صادر شد بدون هیچ تغییری در کد، محافظت کامل برمیگردد. از همان خانوادهی باگ شمارهی ۱۵ است (کوکی امن قبل از آمادهشدن HTTPS)؛ بهعنوان نکتهی ۳۴ در CLAUDE.md ثبت شد. تست: در حالت production، درخواست http هیچکدام از دو هدر را نمیگیرد و صفحه با CSS کامل میآید؛ با X-Forwarded-Proto: https هر دو هدر برمیگردند و بقیهی CSP دستنخورده میماند. linkcheck، clickcheck (۲۷۶ کلیک) و بررسی سرریز افقی هم اجرا شدند. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
هنگام عیبیابی بالا نیامدن fooladiman.ir معلوم شد گواهی SSL برای www.fooladiman.ir صادر شده است. این یعنی سایت احتمالاً روی www سرو میشود، در حالی که SITE_URL روی دامنهی بدون www است. این حالت هیچ خطایی تولید نمیکند و سایت کاملاً سالم به نظر میرسد، ولی هر canonical و هر آدرس نقشهی سایت به آدرسی اشاره میکند که بلافاصله ۳۰۱ میخورد؛ سهم خزش گوگل هدر میرود و اعتبار بین دو آدرس تقسیم میشود. دقیقاً همان دسته خطایی است که ماهها دیده نمیشود. حالا server.js دامنهی واقعی هر درخواست را با SITE_URL میسنجد و اگر نخواند، یکبار در لاگ هشدار میدهد و کلید canonicalHost را در /healthz اعلام میکند — همان الگویی که برای هشدار دیسک استفاده شده است. ضمناً ثابت شد خودِ برنامه در حلقهی ریدایرکت نقشی ندارد: با هدر Host هر دو دامنه و روی هر دو پروتکل، مسیر / همیشه ۲۰۰ برمیگرداند. علت حلقه در تنظیمات «ریدایرکت دامنه»ی پنل لیاراست. هر دو نکته در CLAUDE.md ثبت شد. تست: هشدار با Host ناهماهنگ فعال و با Host درست خاموش میماند؛ linkcheck و clickcheck (۲۷۶ کلیک) بدون ایراد. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
ممیزی نشان داد بزرگترین ضعف سئوی باقیمانده، کممحتوا بودن صفحههای دسته است: بین ۷ تا ۲۵ کلمه متن یکتا داشتند. دقیقاً همین صفحهها باید برای عبارتهای پولساز بالا بیایند («رابیتس گرگان»، «قوطی علیآباد کتول») و گوگل صفحهی کممحتوا را — هرچقدر هم فنی بیعیب باشد — بالا نمیآورد. ۱) راهنمای خرید هر دسته (src/content/category-guides.js) برای هر ۱۰ دسته، ۱۵۰ تا ۲۹۰ کلمه راهنمای واقعی خرید نوشته شد؛ مجموعاً حدود ۲۲۰۰ کلمه. محتوا همان چیزی است که پشت پیشخوان به مشتری گفته میشود: فرق ستون ۹ و ۱۳ رابیتس، انتخاب سایز قوطی برای هر کار، فرق عایق رطوبتی و حرارتی، الکترود ۳ یا ۴، و نکتههای اقلیم مرطوب گلستان. نتیجه: متن یکتای صفحهی دسته از ~۲۰ کلمه به ۴۶۰ تا ۷۶۰ کلمه رسید. سه تصمیم عمدی: - راهنما زیر گرید محصول میآید، نه بالای آن. مشتری آمده محصول ببیند. - فقط روی صفحهی اول و بدون فیلتر نشان داده میشود؛ تکرارش روی صفحهی ۲ و ۳ محتوای تکراری میساخت که برعکس هدف است. با دستهی ۴۹۴ محصولی فرفورژه تست شد. - با article-blocks.ejs رندر میشود تا تایپوگرافی یکدست بماند و CSS جدیدی لازم نشود. ۲) H1 با نام شهر عنوان صفحه از قبل شهر را داشت ولی H1 نه. حالا «رابیتس در گرگان و علیآباد کتول» است و صفحههای بعدی شمارهی صفحه میگیرند تا چند صفحه با تیتر یکسان نمانند. ۳) لینک داخلی دوطرفه مسیر مقاله → محصول از قبل بود ولی برعکسش نه. حالا هر صفحهی دسته به مقالههای مرتبط خودش لینک میدهد. همهی ۱۱ لینک مقاله→دسته هم بررسی و سالم تأیید شدند (بعد از تغییر املای رابیتس، ریسک شکستنشان بود). تست: linkcheck (۶۲۶ صفحه)، clickcheck (۲۷۶ کلیک)، audit بدون هشدار جدید، سرریز افقی در ۳۲۰ تا ۱۴۴۰، و سرعت بدون افت (LCP بین ۷۶ تا ۱۵۲ میلیثانیه). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
مالک پرسید آیا میشود از طریق ایمیل او مشتری پیدا کرد. جوابش نه بود — ارسال ایمیل تبلیغاتی به کسانی که نخواستهاند اسپم است و نتیجهاش مسدود شدن حساب و بدنام شدن دامنه است، یعنی دقیقاً برعکس هدف. بهجایش دو ابزار ساخته شد که بازاریابی خودِ مالک را بیزحمت میکند. ۱) scripts/gen-poster.js یک تابلوی چاپی A5 با کد QR میسازد که مشتری داخل مغازه با دوربین گوشی باز میکند. برای فرفورژه ارزش خاصی دارد: بهجای ورقزدن کاتالوگ کاغذی، مشتری ۴۹۴ مدل را روی گوشی خودش میبیند و کد را همانجا میفرستد. - کد QR کاملاً آفلاین ساخته میشود (qrcode، فقط devDependency) - سطح تصحیح خطای H انتخاب شده تا اگر گوشهی برچسب خط بخورد یا کثیف شود، باز هم خوانده شود - خروجی HTML است نه عکس، تا در هر اندازهای تیز چاپ شود - آدرس را از src/config/site.js میگیرد، پس با تغییر دامنه خودش بهروز میشود راستیآزمایی: کد تولیدشده با sharp رستر و با jsQR رمزگشایی شد و دقیقاً https://fooladiman.ir را برگرداند — یعنی فقط ساخته نشده، واقعاً خوانده میشود. ۲) docs/معرفی-سایت.md متنهای آمادهی کپی برای واتساپ (مشتری قدیمی و پاسخ کد فرفورژه)، اینستاگرام و تلگرام، آگهی دیوار، توضیح کسبوکار برای نشان و بلد، درخواست نظر از مشتری، و فهرست جاهایی که آدرس سایت باید نوشته شود. بهعلاوهی یک بخش صریح دربارهی کارهایی که نباید انجام شوند (پیام انبوه، خرید بکلینک، نظر جعلی) و دلیل هرکدام. خروجی پوستر در .gitignore است چون با یک دستور بازتولید میشود. تست: linkcheck و clickcheck (۲۷۶ کلیک) بدون ایراد، npm audit صفر آسیبپذیری، و همهی صفحههای اصلی ۲۰۰. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
مالک گزارش داد لینک تابلو خطا میدهد. علت، تصمیم اشتباه خودم بود: خروجی تابلو را در .gitignore گذاشته بودم، پس هرگز روی سرور نمیرفت و هر آدرسی از آن ۴۰۴ میداد. ضمناً تولیدش به اجرای دستی `node scripts/gen-poster.js` نیاز داشت — کاری که صاحب یک آهنفروشی نباید برای چاپ یک تابلو انجام بدهد. حالا یک صفحهی واقعی در پنل است: /admin/poster - از داشبورد یک دکمه دارد («تابلوی مغازه») - کد QR سمت سرور و آفلاین ساخته میشود و آدرسش از site.url میآید، پس با تغییر دامنه خودش درست میماند - دکمهی چاپ با nonce کار میکند (CSP سایت onclick را اجرا نمیکند) - نوار راهنما با @media print از کاغذ حذف میشود - پشت ورود است؛ ابزار کار مالک است نه صفحهی عمومی اسکریپت جداگانه حذف شد تا یک پیادهسازی بیشتر نماند، و qrcode از devDependencies به dependencies منتقل شد چون حالا در زمان اجرا لازم است (۲۶۰ کیلوبایت، بدون کامپایل بومی). راستیآزمایی: خروجی واقعی سرور با sharp رستر و با jsQR رمزگشایی شد و https://fooladiman.ir را برگرداند — یعنی کد واقعاً خوانده میشود، نه فقط تولید شده. مسیر بدون ورود ۳۰۲ میدهد و صفحه صفر خطای CSP دارد. تست: linkcheck، clickcheck (۲۷۶ کلیک)، npm audit صفر آسیبپذیری. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
مالک خواست از طریق جیمیل برای کسبوکارهای گلستان ایمیل تبلیغاتی فرستاده شود. این کار انجام نشد: ایمیل ناخواسته به گیرندههایی که رضایتی ندادهاند اسپم است و نتیجهاش مسدودشدن حساب و بدنامشدن دامنهی سایت است — یعنی ضرر مستقیم به همان کسبوکاری که قرار بود کمک شود. بهجایش همان هدف با کانال درست: docs/جذب-مشتری-عمده.md هدفگیری مشتری تکرارشونده (جوشکار، پیمانکار، مجری سقف شیروانی، گلخانهدار) با کانالهایی که این صنف واقعاً استفاده میکند: مراجعهی حضوری به کارگاه، تماس تلفنی و واتساپ — نه ایمیل. شامل متن آمادهی تماس تلفنی (هدفش گرفتن اجازه برای فرستادن کاتالوگ است، نه فروش در تماس اول)، پیام واتساپ بلافاصله بعد از تماس، متن مراجعهی حضوری، جدول پیگیری، و سه مزیت واقعی که در هر تماس باید گفته شود: تولید مستقیم ورق گالوانیزه، ۶۰۰ مدل فرفورژه با کد روی گوشی، و اعلام قیمت روز. هدفگذاری عمداً محافظهکارانه است (۲ تا ۴ مشتری ثابت در ماه اول) چون عدد غیرواقعی باعث میشود مالک بعد از دو هفته کار را رها کند. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
صفحهی آمار از قبل بود ولی فقط بازدید را میشمرد. برای این کسبوکار «بازدید» عدد اصلی نیست — مالک باید بداند از هر صد بازدیدکننده چند نفر واقعاً سراغش آمدند. سه چیز اضافه شد: ۱) تماسها (مهمترین) کلیک روی دکمههای واتساپ، تلگرام، تلفن و «ارسال لیست استعلام» شمرده میشود، با تفکیک کانال و نرخ تبدیل بازدیدکننده به تماس. - لینکها data-track دارند و main.js با navigator.sendBeacon به /e میفرستد. preventDefault صدا زده نمیشود و beacon غیرهمزمان است، پس رفتن کاربر به واتساپ ذرهای عقب نمیافتد؛ مرورگر بدون sendBeacon هم لینک عادیاش را میرود. - نوع رویداد از فهرست بستهی EVENT_KINDS میآید و رباتها شمرده نمیشوند، پس کسی نمیتواند جدول را با دادهی ساختگی پر کند. - بدنه حداکثر ۲۰۰ بایت خوانده میشود و پاسخ ۲۰۴ بدون بدنه است. ۲) جستوجوهای داخل سایت عبارتهایی که مردم جستوجو کردهاند، همراه تعداد نتیجه. ترتیب جدول عمداً اول بر اساس «بینتیجه بودن» است نه تعداد: جستوجوی بینتیجه یعنی مشتری چیزی خواسته که در سایت نبوده — سفارش ازدسترفته. ۳) توزیع ساعتی چه ساعتی سراغ سایت میآیند (به وقت تهران)، تا مالک بداند کِی باید کنار گوشی باشد. همهی دادهها مثل قبل فقط جمعشده ذخیره میشوند — بدون آیپی و بدون شناسهی پایدار. راستیآزمایی: با مرورگر واقعی روی دکمه کلیک شد و beacon های whatsapp و phone در سرور ثبت شدند، با صفر خطای CSP. نوع نامعتبر و درخواست ربات هم رد شدند. تست: linkcheck، clickcheck (۲۷۶ کلیک)، سرریز افقی ۳۲۰ تا ۱۴۴۰، و سرعت بدون افت (LCP بین ۸۴ تا ۱۶۰ میلیثانیه؛ main.js از ۱۴.۸ به ۱۶.۴ کیلوبایت رسید). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
دیوار پرترافیکترین کانالی است که برای کسبوکار محلی در ایران در دسترس است، ولی قبلاً فقط یک نمونه آگهی نوشته شده بود. حالا برای هر پنج دستهی پرفروش — ورق گالوانیزه، فرفورژه، قوطی و پروفیل، رابیتس، فنس و توری — عنوان و متن کامل و آمادهی انتشار هست. هر آگهی جداگانه است، نه یکی برای همه: هر کدام برای عبارت جستوجوی خودش دیده میشود. متن هر آگهی به صفحهی مرتبط سایت لینک میدهد (گالری فرفورژه، محاسبهگر وزن) تا بازدیدکنندهی دیوار به سایت هم بیاید. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
…ی دیوار ۱) ریشهی مشکل: بازدید خودِ مالک شمرده میشد مالک روزی دهها بار سایت خودش را باز میکند تا محصول و عکس را چک کند. همهی اینها در آمار میآمد و عدد «بازدیدکننده» را دروغ میکرد — دقیقاً همان چیزی که آمار قرار بود جلویش را بگیرد. حالا هنگام ورود موفق به پنل، کوکی fi_notrack روی مرورگر مالک گذاشته میشود و میانافزار آمار درخواستهای دارای آن را رد میکند. کوکی هیچ دادهی شخصی ندارد، فقط یک پرچم است، و httpOnly نیست تا خود مالک بتواند در صورت لزوم پاکش کند. ۲) دکمهی «پاککردن آمار و شروع از صفر» در صفحهی آمار. چون برگشتناپذیر است، فقط با POST و توکن CSRF کار میکند و data-confirm دارد — نه یک لینک ساده که با کلیک اشتباهی همهچیز را پاک کند. بافر حافظه هم خالی میشود، وگرنه چند ثانیه بعد بازدیدهای ثبتنشده دوباره روی جدول خالی مینشستند و کاربر فکر میکرد پاک نشده. راستیآزمایی با مرورگر واقعی: کوکی بعد از ورود گذاشته شد، دو بازدید مالک از صفحههای عمومی شمرده نشد، و بعد از ریست هر سه شمارنده صفر شد. ۳) پنج آگهی دیوار، بازنویسیشده نسخهی قبلی فهرست محصول بود. این نسخه با قلاب شروع میشود و روی مشکل مشتری دست میگذارد: ورق بدون واسطه، کاتالوگ ۶۰۰ مدلی روی گوشی بهجای کاغذ، غافلگیرنشدن سر وزن بار، لکهی زنگ سیم معمولی زیر گچ، و زنگزدن فنس ساده در رطوبت شمال. سه قاعدهی انتشار هم اضافه شد (آگهی جدا برای هر دسته، عکس واقعی، تمدید دورهای). محدودیتهای واقعی کسبوکار در متنها رعایت شده: فرفورژه تولید و نصب نمیشود، ورق فقط ضخامت ۰.۵ و دو طرح، و هیچ قیمتی نوشته نشده. تست: linkcheck، clickcheck (۲۷۶ کلیک)، npm audit صفر آسیبپذیری. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
مالک پرسید آیا آمار سایت واقعی است. بررسی نشان داد داده کاملاً واقعی است (هیچ دادهی نمونهای در جدولهای stats_* نوشته نمیشود و تنها نویسندهشان services/stats.js است)، ولی یک باگ ساختاری پیدا شد که عدد «بازدیدکننده» را روی سرور واقعی باد میکرد. نمکِ هش بازدیدکننده با crypto.randomBytes در لحظهی بارگذاری ماژول ساخته میشد — یعنی در هر پردازش یک مقدار متفاوت. سرور واقعی با cluster.js تا چهار پردازش اجرا میشود، پس هش یک بازدیدکننده در هر پردازش فرق میکرد و یک نفر تا چهار بار بهعنوان «یکتا» شمرده میشد. دقیقاً از همان خانوادهی باگی است که قبلاً برای کلید نشست حل شده بود (هر پردازش کلید خودش را میساخت و مدیر از پنل بیرون میافتاد). راهحل هم همان است: cluster.js نمک را قبل از fork میسازد و در STATS_SALT میگذارد تا همهی فرزندان یکی داشته باشند. نمک عمداً در دیتابیس ذخیره نمیشود و فقط در حافظه میماند، پس با در دست داشتن فایل دیتابیس هم نمیشود هش را به آیپی برگرداند. هزینهاش این است که با ریاستارت عوض میشود و بازدیدکنندههای همان روز یکبار دوباره شمرده میشوند — در برابر خطای چهاربرابری، ناچیز. راستیآزمایی: با چهار پردازش واقعی، یک بازدیدکننده بیست درخواست فرستاد و دقیقاً یک بازدیدکنندهی یکتا ثبت شد. تست: linkcheck، clickcheck (۲۷۶ کلیک). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
مالک پرسید بعد از تمامشدن اشتراک چطور سایت را مدیریت کند. بخشی به README اضافه شد که مرز را روشن میکند: همهی کارهای روزمره (محصول، عکس، موجودی، متن، نظر، آمار، تابلو) در پنل مدیریت انجام میشود و به برنامهنویس نیازی ندارد؛ فقط تغییر کد نیاز دارد. بهعلاوه: روش تغییر کد از خود گیتهاب بدون هیچ ابزاری، ترتیب فایلهایی که باید به برنامهنویس بعدی داد، و چهار دستور تستی که قبل از هر تحویل باید سبز باشند. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
مالک درخواست ربات افزایش بازدید داد. انجام نشد: ترافیک ساختگی همان مشکلی را برمیگرداند که دو پیام قبل خواسته بود حل شود (آمار غیرواقعی)، فیلتر ربات خودمان کنارش میگذارد، و گوگل هم تشخیصش میدهد. بهجایش چیزی ساخته شد که ترافیک واقعی و مرکب میآورد: دکمهی اشتراکگذاری در صفحهی محصول. مشتریای که داخل مغازه مدل فرفورژه را روی گوشی میبیند، با یک کلیک لینکش را برای جوشکار یا همسرش میفرستد؛ هر بازدیدکننده تبدیل میشود به یک کانال توزیع. - روی گوشی از navigator.share استفاده میکند: برگهی اشتراکگذاری خود سیستم با واتساپ و تلگرام و پیامک. بدون اسکریپت بیرونی و بدون هیچ سوراخی در CSP. - روی دسکتاپ به کپی لینک در حافظه برمیگردد، با پیام تأیید. - دکمه با hidden شروع میشود و فقط وقتی نمایش داده میشود که مرورگر واقعاً یکی از این دو را داشته باشد؛ دکمهای که کلیک شود و کاری نکند از نبودنش بدتر است. - اشتراکگذاری بهعنوان رویداد share در آمار شمرده میشود، پس مالک میبیند این کانال چقدر کار میکند. راستیآزمایی با مرورگر واقعی، هر دو مسیر: روی موبایل آدرس canonical و نام محصول درست به لایهی اشتراکگذاری رفت؛ روی دسکتاپ لینک در حافظه کپی شد. در هر دو حالت beacon آمار ثبت شد و صفر خطای CSP بود. تست: linkcheck، clickcheck (۲۷۶ کلیک)، audit بدون هشدار جدید، سرریز افقی ۳۲۰ تا ۱۴۴۰. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
مالک پرسید آیا سایت سالها بدون باگ میماند. ممیزی کامل انجام شد و چهار ریسک واقعی — که همهشان بیصدا بودند و در تستهای معمول دیده نمیشدند — پیدا و رفع شد. ۱) Node ۲۰ به پایان پشتیبانی رسیده بود liara.json نسخه را روی ۲۰ پین کرده بود. Node ۲۰ در فروردین ۱۴۰۵ EOL شد، یعنی از آن به بعد هیچ وصلهی امنیتی نمیگیرد. به ۲۲ منتقل شد (engines هم به >=20 بهروز شد). این خطرناکترین مورد بود چون هیچ خطایی تولید نمیکند و فقط به مرور آسیبپذیرتر میشود. ۲) هیچ محافظی برای خطای مدیریتنشده نبود یک Promise بدون catch در تایمر آمار یا پاکسازی نشست میتوانست پردازش را بیصدا بکشد. حالا هر دو رویداد لاگ میشوند و پردازش با کد ۱ بسته میشود تا cluster جایگزینش کند. با تزریق یک Promise ردشده تست شد: پیام در لاگ آمد و کد خروج ۱ بود. ۳) جدول login_attempts هرگز پاک نمیشد هر آیپی که فرم ورود را زده بود یک سطر دائمی میساخت — با رباتهایی که شبانهروز اسکن میکنند، در چند سال دهها هزار سطر بیمصرف. حالا ساعتی یکبار سطرهای خارج از پنجرهی زمانی پاک میشوند. آمار و نشستها از قبل پاکسازی داشتند. ۴) multer روی نسخهی منسوخ ۱.x بود خط نگهداریشده ۲.x است. به ۲.۲.۰ ارتقا یافت و آپلود عکس با مرورگر واقعی تست شد: فایل ذخیره شد و رکورد عکس در دیتابیس ثبت شد. تست: سینتکس همهی فایلها، linkcheck، clickcheck (۲۷۶ کلیک)، audit بدون هشدار جدید، سرریز افقی ۳۲۰ تا ۱۴۴۰، اجرای واقعی چهارپردازشی با صفر خطا، و npm audit صفر آسیبپذیری. سه نکتهی جدید (۳۹ تا ۴۱) در CLAUDE.md ثبت شد، از جمله یادآوری دوساله برای بررسی نسخهی Node. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
بررسی ایندکسشدن صفحهها هیچ مانع فنیای پیدا نکرد (۵۹۲ آدرس نقشهی سایت همه ۲۰۰، بدون noindex، با canonical درست، بدون عنوان تکراری، و بدون هدر X-Robots-Tag)، ولی پیمایش لینکهای داخلی دو مشکل واقعی نشان داد که هر دو دسترسی گوگل به صفحههای محصول را کند میکردند: ۱) «محصولات مشابه» همیشه چهار محصول اول دسته بود، نه همسایههای محصول. یعنی ۴۹۰ صفحهی فرفورژه همگی به یک مشت آدرس ثابت لینک میدادند و ۴۸۶ محصول دیگر هیچ لینک داخلی نمیگرفتند. اندازهگیری: ۶۰ صفحه فقط ۴۰ مقصد متمایز میساخت و پرتکرارها ۸ بار تکرار میشدند. حالا همسایهها با فاصلهی حلقهای انتخاب میشوند تا دسته یک زنجیرهی بسته شود و هیچ محصولی بیلینک نماند → ۷۲ مقصد، حداکثر ۴ بار. ۲) ۲۸۵ صفحه ۴ تا ۵ کلیک از خانه فاصله داشتند، چون تنها راه رسیدنشان زنجیرهی صفحهبندی بود. صفحهی «فهرست کامل کالاها» اضافه شد که به همهی محصولات لینک میدهد؛ برای مشتری هم مفید است چون کد فرفورژه را میشود با Ctrl+F پیدا کرد. نتیجهی اندازهگیریشده: همهی ۵۹۳ آدرس نقشهی سایت حداکثر ۲ کلیک از صفحهی اصلی فاصله دارند (قبلاً تا ۵ کلیک). تستها: linkcheck (۶۲۷ صفحه، ۶۴۳ لنگر، ۵۴۷ عکس) سالم، clickcheck ۲۸۸ کلیک بدون خطا، audit بدون هشدار جدید، و صفحهی تازه در ۳۲۰ تا ۱۴۴۰ پیکسل بدون اسکرول افقی. حجم روی شبکه ۱۴ کیلوبایت، پاسخ ۲۴ms. نکتههای ۴۲ و ۴۳ به CLAUDE.md اضافه شد. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
مخزن گیتهاب عمومی است و رمز پیشفرض پنل یک مقدار ثابت در سورس بود (`foolad1234`) که در پنج فایل مستندات هم تکرار شده بود. زنجیرهی نفوذ کامل روی سرور تست محلی اجرا و تأیید شد: خواندن رمز از سورس عمومی → حل کپچا (جمع ساده، با چشم شدنی) → ورود موفق → صفحهی تغییر اجباری رمز → گذاشتن رمز مهاجم → دسترسی کامل به /admin، /admin/products و /admin/settings نکتهی مهم: اجبار تغییر رمز این حمله را متوقف نمیکرد. آن سازوکار سالم است (تست شد که هر هفت مسیر پنل و همچنین POSTها تا تغییر رمز بسته میمانند) ولی فقط تعیین میکند چه کسی زودتر میرسد — مالک یا مهاجم. اصلاح: رمز اولیه با crypto.randomInt بهصورت تصادفی (۱۴ کاراکتر) ساخته و در لاگ اولین اجرا چاپ میشود. الفبا بدون 0/O و 1/l/I است چون از روی لاگ دستی تایپ میشود. مسیر ADMIN_PASSWORD مثل قبل کار میکند. تست بعد از اصلاح: همان درخواست ورود که قبلاً ۳۰۲ میداد حالا ۴۰۱ میگیرد. دو نصب تازه دو رمز متفاوت ساختند. linkcheck روی ۶۴۳ لنگر سالم. بررسیهای دیگر که مشکلی نداشتند: تاریخچهی گیت بدون هیچ .env/دیتابیس/ کلید، بدون همکار اضافه، و وُرکفلوی استقرار فقط workflow_dispatch است پس LIARA_API_TOKEN در دسترس PR از فورک نیست. نکتههای ۴۴ و ۴۵ به CLAUDE.md اضافه شد. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
سایت کامل کاتالوگ محصولات برای گروه تولیدی صنعتی فولاد ایمان (علیآباد کتول و گرگان)
با استک Node.js + Express + SQLite + EJS + sharp.
صفحات عمومی:
«چرا ما» و منطقهی خدمات
پنل مدیریت (/admin):
امنیت: محافظت CSRF روی همهی فرمها، محدودیت تلاش ورود، هدرهای helmet با CSP و nonce،
کوکی httpOnly، بازسازی نشست بعد از ورود و noindex روی کل پنل.
سرعت: رندر سمت سرور، فشردهسازی، فونت سلفهاست Vazirmatn، lazy-loading و srcset،
بدون هیچ کتابخانهی سنگین سمت کاربر.
سئوی محلی: Schema.org LocalBusiness با areaServed، Product schema با وضعیت موجودی،
BreadcrumbList، متای اختصاصی هر صفحه، sitemap.xml و robots.txt پویا.
ساختار کانالهای ارتباطی طوری نوشته شده که افزودن تلگرام در آینده فقط با تغییر
یک تنظیم در src/config/site.js انجام شود.
Co-Authored-By: Claude Opus 5 noreply@anthropic.com
Claude-Session: https://claude.ai/code/session_01XujcJWcHbNHYFHaqkDkvbC