Seo updated

This commit is contained in:
akash 2025-12-09 23:29:52 +05:30
parent 12b2e77449
commit 92e2b8e210
13 changed files with 171 additions and 37 deletions

View File

@ -6,7 +6,8 @@
"dev": "next dev",
"build": "next build && node script/copy-server-config.cjs",
"start": "next start",
"lint": "eslint"
"lint": "eslint",
"sitemap": "node script/generate-sitemap.cjs"
},
"dependencies": {
"@types/leaflet": "^1.9.21",

80
public/.htaccess Normal file
View File

@ -0,0 +1,80 @@
# ----------------------------------------------------------------------
# | Best Match for Next.js Static Export with Trailing Slash |
# ----------------------------------------------------------------------
<IfModule mod_rewrite.c>
RewriteEngine On
RewriteBase /
# --------------------------------------------------------------------
# | 1. Force HTTPS |
# --------------------------------------------------------------------
RewriteCond %{HTTPS} off
RewriteRule ^ https://%{HTTP_HOST}%{REQUEST_URI} [L,R=301]
# --------------------------------------------------------------------
# | 2. Handle Trailing Slashes (Consistent with next.config.ts) |
# --------------------------------------------------------------------
# If request does NOT end in slash and creates a valid directory, redirect
# This matches Next.js "trailingSlash: true" behavior
RewriteCond %{REQUEST_FILENAME} !-f
RewriteCond %{REQUEST_URI} !index.html
RewriteCond %{REQUEST_URI} !/$
RewriteRule ^(.*)$ $1/ [L,R=301]
# --------------------------------------------------------------------
# | 3. Route to index.html within directories |
# --------------------------------------------------------------------
# If the request points to a directory that has an index.html, serve it
RewriteCond %{REQUEST_FILENAME} -d
RewriteCond %{REQUEST_FILENAME}/index.html -f
RewriteRule ^(.*)/$ $1/index.html [L]
# --------------------------------------------------------------------
# | 4. Fallback for SPA-like refreshing (The "White Screen" Fix) |
# --------------------------------------------------------------------
# If the file exists, serve it (images, css, js)
RewriteCond %{REQUEST_FILENAME} -f
RewriteRule ^ - [L]
# If not found, trying appending .html (for flat files)
RewriteCond %{REQUEST_FILENAME}.html -f
RewriteRule ^(.*)$ $1.html [L]
# If still not found, show Custom 404 Page
# DO NOT fallback to root index.html for non-existent pages (causes soft 404s)
ErrorDocument 404 /404.html
</IfModule>
# ----------------------------------------------------------------------
# | Performance: Compression (Gzip) |
# ----------------------------------------------------------------------
<IfModule mod_deflate.c>
AddOutputFilterByType DEFLATE text/html text/plain text/xml text/css text/javascript application/javascript application/json application/xml image/svg+xml
</IfModule>
# ----------------------------------------------------------------------
# | Performance: Browser Caching |
# ----------------------------------------------------------------------
<IfModule mod_expires.c>
ExpiresActive On
# Images: 1 Month
ExpiresByType image/jpg "access plus 1 month"
ExpiresByType image/jpeg "access plus 1 month"
ExpiresByType image/gif "access plus 1 month"
ExpiresByType image/png "access plus 1 month"
ExpiresByType image/webp "access plus 1 month"
ExpiresByType image/svg+xml "access plus 1 month"
# CSS/JS: 1 Year (Immutable if hashed, safe for Next.js)
ExpiresByType text/css "access plus 1 year"
ExpiresByType application/javascript "access plus 1 year"
# Fonts: 1 Year
ExpiresByType font/woff2 "access plus 1 year"
# HTML: Short cache to ensure updates are seen
ExpiresByType text/html "access plus 5 minutes"
</IfModule>

View File

@ -1 +1 @@
<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>https://skyandsoil.metatronnest.com/</loc></url><url><loc>https://skyandsoil.metatronnest.com/about/</loc></url><url><loc>https://skyandsoil.metatronnest.com/projects/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/</loc></url><url><loc>https://skyandsoil.metatronnest.com/lifestyle/</loc></url><url><loc>https://skyandsoil.metatronnest.com/contact/</loc></url><url><loc>https://skyandsoil.metatronnest.com/compare/</loc></url><url><loc>https://skyandsoil.metatronnest.com/privacy-policy/</loc></url><url><loc>https://skyandsoil.metatronnest.com/terms-of-service/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/barca-at-godrej-msr-city/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/godrej-woods/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/godrej-hoskote/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/godrej-lakeside-orchard/</loc></url><url><loc>https://skyandsoil.metatronnest.com/residential-real-estate/godrej-tiara/</loc></url></urlset>
<?xml version="1.0" encoding="UTF-8"?><urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1"><url><loc>http://localhost:3000/</loc></url><url><loc>http://localhost:3000/about/</loc></url><url><loc>http://localhost:3000/projects/</loc></url><url><loc>http://localhost:3000/residential-real-estate/</loc></url><url><loc>http://localhost:3000/lifestyle/</loc></url><url><loc>http://localhost:3000/contact/</loc></url><url><loc>http://localhost:3000/compare/</loc></url><url><loc>http://localhost:3000/privacy-policy/</loc></url><url><loc>http://localhost:3000/terms-of-service/</loc></url><url><loc>http://localhost:3000/residential-real-estate/barca-at-godrej-msr-city/</loc></url><url><loc>http://localhost:3000/residential-real-estate/godrej-woods/</loc></url><url><loc>http://localhost:3000/residential-real-estate/godrej-hoskote/</loc></url><url><loc>http://localhost:3000/residential-real-estate/godrej-lakeside-orchard/</loc></url><url><loc>http://localhost:3000/residential-real-estate/godrej-tiara/</loc></url></urlset>

View File

@ -17,10 +17,6 @@
<mimeMap fileExtension=".woff" mimeType="font/woff" />
<remove fileExtension=".woff2" />
<mimeMap fileExtension=".woff2" mimeType="font/woff2" />
<remove fileExtension=".js" />
<mimeMap fileExtension=".js" mimeType="application/javascript" />
<remove fileExtension=".css" />
<mimeMap fileExtension=".css" mimeType="text/css" />
</staticContent>
<httpErrors errorMode="Custom">
<remove statusCode="404" />
@ -28,13 +24,41 @@
</httpErrors>
<rewrite>
<rules>
<rule name="Handling Trailing Slashes" stopProcessing="false">
<match url="(.*)" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
</conditions>
<action type="Rewrite" url="{R:1}" />
<!-- 1. Force HTTPS -->
<rule name="Force HTTPS" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{HTTPS}" pattern="off" ignoreCase="true" />
</conditions>
<action type="Redirect" url="https://{HTTP_HOST}/{R:1}" redirectType="Permanent" />
</rule>
<!-- 2. Trailing Slash Enforcement (Matches next.config.ts) -->
<rule name="Add Trailing Slash" stopProcessing="true">
<match url="(.*[^/])$" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.html$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.xml$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.txt$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.json$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.png$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.jpg$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.jpeg$" negate="true" />
<add input="{REQUEST_FILENAME}" pattern="(.*?)\.svg$" negate="true" />
</conditions>
<action type="Redirect" url="{R:1}/" redirectType="Permanent" />
</rule>
<!-- 3. Handle HTML Extension Fallback -->
<rule name="HtmlExtension" stopProcessing="true">
<match url="(.*)" />
<conditions>
<add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
<add input="{REQUEST_FILENAME}.html" matchType="IsFile" />
</conditions>
<action type="Rewrite" url="{R:1}.html" />
</rule>
</rules>
</rewrite>

View File

@ -69,7 +69,7 @@ export default function About() {
<div
ref={containerRef}
className="
h-[600px]
h-[400px]
w-full
mx-auto
rounded-2xl

View File

@ -127,6 +127,34 @@ function ZoomHandler({ zoomIn, zoomOut }: { zoomIn: () => void, zoomOut: () => v
);
}
function CustomTileLayer() {
const map = useMap();
useEffect(() => {
const layer = L.tileLayer("https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
attribution: '&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
});
// Patch createTile to add unique alt text
const originalCreateTile = (layer as any).createTile;
(layer as any).createTile = function (coords: any, done: any) {
// @ts-ignore - createTile expects a different signature in some type definitions but this is standard Leaflet
const tile = originalCreateTile.call(this, coords, done);
if (tile instanceof HTMLImageElement) {
tile.alt = `Map tile location ${coords.x}, ${coords.y} zoom level ${coords.z}`;
}
return tile;
};
layer.addTo(map);
return () => {
layer.removeFrom(map);
};
}, [map]);
return null;
}
export default function ConnectivityMap() {
const [activeTab, setActiveTab] = useState("Commute");
@ -213,10 +241,7 @@ export default function ConnectivityMap() {
zoomControl={false}
ref={setMapRef}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>
<CustomTileLayer />
<MapController center={PROPERTY_LOCATION} />
<ZoomHandler zoomIn={handleZoomIn} zoomOut={handleZoomOut} />

View File

@ -65,7 +65,7 @@ export default function Header() {
<div className="relative w-60 h-20 transition-transform group-hover:scale-105 -left-[83px]">
<Image
src={(isScrolled && isLight) ? "/assets/images/blue-logo.png" : "/assets/images/white-logo.png"}
alt="Sky and Soil Logo"
alt="Sky and Soil Header Logo"
fill
className="object-contain"
priority

View File

@ -21,7 +21,7 @@ export default function InnerBanner({
<div className="absolute inset-0">
<Image
src={backgroundImage}
alt={`${title} - Sky and Soil Real Estate`}
alt={`Hero image of ${title} - Sky and Soil Real Estate`}
fill
className="object-cover"
priority

View File

@ -118,7 +118,7 @@ export default function Properties({ layout = "slider" }: PropertiesProps) {
<div className={activeTab === "All" ? "h-64 w-full relative overflow-hidden" : "h-80 w-full relative overflow-hidden"}>
<div className={`absolute inset-0 ${property.image.startsWith('/') ? '' : property.image}`} />
{property.image.startsWith('/') && (
<img src={property.image} alt={property.title} className="w-full h-full object-cover transition-transform duration-500 group-hover/card:scale-110" />
<img src={property.image} alt={`Main view of ${property.title}`} className="w-full h-full object-cover transition-transform duration-500 group-hover/card:scale-110" />
)}
<div className="absolute top-4 left-4 bg-white/90 dark:bg-black/80 backdrop-blur-sm px-3 py-1 rounded-full text-xs font-semibold text-foreground uppercase tracking-wider">
@ -173,7 +173,7 @@ export default function Properties({ layout = "slider" }: PropertiesProps) {
<div className={`h-64 w-full relative overflow-hidden`}>
<div className={`absolute inset-0 ${property.image.startsWith('/') ? '' : property.image}`} />
{property.image.startsWith('/') && (
<img src={property.image} alt={property.title} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
<img src={property.image} alt={`Main view of ${property.title}`} className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
)}
<div className="absolute top-4 left-4 bg-white/90 dark:bg-black/80 backdrop-blur-sm px-3 py-1 rounded-full text-xs font-semibold text-foreground uppercase tracking-wider">

View File

@ -76,7 +76,7 @@ export default function PropertyCard({ property }: PropertyCardProps) {
<div className="relative h-64 w-full overflow-hidden">
<Image
src={property.image}
alt={property.title}
alt={`Property view of ${property.title}`}
fill
className="object-cover group-hover:scale-110 transition-transform duration-500"
/>

View File

@ -558,7 +558,7 @@ export default function PropertyDetailClient({ property }: { property: Property
<div>
<div className="flex items-center gap-3 mb-3">
<div className="flex flex-col items-start gap-2 md:flex-row md:items-center md:gap-3 mb-3">
<h1 className="text-2xl md:text-3xl font-bold text-foreground">{property.title}</h1>
@ -1737,7 +1737,7 @@ export default function PropertyDetailClient({ property }: { property: Property
src={property.locality?.mapImage || "/assets/images/map-placeholder.webp"}
alt={`${property.locality?.name} Locality`}
alt={`Map of ${property.locality?.name} Locality`}
className="w-full h-full object-cover transition-transform duration-700 group-hover:scale-105"
@ -1853,7 +1853,7 @@ export default function PropertyDetailClient({ property }: { property: Property
<div className="rounded-xl overflow-hidden mb-4 h-48">
<img src="/assets/images/map-placeholder.webp" alt="Prestige Raintree Park" className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
<img src="/assets/images/map-placeholder.webp" alt="Nearby property Prestige Raintree Park" className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
</div>
@ -1879,7 +1879,7 @@ export default function PropertyDetailClient({ property }: { property: Property
<div className="rounded-xl overflow-hidden mb-4 h-48">
<img src="/assets/images/image.png" alt="Adarsh Park Heights" className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
<img src="/assets/images/image.png" alt="Nearby property Adarsh Park Heights" className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
</div>
@ -1905,7 +1905,7 @@ export default function PropertyDetailClient({ property }: { property: Property
<div className="rounded-xl overflow-hidden mb-4 h-48">
<img src="/assets/images/map-placeholder.webp" alt="Tru Aquapolis" className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
<img src="/assets/images/map-placeholder.webp" alt="Nearby property Tru Aquapolis" className="w-full h-full object-cover transition-transform duration-500 group-hover:scale-110" />
</div>
@ -1951,9 +1951,9 @@ export default function PropertyDetailClient({ property }: { property: Property
{/* FAQ Section */}
<AnimateSection id="faq" className="bg-white dark:bg-gray-900 rounded-2xl p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32" direction="up">
<AnimateSection id="faq" className="bg-white dark:bg-gray-900 rounded-2xl p-4 md:p-8 shadow-sm border border-gray-200 dark:border-gray-800 scroll-mt-32" direction="up">
<h2 className="text-3xl font-bold text-foreground mb-2">Frequently Asked Questions</h2>
<h2 className="text-xl md:text-3xl font-bold text-foreground mb-2">Frequently Asked Questions</h2>
<p className="text-gray-500 dark:text-gray-400 mb-8">
@ -2009,7 +2009,7 @@ export default function PropertyDetailClient({ property }: { property: Property
<button
onClick={() => setExpandedFaq(expandedFaq === index ? null : index)}
className="w-full flex items-center justify-between p-6 text-left bg-white dark:bg-gray-800
className="w-full flex items-center justify-between p-3 md:p-6 text-left bg-white dark:bg-gray-800
hover:bg-gray-50 dark:hover:bg-gray-750 transition-colors group"
>
<span className="font-medium text-gray-900 dark:text-white pr-8 group-hover:text-blue-600">
@ -2034,7 +2034,7 @@ export default function PropertyDetailClient({ property }: { property: Property
{expandedFaq === index && (
<div className="p-6 pt-0 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 leading-relaxed border-t border-gray-100 dark:border-gray-700">
<div className="p-3 pt-0 md:p-6 md:pt-0 bg-white dark:bg-gray-800 text-gray-600 dark:text-gray-300 leading-relaxed border-t border-gray-100 dark:border-gray-700">
{item.answer}

View File

@ -124,7 +124,7 @@ export default function PropertyGallery({ images, title }: PropertyGalleryProps)
>
<Image src={img} alt={`View ${idx + 1}`} fill className="object-cover" />
<Image src={img} alt={`View detail ${idx + 1} of ${title}`} fill className="object-cover" />
</div>
@ -144,7 +144,7 @@ export default function PropertyGallery({ images, title }: PropertyGalleryProps)
src={displayImages[3]}
alt="View all photos"
alt={`View all photos of ${title}`}
fill
@ -294,7 +294,7 @@ export default function PropertyGallery({ images, title }: PropertyGalleryProps)
>
<Image src={img} alt={`Thumbnail ${idx + 1}`} fill className="object-cover" />
<Image src={img} alt={`Thumbnail ${idx + 1} of ${title}`} fill className="object-cover" />
</div>

View File

@ -131,9 +131,13 @@ export default function Testimonials() {
style={{ scrollbarWidth: 'none', msOverflowStyle: 'none' }}
>
{testimonials.map((item, index) => (
<div
<motion.div
key={index}
className="flex-shrink-0 snap-center w-full md:w-[calc(50%-12px)] lg:w-[calc(33.333%-16px)]"
initial={{ opacity: 0, y: 40 }}
whileInView={{ opacity: 1, y: 0 }}
viewport={{ once: true, margin: "-50px" }}
transition={{ duration: 0.6, delay: index * 0.15 }}
>
<div className="bg-white dark:bg-gray-900 p-6 rounded-2xl border border-gray-200 dark:border-gray-800 hover:shadow-xl transition-all duration-300 h-full flex flex-col">
{/* Header with Avatar and Info */}
@ -168,7 +172,7 @@ export default function Testimonials() {
</p>
</div>
</div>
</div>
</motion.div>
))}
</div>
</div>