diff --git a/README.md b/README.md index 02695bc..877eb9d 100644 --- a/README.md +++ b/README.md @@ -1 +1,34 @@ -This is a starter template for [Learn Next.js](https://nextjs.org/learn). \ No newline at end of file +# وبسایت بیمارستان مهر پرند + +پروژه با Next.js 11 و Pages Router اجرا میشود. + +## اجرا و بررسی + +```sh +npm run dev +npm run build +npm run start +npm test +``` + +تستها با test runner داخلی Node.js اجرا میشوند و به Node.js 20 یا جدیدتر نیاز دارند. بیلد فعلی با Node.js 14.21.3 بررسی شده است؛ ارتقای نسخههای قدیمی Next.js و Node.js باید جداگانه انجام شود. تنظیمات و نسخه ESLint موجود با فرایند lint این نسخه Next.js سازگار نیست و بیلد هشدار میدهد؛ موفقیت بیلد به معنی موفقیت lint نیست. + +## تنظیمات سئو + +- عنوان و توضیحات صفحات عمومی و آدرس اصلی سایت در `lib/seo.js` تعریف شدهاند. هنگام افزودن صفحه عمومی، آن را به `PUBLIC_PAGES` اضافه کنید؛ این فهرست منبع صفحات ثابت sitemap نیز هست. +- `src/components/Seo.jsx` از طریق `_app.js` متادیتا، canonical، Open Graph، Twitter Card و JSON-LD را در HTML سرور تولید میکند. متادیتای محتوای پویا را با `props.seo` از `getServerSideProps` ارسال کنید. +- canonical از دامنه ثابت HTTPS ساخته میشود و query string و fragment را حذف میکند. عنوان سفارشی صفحات را در registry یا `props.seo` تنظیم کنید؛ prop قدیمی `AppContainer.title` دیگر منبع متادیتا نیست. +- صفحات فنی، کامپوننتهای مستقل، فرمهای درخواست و پیگیری و نمودارهایی که محتوایشان صرفاً سمت مرورگر است، بهصورت پیشفرض `noindex, follow` هستند. صفحه عمومی بخشها در `/components/service` استثنا است. `noindex` جایگزین کنترل دسترسی نیست. +- سند HTML دارای `lang="fa"` و `dir="rtl"` است. داده ساختاریافته بیمارستان از اطلاعات تماس موجود سایت ساخته شده؛ ساعت کاری یا تاریخ افتتاح فرضی درج نشده است. + +## نقشه سایت و اخبار + +آدرس عمومی نقشه سایت `https://miphospital.ir/sitemap.xml` است و به handler موجود `/api/my-sitemap` بازنویسی میشود. robots.txt همین آدرس HTTPS را معرفی میکند. + +نقشه سایت صفحات عمومی و شناسههای معتبر اخبار برگشتی از `AppUrl('news')` را شامل میشود. اگر API اخبار در دسترس نباشد، صفحات ثابت همچنان برگردانده میشوند و زمان cache به ۶۰ ثانیه کاهش مییابد. API فعلی باید آرایه اخبار یا آبجکت دارای آرایه `data` برگرداند؛ در صورت اضافه شدن pagination، پیمایش صفحههای API نیز باید اضافه شود. + +خبر ناموجود پاسخ ۴۰۴ دارد؛ خرابی موقت API جزئیات خبر به خطای سرور منجر میشود. فهرست اخبار هنگام خرابی API پاسخ ۵۰۳ با `Retry-After` میدهد تا خطا بهعنوان محتوای خالی موفق ثبت نشود. + +در بررسی محلی، API فعلی اخبار (`https://mehrara-api.iran.liara.run/api/news`) پاسخ ۴۰۴ داد؛ در نتیجه مسیرهای واقعی اخبار و محتوای زنده آنها تا رفع API قابل بررسی نیستند. بعد از انتشار، sitemap را در Google Search Console ثبت و صفحات اصلی را با URL Inspection بررسی کنید. + +مرجع: [راهنمای فنی سئو برای توسعهدهندگان در Google Search Central](https://developers.google.com/search/docs/fundamentals/get-started-developers). diff --git a/lib/seo.js b/lib/seo.js new file mode 100644 index 0000000..e6c239e --- /dev/null +++ b/lib/seo.js @@ -0,0 +1,116 @@ +const SITE_URL = 'https://miphospital.ir' +const SITE_NAME = 'بیمارستان مهر پرند' +const DEFAULT_DESCRIPTION = 'وبسایت رسمی بیمارستان مهر ایرانیان پرند؛ معرفی بیمارستان، بخشهای تخصصی، اخبار افتتاح، فرصتهای شغلی و راههای ارتباط با ما.' +const DEFAULT_IMAGE = `${SITE_URL}/resource/logo/logo-green.png` + +// This registry also defines the public, canonical pages included in the sitemap. +const PUBLIC_PAGES = { + '/': [SITE_NAME, DEFAULT_DESCRIPTION], + '/landing': ['معرفی بیمارستان مهر پرند', 'با بیمارستان مهر پرند، زیرساختهای درمانی، بخشهای تخصصی، امکانات و مسیرهای ارتباط با بیمارستان آشنا شوید.'], + '/page/about': ['درباره بیمارستان', 'معرفی بیمارستان مهر ایرانیان پرند، اهداف، امکانات و آخرین وضعیت پیشرفت پروژه بیمارستان در شهر پرند.'], + '/page/contact-us': ['تماس با ما', 'آدرس بیمارستان مهر پرند در فاز ۵، بلوار شهید قاسم سلیمانی؛ شماره تماس ۰۲۱-۵۳۴۵۷۰۰۰، ایمیل و موقعیت بیمارستان روی نقشه.'], + '/page/Directors': ['تیم پروژه بیمارستان', 'آشنایی با اعضای تیم و مدیران پروژه بیمارستان مهر ایرانیان پرند.'], + '/page/docs': ['اسناد رسمی شرکت', 'دسترسی به اساسنامه و اسناد رسمی شرکت پزشکی مهر ایرانیان پرند.'], + '/page/cooperationsales': ['همکاری در فروش', 'اطلاعات و شرایط همکاری در فروش با شرکت پزشکی مهر ایرانیان پرند.'], + '/Gallery': ['گالری تصاویر بیمارستان', 'تصاویر ساختمان، فضاهای داخلی و روند پیشرفت پروژه بیمارستان مهر پرند.'], + '/News': ['اخبار و اطلاعیهها', 'اخبار، اطلاعیهها و فایلهای معرفی بیمارستان مهر ایرانیان پرند.'], + '/questions': ['سوالات متداول', 'پاسخ به سوالات متداول درباره بیمارستان مهر پرند و امکانات باشگاه سهامداران.'], + '/recruitment': ['فرصتهای شغلی و استخدام', 'مشاهده فرصتهای شغلی و گروههای استخدامی بیمارستان مهر ایرانیان پرند و ارسال درخواست همکاری.'], + '/medical-equipment': ['واحد تجهیزات پزشکی', 'آشنایی با واحد تجهیزات پزشکی بیمارستان مهر پرند و راههای ارتباط با این واحد.'], + '/components/service': ['کلینیکها و بخشها', 'معرفی کلینیکها و بخشهای تخصصی بیمارستان مهر پرند و امکانات هر بخش.'], + '/stock-unit': ['واحد امور سهام', 'اطلاعات واحد امور سهام بیمارستان مهر پرند و دسترسی به خدمات و راههای ارتباط با این واحد.'] +} + +const UTILITY_TITLES = { + '/CooperationRequest': 'درخواست همکاری', + '/recruitment/inquiry': 'استعلام وضعیت درخواست استخدام', + '/stock-unit/stock-check': 'پیگیری درخواست سهام', + '/stock-unit/stock-sales': 'درخواست خرید سهام', + '/stock-unit/stock-transfer': 'درخواست انتقال سهام', + '/stock-price': 'قیمت سهام بیمارستان مهر پرند', + '/page/stock-price': 'نمودار قیمت سهام', + '/page/sales': 'ارتباط با واحد فروش', + '/smartadvice': 'مشاوره سرمایهگذاری' +} + +function canonicalUrl(path = '/') { + const clean = String(path).split(/[?#]/)[0] + if (!clean.startsWith('/') || clean.startsWith('//') || clean.includes('\\')) return `${SITE_URL}/` + return SITE_URL + (clean.replace(/\/+$/, '') || '/') +} + +function getSeo(path = '/', overrides = {}) { + const canonical = canonicalUrl(path) + const pathname = canonical.slice(SITE_URL.length) + const page = PUBLIC_PAGES[pathname] + const dynamicPublic = /^\/page\/news\/[^/]+$/.test(pathname) || /^\/recruitment\/item\/[^/]+$/.test(pathname) + return { + title: page ? page[0] : UTILITY_TITLES[pathname] || (pathname.startsWith('/recruitment/item/request/') ? 'ثبت درخواست استخدام' : SITE_NAME), + description: page ? page[1] : DEFAULT_DESCRIPTION, + image: DEFAULT_IMAGE, + type: 'website', + noindex: !page && !dynamicPublic, + ...overrides, + canonical + } +} + +function newsSeo(post, slug) { + const text = String(post.text || '') + .replace(/<(script|style)\b[^>]*>[\s\S]*?<\/\1>/gi, '') + .replace(/<[^>]*>/g, ' ') + .replace(/ | /g, ' ') + .replace(/&/g, '&') + .replace(/\s+/g, ' ').trim() + const description = text.slice(0, 160) || post.title + const image = post.cover ? `https://mehrara-api.iran.liara.run/panel-public/uploads/${post.cover}` : DEFAULT_IMAGE + const canonical = canonicalUrl(`/page/news/${encodeURIComponent(slug)}`) + return { + title: post.title, + description, + image, + type: 'article', + structuredData: { + '@context': 'https://schema.org', + '@type': 'NewsArticle', + headline: post.title, + description, + image: [image], + mainEntityOfPage: canonical, + publisher: { '@id': `${SITE_URL}/#hospital` } + } + } +} + +const hospitalSchema = { + '@context': 'https://schema.org', + '@type': 'Hospital', + '@id': `${SITE_URL}/#hospital`, + name: SITE_NAME, + alternateName: 'بیمارستان مهر ایرانیان پرند', + url: `${SITE_URL}/`, + logo: DEFAULT_IMAGE, + telephone: '+98-21-53457000', + email: 'contact-us@miphospital.ir', + address: { + '@type': 'PostalAddress', + addressCountry: 'IR', + addressRegion: 'تهران', + addressLocality: 'پرند', + streetAddress: 'فاز ۵، بلوار شهید قاسم سلیمانی، نبش بلوار رسول اکرم (ص)' + }, + sameAs: ['https://www.instagram.com/miphospital/'] +} + +function serializeJsonLd(value) { + return JSON.stringify(value).replace(/ post && /^\d+$/.test(String(post.id))) + .map(post => `/page/news/${post.id}`) + return [...new Set([...Object.keys(PUBLIC_PAGES), ...articles])] +} + +module.exports = { SITE_URL, SITE_NAME, canonicalUrl, getSeo, newsSeo, hospitalSchema, serializeJsonLd, sitemapLinks } diff --git a/next.config.js b/next.config.js index aea2183..e12ae0e 100644 --- a/next.config.js +++ b/next.config.js @@ -4,13 +4,14 @@ module.exports = { experimental: { outputStandalone: true }, - siteUrl: 'https://miphospital.ir', - generateRobotsTxt: true, // (optional) ...withImages(), // future: { // webpack5: true // }, images: { disableStaticImages: true + }, + async rewrites() { + return [{ source: '/sitemap.xml', destination: '/api/my-sitemap' }] } } diff --git a/package.json b/package.json index 7f41882..4534bdb 100644 --- a/package.json +++ b/package.json @@ -3,6 +3,7 @@ "version": "0.1.0", "private": true, "scripts": { + "test": "node --test tests/*.test.cjs", "dev": "next dev -p 2000", "build": "next build", "start": "next start -p 2000" diff --git a/pages/News.js b/pages/News.js index 98339a8..93ea835 100644 --- a/pages/News.js +++ b/pages/News.js @@ -1,105 +1,59 @@ -import React from 'react' -import { - MDBCard, - MDBCardImage, - MDBCardBody, - MDBCardTitle, - MDBCardText, - MDBRow, - MDBCol, - MDBContainer, - MDBCardHeader, - MDBBtn, - MDBIcon, - MDBInputGroupText, - MDBTypography -} from 'mdb-react-ui-kit' -import axios from 'axios' -import Link from 'next/link' -import AppContainer from './appContainer' -import { useRouter } from 'next/router' -import { AppUrl } from '../lib/api' -import { DownloadIcon } from '@chakra-ui/icons' - -const News = ({ data, children }) => { - const router = useRouter() - const query = router.query.list - - const dataPage = ( -
دریافت اخبار موقتاً امکانپذیر نیست. لطفاً کمی بعد دوباره مراجعه کنید.
} +