fix: seo friendly

This commit is contained in:
reza karimi
2026-10-17 17:20:59 +03:30
parent f6c889e633
commit 3d7485bbfd
26 changed files with 417 additions and 192 deletions
+34 -1
View File
@@ -1 +1,34 @@
This is a starter template for [Learn Next.js](https://nextjs.org/learn). # وب‌سایت بیمارستان مهر پرند
پروژه با 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).
+116
View File
@@ -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(/&nbsp;|&#160;/g, ' ')
.replace(/&amp;/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(/</g, '\\u003c')
}
function sitemapLinks(news = []) {
const articles = news
.filter(post => 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 }
+3 -2
View File
@@ -4,13 +4,14 @@ module.exports = {
experimental: { experimental: {
outputStandalone: true outputStandalone: true
}, },
siteUrl: 'https://miphospital.ir',
generateRobotsTxt: true, // (optional)
...withImages(), ...withImages(),
// future: { // future: {
// webpack5: true // webpack5: true
// }, // },
images: { images: {
disableStaticImages: true disableStaticImages: true
},
async rewrites() {
return [{ source: '/sitemap.xml', destination: '/api/my-sitemap' }]
} }
} }
+1
View File
@@ -3,6 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"test": "node --test tests/*.test.cjs",
"dev": "next dev -p 2000", "dev": "next dev -p 2000",
"build": "next build", "build": "next build",
"start": "next start -p 2000" "start": "next start -p 2000"
+59 -105
View File
@@ -1,105 +1,59 @@
import React from 'react' import React from 'react'
import { import axios from 'axios'
MDBCard, import Link from 'next/link'
MDBCardImage, import { useRouter } from 'next/router'
MDBCardBody, import AppContainer from './appContainer'
MDBCardTitle, import { AppUrl } from '../lib/api'
MDBCardText, import { newsSeo } from '../lib/seo'
MDBRow,
MDBCol, export default function News({ data = [], unavailable = false, children }) {
MDBContainer, const router = useRouter()
MDBCardHeader, const isHome = router.pathname === '/'
MDBBtn, const posts = (Array.isArray(data) ? data : []).filter(post => post && post.title && /^\d+$/.test(String(post.id)))
MDBIcon, if (isHome && posts.length === 0) return null
MDBInputGroupText,
MDBTypography const content = (
} from 'mdb-react-ui-kit' <section className='lg:mx-20 sm:mx-5 my-5 RTL' aria-labelledby='news-title'>
import axios from 'axios' {isHome ? <h2 id='news-title'>اخبار و اطلاعیه‌ها</h2> : <h1 id='news-title'>اخبار و اطلاعیه‌های بیمارستان مهر پرند</h1>}
import Link from 'next/link' <hr />
import AppContainer from './appContainer' {unavailable && <p role='status'>دریافت اخبار موقتاً امکان‌پذیر نیست. لطفاً کمی بعد دوباره مراجعه کنید.</p>}
import { useRouter } from 'next/router' <div className='row row-cols-1 row-cols-md-3 g-4'>
import { AppUrl } from '../lib/api' {posts.map(post => (
import { DownloadIcon } from '@chakra-ui/icons' <article className='col' key={post.id}>
<div className='card h-100 p-4'>
const News = ({ data, children }) => { <h2 className='h5'>
const router = useRouter() <Link href={`/page/news/${post.id}`}><a>{post.title}</a></Link>
const query = router.query.list </h2>
<p>{newsSeo(post, post.id).description}</p>
const dataPage = ( <Link href={`/page/news/${post.id}`}><a>مشاهده خبر</a></Link>
<div className='lg:mx-20 sm:mx-5'> </div>
<h5 style={{ marginTop: '2rem' }} className={'right-align font-weight-bold'}> </article>
اطلاعیه ها{' '} ))}
</h5> </div>
<hr /> {!isHome && (
<div> <div className='mt-4'>
<MDBTypography <h2 className='h5'>فایل‌های معرفی بیمارستان</h2>
style={{ direction: 'rtl', borderRadius: '6px solid', borderRight: '6px solid #1266f1', borderLeft: 'none' }} <p><a download href='/take-stock.pdf'>دانلود دفترچه معرفی و فروش سهام عمومی بیمارستان مهر پرند</a></p>
note <p><a download href='/Explanatory_plan_of_Mehr_ranian_Parand_Hospital.pdf'>دانلود طرح توجیهی بیمارستان مهر پرند</a></p>
noteColor='primary' </div>
> )}
<DownloadIcon className={'ml-2'} /> {children}
<a </section>
title={'دانلود دفترچه معرفی و فروش سهام عمومی بیمارستان تخصصی مهر ایرانیان پرند'} )
download
href={'./take-stock.pdf'} return isHome ? content : <AppContainer>{content}</AppContainer>
> }
{' '}
دانلود دفترچه معرفی و فروش سهام عمومی بیمارستان تخصصی مهر پرند export async function getServerSideProps({ res }) {
</a> try {
</MDBTypography> const { data } = await axios.get(AppUrl('news'), { timeout: 10000 })
<MDBTypography const posts = Array.isArray(data) ? data : data && data.data
style={{ direction: 'rtl', borderRadius: '6px solid', borderRight: '6px solid #1266f1', borderLeft: 'none' }} if (!Array.isArray(posts)) throw new Error('Unexpected news response')
note return { props: { data: posts } }
noteColor='primary' } catch (error) {
> console.warn('News list: API unavailable:', error.message)
<DownloadIcon className={'ml-2'} /> res.statusCode = 503
<a res.setHeader('Retry-After', '60')
title={'دانلود طرح توجیهی بیمارستان تخصصی مهر ایرانیان پرند'} return { props: { data: [], unavailable: true } }
download }
href={'./Explanatory_plan_of_Mehr_ranian_Parand_Hospital.pdf'} }
>
{' '}
دانلود طرح توجیهی بیمارستان تخصصی مهر ایرانیان پرند
</a>
</MDBTypography>
</div>
<br />
{/* <MDBRow dir={'rtl'} className='row-cols-1 row-cols-md-3 g-4'>
{data && data.map(({ id, slug, title }) => (
<MDBCol key={id}>
<Link href={`page/news/${id}`} >
<MDBCard className='mb-3 h-3/12 new-card'>
<MDBCardHeader title={title} className={'right-align bk-med RTL font-weight-bold border-bottom-1 border-white '}>{title} </MDBCardHeader>
<MDBCardBody>
<MDBCardText className={'right-align RTL text-summery'}>
{slug}
</MDBCardText>
<div className={'flex justify-content-end'}>
<MDBBtn rounded className=' RTL text-primary-teal bg-white'>
مشاهده
</MDBBtn>
</div>
</MDBCardBody>
</MDBCard>
</Link>
</MDBCol>
))}
</MDBRow> */}
{children}
</div>
)
return <div>{/* {query ? <AppContainer children={dataPage} title={'اطلاعیه ها '} /> : dataPage} */}</div>
}
export default News
export const getServerSideProps = async context => {
// const allPostsData = await axios.get(AppUrl('news'))
// const data = await allPostsData.data
return {
props: {
data: null,
fallback: false
} // will be passed to the page component as props
}
}
+17 -5
View File
@@ -7,7 +7,7 @@ import "../styles/simple-grid.css"
import 'nprogress/nprogress.css'; import 'nprogress/nprogress.css';
import "react-lightbox-pack/dist/index.css"; import "react-lightbox-pack/dist/index.css";
import Router from "next/router"; import Router from "next/router";
import React, {useState} from "react"; import React, {useEffect, useState} from "react";
import Loader from "./components/Loader"; import Loader from "./components/Loader";
import NProgress from 'nprogress'; import NProgress from 'nprogress';
import {ChakraProvider} from "@chakra-ui/react"; import {ChakraProvider} from "@chakra-ui/react";
@@ -15,6 +15,7 @@ import { extendTheme } from '@chakra-ui/react'
import { StepsStyleConfig as Steps } from 'chakra-ui-steps'; import { StepsStyleConfig as Steps } from 'chakra-ui-steps';
import {Provider} from "react-redux"; import {Provider} from "react-redux";
import {CoreStore} from "../src/redux/store/store"; import {CoreStore} from "../src/redux/store/store";
import Seo from '../src/components/Seo';
const theme = extendTheme({ const theme = extendTheme({
components: { components: {
@@ -25,16 +26,27 @@ const theme = extendTheme({
export default function App({ Component, pageProps }) { export default function App({ Component, pageProps }) {
const [Loading,setLoading] = useState(false); const [Loading,setLoading] = useState(false);
Router.events.on('routeChangeStart',(url) => { useEffect(() => {
const start = () => {
NProgress.start() NProgress.start()
setLoading(true) setLoading(true)
}) }
Router.events.on('routeChangeComplete',(url) => { const complete = () => {
NProgress.done() NProgress.done()
setLoading(false) setLoading(false)
}) }
Router.events.on('routeChangeStart', start)
Router.events.on('routeChangeComplete', complete)
Router.events.on('routeChangeError', complete)
return () => {
Router.events.off('routeChangeStart', start)
Router.events.off('routeChangeComplete', complete)
Router.events.off('routeChangeError', complete)
}
}, [])
return ( return (
<Provider store={CoreStore}> <Provider store={CoreStore}>
<Seo metadata={pageProps.seo || (pageProps.statusCode >= 400 ? { noindex: true } : {})} />
{Loading && <Loader />} {Loading && <Loader />}
<ChakraProvider resetCSS={false} theme={theme}> <ChakraProvider resetCSS={false} theme={theme}>
{/* <link href="https://use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" /> */} {/* <link href="https://use.fontawesome.com/releases/v5.15.1/css/all.css" rel="stylesheet" /> */}
+15
View File
@@ -0,0 +1,15 @@
import Document, { Html, Head, Main, NextScript } from 'next/document'
export default class MyDocument extends Document {
render() {
return (
<Html lang='fa' dir='rtl'>
<Head />
<body>
<Main />
<NextScript />
</body>
</Html>
)
}
}
+26 -25
View File
@@ -1,25 +1,26 @@
const { SitemapStream, streamToPromise } = require("sitemap"); import axios from 'axios'
const { Readable } = require("stream"); import { SitemapStream, streamToPromise } from 'sitemap'
import { Readable } from 'stream'
export default async (req, res) => { import { AppUrl } from '../../lib/api'
// An array with your links import { SITE_URL, sitemapLinks } from '../../lib/seo'
const links = [
{ url: "/page/news/36/", priority: 0.3 }, export default async function sitemap(req, res) {
{ url: "/page/about", priority: 0.3 }, let news = []
{ url: "/recruitment", priority: 0.3 }, let newsAvailable = false
{ url: "/contact-us", priority: 0.3 } try {
]; const { data } = await axios.get(AppUrl('news'), { timeout: 5000 })
const posts = Array.isArray(data) ? data : data && data.data
// Create a stream to write to if (!Array.isArray(posts)) throw new Error('Unexpected news response')
const stream = new SitemapStream({ hostname: `https://${req.headers.host}` }); news = posts
newsAvailable = true
res.writeHead(200, { } catch (error) {
"Content-Type": "application/xml", // Keep core pages discoverable during a news API outage; retry soon.
}); console.warn('Sitemap: news API unavailable:', error.message)
}
const xmlString = await streamToPromise(
Readable.from(links).pipe(stream) const stream = new SitemapStream({ hostname: SITE_URL })
).then((data) => data.toString()); const xml = await streamToPromise(Readable.from(sitemapLinks(news).map(url => ({ url }))).pipe(stream))
res.setHeader('Content-Type', 'application/xml; charset=utf-8')
res.end(xmlString); res.setHeader('Cache-Control', newsAvailable ? 'public, s-maxage=3600, stale-while-revalidate=86400' : 'public, s-maxage=60')
}; res.status(200).send(xml.toString())
}
+2 -9
View File
@@ -6,18 +6,11 @@ import dynamic from 'next/dynamic'
import { Router } from 'next/router' import { Router } from 'next/router'
const Chat = dynamic(() => import('./pack'), { ssr: false }) const Chat = dynamic(() => import('./pack'), { ssr: false })
const AppContainer = ({ children, title }) => { const AppContainer = ({ children }) => {
return ( return (
<div> <div>
<Head> <Head>
<title>{title ? title : 'بیمارستان مهر پرند '}</title>
<meta name='description' content='بیمارستان 96 تخت خوابی مهر ایرانیان پرند واقع در شهر پرند ' />
<meta
name='keywords'
content=' درمانگاه مهرپرند,بیمارستان مهرایرانیان پرند ,بیمارستان, بیمارستان مهر, سهام,پرند'
/>
<meta name="google-site-verification" content="hygRCp6fRa9qi8RXCaAosXK4mBcnCJVRH1ayHtAQy8o" /> <meta name="google-site-verification" content="hygRCp6fRa9qi8RXCaAosXK4mBcnCJVRH1ayHtAQy8o" />
<link rel='icon' type='image/x-icon' href='/favicon.ico' />
<script <script
type='text/javascript' type='text/javascript'
dangerouslySetInnerHTML={{ dangerouslySetInnerHTML={{
@@ -33,7 +26,7 @@ const AppContainer = ({ children, title }) => {
</Head> </Head>
<Chat /> <Chat />
<Header /> <Header />
{children} <main id='main-content'>{children}</main>
<Footer /> <Footer />
</div> </div>
) )
+2 -2
View File
@@ -248,7 +248,7 @@ const Service = () => {
<MDBCardBody style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}> <MDBCardBody style={{ display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
<Link href={'components/service'}> <Link href={'components/service'}>
<MDBCardText className={'bk'}> <MDBCardText className={'bk'}>
<h1 className={'RTL dana'}>بیشتر ...</h1>{' '} <span className={'RTL dana'}>بیشتر ...</span>{' '}
</MDBCardText> </MDBCardText>
</Link> </Link>
</MDBCardBody> </MDBCardBody>
@@ -262,7 +262,7 @@ const Service = () => {
) : ( ) : (
<AppContainer title={'کلینیک ها و بخش ها '}> <AppContainer title={'کلینیک ها و بخش ها '}>
<div className='parallax-container'> <div className='parallax-container'>
<h2 className={'center-align text-dark service-text dana font-weight-bold'}> کلینیک ها و بخش ها </h2> <h1 className={'center-align text-dark service-text dana font-weight-bold'}> کلینیک ها و بخش ها </h1>
<Image <Image
loader={Parallax} loader={Parallax}
src='Parallax.png' src='Parallax.png'
+3 -3
View File
@@ -39,12 +39,12 @@ const Slider = () => {
</SwiperSlide> */} </SwiperSlide> */}
<SwiperSlide key={3} style={{ cursor: 'pointer' }} className='pointer'> <SwiperSlide key={3} style={{ cursor: 'pointer' }} className='pointer'>
<Link href='/landing'> <Link href='/landing'>
<img src={sliderMIpStart} /> <a><img src={sliderMIpStart} alt='معرفی بیمارستان مهر پرند و آخرین اخبار افتتاح' width='2071' height='759' style={{ width: '100%', height: 'auto' }} /></a>
</Link> </Link>
</SwiperSlide> </SwiperSlide>
<SwiperSlide key={3} style={{ cursor: 'pointer' }} className='pointer'> <SwiperSlide key={4} style={{ cursor: 'pointer' }} className='pointer'>
<Link href='/recruitment'> <Link href='/recruitment'>
<img src={sliderJob} /> <a><img src={sliderJob} alt='فرصت‌های شغلی و استخدام در بیمارستان مهر پرند' width='2877' height='1120' style={{ width: '100%', height: 'auto' }} loading='lazy' /></a>
</Link> </Link>
</SwiperSlide> </SwiperSlide>
</Swiper> </Swiper>
+9 -5
View File
@@ -66,7 +66,7 @@ export default function Home({ data_NEWS }) {
font-size: 0.85rem; font-size: 0.85rem;
font-weight: 700; font-weight: 700;
} }
.opening-copy h2 { .opening-copy h1 {
margin: 0; margin: 0;
color: #fff; color: #fff;
font-size: clamp(1.55rem, 3vw, 2.5rem); font-size: clamp(1.55rem, 3vw, 2.5rem);
@@ -123,7 +123,7 @@ export default function Home({ data_NEWS }) {
<section className='opening-announcement' aria-labelledby='opening-announcement-title'> <section className='opening-announcement' aria-labelledby='opening-announcement-title'>
<div className='opening-copy'> <div className='opening-copy'>
<span className='opening-label'>اطلاع‌رسانی افتتاح</span> <span className='opening-label'>اطلاع‌رسانی افتتاح</span>
<h2 id='opening-announcement-title'>در چند قدمی افتتاح بیمارستان مهر پرند</h2> <h1 id='opening-announcement-title'>در چند قدمی افتتاح بیمارستان مهر پرند</h1>
<p>اخبار و زمان دقیق افتتاح بیمارستان از طریق همین وب‌سایت اطلاع‌رسانی خواهد شد.</p> <p>اخبار و زمان دقیق افتتاح بیمارستان از طریق همین وب‌سایت اطلاع‌رسانی خواهد شد.</p>
</div> </div>
<div className='opening-actions'> <div className='opening-actions'>
@@ -136,9 +136,11 @@ export default function Home({ data_NEWS }) {
<div className='lg:mx-20 sm:mx-5'> <div className='lg:mx-20 sm:mx-5'>
<SimpleGrid spacing={15} columns={[1, 2, 2]}> <SimpleGrid spacing={15} columns={[1, 2, 2]}>
<Link href={'/page/contact-us'}> <Link href={'/page/contact-us'}>
<a>
<MDBCard className={'bg-neutral-100 fst-box hover:bg-neutral-200 cursor-pointer hover:shadow-2xl'}> <MDBCard className={'bg-neutral-100 fst-box hover:bg-neutral-200 cursor-pointer hover:shadow-2xl'}>
<MDBCardBody className={'flex justify-content-center'}>ارتباط با ما</MDBCardBody> <MDBCardBody className={'flex justify-content-center'}>ارتباط با ما</MDBCardBody>
</MDBCard> </MDBCard>
</a>
</Link> </Link>
{/* <Link href={'/page/sales'}> {/* <Link href={'/page/sales'}>
<MDBCard className={'bg-neutral-100 fst-box hover:bg-neutral-200 cursor-pointer hover:shadow-2xl'}> <MDBCard className={'bg-neutral-100 fst-box hover:bg-neutral-200 cursor-pointer hover:shadow-2xl'}>
@@ -146,9 +148,11 @@ export default function Home({ data_NEWS }) {
</MDBCard> </MDBCard>
</Link> */} </Link> */}
<Link href={'/questions'}> <Link href={'/questions'}>
<a>
<MDBCard className={'bg-neutral-100 fst-box hover:bg-neutral-200 cursor-pointer hover:shadow-2xl'}> <MDBCard className={'bg-neutral-100 fst-box hover:bg-neutral-200 cursor-pointer hover:shadow-2xl'}>
<MDBCardBody className={'flex justify-content-center'}>سوالات متداول</MDBCardBody> <MDBCardBody className={'flex justify-content-center'}>سوالات متداول</MDBCardBody>
</MDBCard> </MDBCard>
</a>
</Link> </Link>
{/* <Link href={'/stock-unit'}> {/* <Link href={'/stock-unit'}>
<MDBCard className={'bg-neutral-100 fst-box hover:bg-neutral-200 cursor-pointer hover:shadow-2xl'}> <MDBCard className={'bg-neutral-100 fst-box hover:bg-neutral-200 cursor-pointer hover:shadow-2xl'}>
@@ -169,15 +173,15 @@ export default function Home({ data_NEWS }) {
<Link <Link
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
className={'text-white'} className={'text-white'}
href={{ pathname: '/News', query: { list: 'list' } }} href='/News'
> >
<MDBBtn style={{ cursor: 'pointer' }} className='mx-2 RTL bg-dark shadow-1-strong' color=''> <a className='btn mx-2 RTL bg-dark shadow-1-strong text-white'>
<MDBIcon className='ms-1 ' icon='plus' size='sm' /> <MDBIcon className='ms-1 ' icon='plus' size='sm' />
<span style={{ cursor: 'pointer !important' }} className={'text-white '}> <span style={{ cursor: 'pointer !important' }} className={'text-white '}>
{' '} {' '}
بیشتر بیشتر
</span> </span>
</MDBBtn> </a>
</Link> </Link>
</div> </div>
</News> </News>
+2 -2
View File
@@ -7,11 +7,11 @@ const MedicalEquipment = () => {
return ( return (
<AppContainer title={'واحد تجهیزات پزشکی'}> <AppContainer title={'واحد تجهیزات پزشکی'}>
<div className='parallax-container'> <div className='parallax-container'>
<h2 className={'center-align text-white drop-shadow service-text dana font-weight-bold'}>واحد تجهیزات پزشکی</h2> <h1 className={'center-align text-white drop-shadow service-text dana font-weight-bold'}>واحد تجهیزات پزشکی</h1>
<Image <Image
src={MedicalStock} src={MedicalStock}
className={'responsive-img shadow-5'} className={'responsive-img shadow-5'}
alt='Picture of the author' alt='تجهیزات پزشکی بیمارستان مهر پرند'
width={4090} width={4090}
height={2000} height={2000}
/> />
+2 -2
View File
@@ -72,10 +72,10 @@ const Directors = () => {
return ( return (
<AppContainer title={' تیم پروژه بیمارستان مهر پرند'}> <AppContainer title={' تیم پروژه بیمارستان مهر پرند'}>
<div className='parallax-container'> <div className='parallax-container'>
<h2 className={'center-align text-white header_back service-text dana font-weight-bold'}> <h1 className={'center-align text-white header_back service-text dana font-weight-bold'}>
{' '} {' '}
تیم پروژه بیمارستان{' '} تیم پروژه بیمارستان{' '}
</h2> </h1>
<Image <Image
loader={Parallax} loader={Parallax}
src='Parallax.png' src='Parallax.png'
+4 -4
View File
@@ -19,14 +19,14 @@ const ContactUs = () => {
return ( return (
<> <>
<AppContainer title={'درباره ما'}> <AppContainer title={'تماس با ما'}>
<div className="parallax-container"> <div className="parallax-container">
<h2 className={'center-align text-white header_back service-text dana font-weight-bold'}> تماس با ما </h2> <h1 className={'center-align text-white header_back service-text dana font-weight-bold'}> تماس با ما </h1>
<Image <Image
loader={Parallax} loader={Parallax}
src="Parallax.png" src="Parallax.png"
className={'responsive-img shadow-5'} className={'responsive-img shadow-5'}
alt="Picture of the author" alt="بیمارستان مهر پرند؛ آدرس و راه‌های ارتباطی"
width={2050} width={2050}
height={950} height={950}
/> />
@@ -102,4 +102,4 @@ const ContactUs = () => {
); );
}; };
export default ContactUs; export default ContactUs;
+2 -2
View File
@@ -20,9 +20,9 @@ const CooperationSales = () => {
{/* <img src ={cartmo} alt="" /> */} {/* <img src ={cartmo} alt="" /> */}
</div> </div>
<h2 className="text-white text-lg font-bold lg:text-[28px] font-extrabold text-center pt-8 xl:pt-16"> <h1 className="text-white text-lg font-bold lg:text-[28px] font-extrabold text-center pt-8 xl:pt-16">
همکاری در فروش سهام همکاری در فروش سهام
</h2> </h1>
<p className="text-white w-[85%] md:w-[60%] text-sm lg:text-[22px] font-medium leading-[1.8rem] lg:w-[42%] xl:w-[40%] mx-auto text-center lg:leading-8 xl:leading-10 py-2 lg:py-4"> <p className="text-white w-[85%] md:w-[60%] text-sm lg:text-[22px] font-medium leading-[1.8rem] lg:w-[42%] xl:w-[40%] mx-auto text-center lg:leading-8 xl:leading-10 py-2 lg:py-4">
با ثبت نام در سامانه خرید سهام، کد معرف شما ایجاد و قابل اشتراک با ثبت نام در سامانه خرید سهام، کد معرف شما ایجاد و قابل اشتراک
گذاری می شود. کد معرف یکتای خود را با دوستان خود به اشتراک بگذارید گذاری می شود. کد معرف یکتای خود را با دوستان خود به اشتراک بگذارید
+2 -2
View File
@@ -18,7 +18,7 @@ const Docs = () => {
<AppContainer title={' اساسنامه شرکت'}> <AppContainer title={' اساسنامه شرکت'}>
<div className="parallax-container"> <div className="parallax-container">
<div className={'d-flex'}> <div className={'d-flex'}>
<h2 className={'center-align text-white header_back service-text dana font-weight-bold'}> اسناد رسمی شرکت پزشکی مهر ایرانیان پرند </h2> <h1 className={'center-align text-white header_back service-text dana font-weight-bold'}> اسناد رسمی شرکت پزشکی مهر ایرانیان پرند </h1>
</div> </div>
<Image <Image
loader={Parallax} loader={Parallax}
@@ -47,4 +47,4 @@ const Docs = () => {
); );
}; };
export default Docs; export default Docs;
+11 -7
View File
@@ -19,6 +19,7 @@ import {
} from 'mdb-react-ui-kit' } from 'mdb-react-ui-kit'
import Image from 'next/image' import Image from 'next/image'
import { AppFile, AppUrl } from '../../../lib/api' import { AppFile, AppUrl } from '../../../lib/api'
import { newsSeo } from '../../../lib/seo'
import { Tag, TagLabel, HStack } from '@chakra-ui/react' import { Tag, TagLabel, HStack } from '@chakra-ui/react'
const moment = require('moment-jalaali') const moment = require('moment-jalaali')
@@ -40,7 +41,7 @@ const News = ({ post }) => {
loader={Parallax} loader={Parallax}
src='Parallax.png' src='Parallax.png'
className={'responsive-img shadow-5'} className={'responsive-img shadow-5'}
alt='Picture of the author' alt={post.title}
width={2050} width={2050}
height={1050} height={1050}
priority priority
@@ -175,11 +176,14 @@ const News = ({ post }) => {
export default News export default News
export const getServerSideProps = async context => { export const getServerSideProps = async context => {
const { query } = context const { slug } = context.params
const posData = await axios.get(AppUrl(`news/${query.slug}`)) try {
const post = await posData.data const { data: post } = await axios.get(AppUrl(`news/${encodeURIComponent(slug)}`), { timeout: 10000 })
if (!post || !post.title || typeof post.text !== 'string') return { notFound: true }
return { return { props: { post, seo: newsSeo(post, slug) } }
props: { post, fallback: false } // will be passed to the page component as props } catch (error) {
if (error.response && error.response.status === 404) return { notFound: true }
// Temporary upstream failures must stay 5xx rather than removing real articles from search.
throw error
} }
} }
+1 -1
View File
@@ -44,7 +44,7 @@ const Sales = () => {
return ( return (
<div id='callstock' className=' text-center py-8 m '> <div id='callstock' className=' text-center py-8 m '>
<h1 className='text-[#3c3c3c] text-[28px] font-bold mt-5 '>ارتباط با ما </h1> <h2 className='text-[#3c3c3c] text-[28px] font-bold mt-5 '>ارتباط با ما </h2>
<span className='text-[#656565] text-base p-2 md:text-[22px] md:p-0 font-light'> <span className='text-[#656565] text-base p-2 md:text-[22px] md:p-0 font-light'>
برای ارتباط با ما فرم زیر را تکمیل کنید برای ارتباط با ما فرم زیر را تکمیل کنید
</span> </span>
+5 -5
View File
@@ -31,9 +31,9 @@ const Recruitment = dataoccupational => {
<div className='parallax-container'> <div className='parallax-container'>
<div className={'flex flex-column'}> <div className={'flex flex-column'}>
<div className={'mb-5'}> <div className={'mb-5'}>
<h2 className={'center-align bk-med text-white header_back service-text dana font-weight-bold'}> <h1 className={'center-align bk-med text-white header_back service-text dana font-weight-bold'}>
فرصت های شغلی فرصت های شغلی
</h2> </h1>
</div> </div>
<br /> <br />
<br /> <br />
@@ -82,11 +82,11 @@ const Recruitment = dataoccupational => {
<FiUsers size={35} /> <FiUsers size={35} />
</div> </div>
<MDBCardTitle className={'bk-med mt-2 right'}>{item.title} </MDBCardTitle> <MDBCardTitle className={'bk-med mt-2 right'}>{item.title} </MDBCardTitle>
<Link href={`recruitment/item/${item.id}`}> <Link href={`/recruitment/item/${item.id}`}>
<MDBBtn rounded size={'sm'} className={'mt-2 flex align-items-center bk-light bg-primary-teal'}> <a className={'btn btn-rounded btn-sm mt-2 flex align-items-center bk-light bg-primary-teal text-white'}>
مشاهده مشاهده
<FiArrowLeft className={'mr-2'} /> <FiArrowLeft className={'mr-2'} />
</MDBBtn> </a>
</Link> </Link>
</MDBCardBody> </MDBCardBody>
</MDBCard> </MDBCard>
+7 -5
View File
@@ -19,7 +19,6 @@ import axios from 'axios'
import { AppProduction } from '../../../lib/api' import { AppProduction } from '../../../lib/api'
const Recruitment = jobs => { const Recruitment = jobs => {
console.log(jobs)
const Parallax = ({ quality }) => { const Parallax = ({ quality }) => {
return `../../resource/parallax/33012321.jpg` return `../../resource/parallax/33012321.jpg`
} }
@@ -32,10 +31,10 @@ const Recruitment = jobs => {
<div className='parallax-container'> <div className='parallax-container'>
<div className={'flex flex-column'}> <div className={'flex flex-column'}>
<div className={'mb-5'}> <div className={'mb-5'}>
<h2 className={'center-align bk-med text-white header_back service-text dana font-weight-bold'}> <h1 className={'center-align bk-med text-white header_back service-text dana font-weight-bold'}>
فرصت های شغلی بیمارستان مهر پرند فرصت‌های شغلی {jobs.jobs.name.title} بیمارستان مهر پرند
</h2> </h1>
</div> </div>
<br /> <br />
<br /> <br />
@@ -127,7 +126,10 @@ export async function getServerSideProps(ctx) {
const jobs = await res const jobs = await res
// Pass data to the page via props // Pass data to the page via props
return { props: { jobs } } return { props: { jobs, seo: {
title: `استخدام ${jobs.name.title}`,
description: `فرصت‌های شغلی گروه ${jobs.name.title} در بیمارستان مهر پرند؛ مشاهده عنوان‌های شغلی و ثبت درخواست استخدام.`
} } }
} }
export default Recruitment export default Recruitment
+1
View File
@@ -39,6 +39,7 @@ const StockUnit = () => {
/> />
</div> </div>
<div className={'mb-5 mt-5 mx-5'}> <div className={'mb-5 mt-5 mx-5'}>
<h1 className='text-center'>واحد امور سهام بیمارستان مهر پرند</h1>
<div className='my-4'> <div className='my-4'>
<SimpleGrid spacing={15} columns={[1, 2]}> <SimpleGrid spacing={15} columns={[1, 2]}>
<Link href={'/stock-unit/stock-check'}> <Link href={'/stock-unit/stock-check'}>
+3 -5
View File
@@ -1,7 +1,5 @@
User-agent: * User-agent: *
Disallow: /includes/ Allow: /
Disallow: /api/
User-agent: Google Sitemap: https://miphospital.ir/sitemap.xml
Allow: /includes/
Sitemap: http://miphospital.ir/api/my-sitemap
+7
View File
@@ -1,29 +1,36 @@
@font-face { @font-face {
font-display: swap;
font-family: Vazir-import !important; font-family: Vazir-import !important;
src: url("Vazir-Regular.ttf"); src: url("Vazir-Regular.ttf");
} }
@font-face { @font-face {
font-display: swap;
font-family: dana-fanum; font-family: dana-fanum;
src: url("Dana-FaNum-Regular.ttf"); src: url("Dana-FaNum-Regular.ttf");
} }
@font-face { @font-face {
font-display: swap;
font-family: yekanBakh; font-family: yekanBakh;
src: url("YekanLight.ttf"); src: url("YekanLight.ttf");
} }
@font-face { @font-face {
font-display: swap;
font-family: yekanBakhMed; font-family: yekanBakhMed;
src: url("YekanMedium.ttf"); src: url("YekanMedium.ttf");
} }
@font-face { @font-face {
font-display: swap;
font-family: yekanBakhLight; font-family: yekanBakhLight;
src: url("YekanLight.ttf"); src: url("YekanLight.ttf");
} }
@font-face { @font-face {
font-display: swap;
font-family: yekanBakhHeavy; font-family: yekanBakhHeavy;
src: url("YekanBakhHeavy.ttf"); src: url("YekanBakhHeavy.ttf");
} }
@font-face { @font-face {
font-display: swap;
font-family: yekanBakhBold; font-family: yekanBakhBold;
src: url("YekanBakhBold.ttf"); src: url("YekanBakhBold.ttf");
} }
+36
View File
@@ -0,0 +1,36 @@
import Head from 'next/head'
import { useRouter } from 'next/router'
import { getSeo, SITE_NAME, hospitalSchema, serializeJsonLd } from '../../lib/seo'
export default function Seo({ metadata = {} }) {
const router = useRouter()
const seo = getSeo(router.asPath, metadata)
const title = seo.title.includes(SITE_NAME) ? seo.title : `${seo.title} | ${SITE_NAME}`
return (
<Head>
<title>{title}</title>
<meta name='description' content={seo.description} key='description' />
<meta name='robots' content={seo.noindex ? 'noindex, follow' : 'index, follow, max-image-preview:large'} key='robots' />
<link rel='canonical' href={seo.canonical} key='canonical' />
<meta property='og:locale' content='fa_IR' />
<meta property='og:site_name' content={SITE_NAME} />
<meta property='og:type' content={seo.type} />
<meta property='og:title' content={title} />
<meta property='og:description' content={seo.description} />
<meta property='og:url' content={seo.canonical} />
<meta property='og:image' content={seo.image} />
<meta property='og:image:alt' content={seo.title} />
<meta name='twitter:card' content='summary_large_image' />
<meta name='twitter:title' content={title} />
<meta name='twitter:description' content={seo.description} />
<meta name='twitter:image' content={seo.image} />
<link rel='icon' href='/favicon.ico' />
{!seo.noindex && (
<script type='application/ld+json' key='structured-data' dangerouslySetInnerHTML={{
__html: serializeJsonLd(seo.structuredData ? [hospitalSchema, seo.structuredData] : hospitalSchema)
}} />
)}
</Head>
)
}
+47
View File
@@ -0,0 +1,47 @@
const { test } = require('node:test')
const assert = require('node:assert/strict')
const { canonicalUrl, getSeo, newsSeo, serializeJsonLd, sitemapLinks } = require('../lib/seo')
test('canonical URLs remove tracking, fragments and trailing slashes without trusting an external host', () => {
assert.equal(canonicalUrl('/page/about/?utm_source=test#intro'), 'https://miphospital.ir/page/about')
assert.equal(canonicalUrl('/'), 'https://miphospital.ir/')
assert.equal(canonicalUrl('//evil.example/page'), 'https://miphospital.ir/')
assert.equal(canonicalUrl('https://evil.example/page'), 'https://miphospital.ir/')
})
test('public pages have distinct descriptions and internal pages remain out of search', () => {
assert.notEqual(getSeo('/page/about').description, getSeo('/page/contact-us').description)
assert.equal(getSeo('/page/contact-us').noindex, false)
for (const route of ['/components/slider', '/appContainer', '/recruitment/inquiry', '/recruitment/item/request/23', '/404']) {
assert.equal(getSeo(route).noindex, true, route)
}
assert.equal(getSeo('/components/service').noindex, false)
assert.equal(getSeo('/page/news/36').noindex, false)
assert.match(getSeo('/recruitment/inquiry').title, /استعلام/)
})
test('article metadata derives a plain text summary and absolute cover URL from the actual article', () => {
const seo = newsSeo({ title: 'خبر افتتاح', text: '<p>خبر <strong>بیمارستان</strong></p><script>bad()</script>', cover: 'news.jpg' }, '36')
assert.equal(seo.title, 'خبر افتتاح')
assert.equal(seo.description, 'خبر بیمارستان')
assert.equal(seo.image, 'https://mehrara-api.iran.liara.run/panel-public/uploads/news.jpg')
assert.equal(seo.structuredData['@type'], 'NewsArticle')
assert.equal(seo.structuredData.mainEntityOfPage, 'https://miphospital.ir/page/news/36')
})
test('JSON-LD cannot terminate its HTML script element', () => {
const value = { name: '</script><script>alert(1)</script>' }
const serialized = serializeJsonLd(value)
assert.equal(serialized.includes('<'), false)
assert.deepEqual(JSON.parse(serialized), value)
})
test('sitemap contains real public routes and unique valid article IDs only', () => {
const links = sitemapLinks([{ id: 36 }, { id: 36 }, { id: '../bad' }, { id: null }, { id: 82 }])
assert.ok(links.includes('/page/contact-us'))
assert.ok(links.includes('/page/news/36'))
assert.ok(links.includes('/page/news/82'))
assert.equal(links.filter(url => url === '/page/news/36').length, 1)
assert.equal(links.includes('/contact-us'), false)
assert.equal(links.some(url => url.includes('request') || url.includes('../')), false)
})