خواهرم در تابلوی مشاغل NHS اسکاتلند، مشخصاً در بخش فرصتهای شغلی پزشکی و دندانپزشکی، دنبال کار میگشت. آگهیهای جدید میتوانند خیلی سریع بسته شوند، پس تنها راه مطمئن برای از دست ندادن آنها این بود که صفحه را مدام رفرش کند — بارها و بارها، تمام روز، به امید اینکه همان لحظهی ظاهر شدن یک آگهی را بگیرد.
چالش: رفرش کردن دستی متوقف شود و همان لحظه که یک آگهی جدید ظاهر میشود، یک اعلان دریافت شود — فقط برای استفادهی شخصی و غیرتجاری، و رایگان برای اجرای نامحدود.
هیچ APIای وجود ندارد، اما چیز بهتری هست
اولین قدم این بود که ببینم آیا NHS اسکاتلند چیزی در اختیار میگذارد که بتوانم مستقیماً صدا بزنم. نداشت — هیچ API عمومی برای مشاغل وجود نداشت. راهحل جایگزین بدیهی یک مرورگر headless بود: اجرای Playwright روی صفحه، اجازه دادن به رندر شدن، و استخراج داده از DOM.
قبل از رفتن سراغ آن، تب شبکهی مرورگر را باز کردم تا ببینم صفحه واقعاً چطور نتایجش را بارگذاری میکند. معلوم شد اندپوینت داخلی خود سایت — همان که دکمهی Load more صدا میزند — فهرست مشاغل را بهصورت HTML کاملاً رندرشده برمیگرداند، با تمام جزئیات، بدون هیچ رندر سمت کلاینتی که لازم باشد شبیهسازی شود. یعنی اصلاً به مرورگر نیازی نداشتم: یک درخواست HTTP ساده بههمراه Cheerio برای پارس کردن HTML کافی بود تا همان دادهی ساختاریافتهای را بگیرم که Playwright میگرفت، آن هم با کسری از حافظه و زمان راهاندازی.
این یک ابزار شخصی و تکگیرنده بود، نه یک اسکرِیپر که دادههای NHS را بازفروشی یا بازنشر کند — یک درخواست سبک در دقیقه، برای یک خانوادهی شغلی، خواندهشده توسط یک نفر. این ردپای بسیار متفاوتی نسبت به اسکرِیپینگ در مقیاس بزرگ دارد، به همین دلیل هرگز نگرانیهایی که اسکرِیپینگ تجاری یا انبوه ایجاد میکند را برنینگیخت.
نسخهی اول: سایتی در Next.js که خودش را میخزید
نسخهی اول یک اپلیکیشن تکی در Next.js بود، میزبانیشده روی پلن رایگان Hobby ورسل. صفحهی NHS را با یک بازهی زمانی میخزید، مشاغل را پارس میکرد و آنها را با فیلترهایی که او واقعاً خواسته بود نمایش میداد — تقسیمشده به مشاغل قدیمی و جدید، بههمراه یک نمای کلی سریع از هر آگهی — نه یک داشبورد عمومی که حدس زده بودم بخواهد.
برای تشخیص قدیمی از جدید بدون پایگاهداده، این مرورگر بود که بهخاطر میسپرد: مشاغل قبلاً دیدهشده در localStorage ذخیره میشدند، و یک دکمه در رابط کاربری به او اجازه میداد دستهی فعلی را «دیدهشده» علامت بزند تا خزش بعدی بتواند آن را مقایسه کند و هرچیز جدیدی را پرچمگذاری کند.

مشکل نسخهی اول
کار میکرد، اما فقط وقتی کار میکرد که چیزی فعالانه به آن نگاه میکرد:
- یک دستگاه باید روی صفحه میماند تا حلقهی خزش و مقایسهی localStorage اصلاً اجرا شود.
- اعلانهای مرورگر فقط وقتی فعال میشوند که تب توانایی اجرا داشته باشد — قفل کردن گوشی یا تعویض اپلیکیشن یعنی NHS میتوانست ده آگهی منتشر کند و هیچ اتفاقی نیفتد.
- حتی با اجازهی اعلان، راه فراری نبود: یک دستگاه، یک جایی، باید روی آن تب مینشست، بیدار.
برای یک سیستم هشداردهی همیشهروشن، کلاینت نمیتواند چیزی باشد که زمان را نگه میدارد. این باید به سمت سرور منتقل شود.
نسخهی دوم: یک بکاند واقعی
پس خزنده را کاملاً از سایت جدا کردم. تبدیل شد به یک API مستقل بر پایهی Express.js — که آن هم رایگان روی ورسل میزبانی میشد — که تنها کارش خزیدن NHS، پارس مشاغل با Cheerio، و ذخیرهی آنها در MongoDB بود: مشاغل فعلی، بههمراه لاگی از هر تلاش خزش (تعداد، زمانبندی، خطاها).

هنوز چیزی لازم بود که هر دقیقه خزش را فعال کند. پلن رایگان ورسل حلقهی پسزمینه اجرا نمیکند، پس زمانبند رایگان cron-job.org را بهجای آن به اندپوینت خزش وصل کردم — که برای همیشه، هر دقیقه API را صدا میزند، بدون نیاز به سروری که مجبور باشم برای روشن نگهداشتنش پول بدهم.

برای اعلانها، از Firebase Cloud Messaging صرفنظر کردم — که همچنان به یک دستگاه ثبتشده و در دسترس نیاز داشت — و بهجایش یک بات تلگرام راه انداختم. همان لحظه که خزش مشاغلی پیدا کند که هنوز در Mongo نیستند، مستقیماً به او پیام میدهد. بدون اپلیکیشنی که باز بماند، بدون درخواست مجوز، بدون دستگاهی که باید بیدار باشد — فقط یک پیام که ظاهر میشود.
نسخهی اول — فقط Next.js
- ·حلقهی خزش در تب مرورگر اجرا میشود
- ·تشخیص جدید و قدیمی در localStorage
- ·به دستگاهی نیاز دارد که همیشه روی صفحه و بیدار باشد
- ·اعلانها فقط وقتی در پیشزمینه است فعال میشوند
نسخهی دوم — API + پایگاهداده + بات
- ✓حلقهی خزش سمت سرور اجرا میشود، توسط cron-job.org فعال میشود
- ✓تشخیص جدید و قدیمی در MongoDB
- ✓هیچ دستگاهی نیاز نیست باز یا بیدار باشد
- ✓تلگرام مستقیماً هشدار را تحویل میدهد

نتیجهی نهایی
او ظرف یک دقیقه از انتشار یک فرصت شغلی جدید در حوزهی پزشکی و دندانپزشکی روی NHS اسکاتلند، پیام تلگرام دریافت میکند. بدون تب باز، بدون صفحهی گوشی، بدون رفرش دستی. و کل ماجرا — API خزنده، پایگاهداده، محرک کرون، بات — همچنان روی لایههای رایگان اجرا میشود: Vercel Hobby، لایهی رایگان MongoDB، cron-job.org، و تلگرام.
چه چیزی از این تجربه یاد گرفتم
- قبل از رفتن سراغ یک مرورگر headless، ببینید دکمهی «load more» خود صفحه واقعاً چه چیزی را صدا میزند — خیلی از سایتها بدون API عمومی، همچنان زیرِ پوسته HTML ساختاریافته سرو میکنند.
- اگر «پایش همیشگی» یک نیاز است، از همان روز اول ساعت را از کلاینت جدا کنید. یک تب، زمانبند نیست.
- برای یک گیرندهی تکی، یک بات بهسبک webhook (تلگرام) بهتر از یک سرویس پوشنوتیفیکیشن است — بدون فرآیند ثبتنام، بدون مجوز، بدون SDK کلاینتی که باید بهروز نگهداشت.