Seo updated
This commit is contained in:
parent
12b2e77449
commit
92e2b8e210
@ -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
80
public/.htaccess
Normal 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>
|
||||
@ -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>
|
||||
@ -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>
|
||||
|
||||
@ -69,7 +69,7 @@ export default function About() {
|
||||
<div
|
||||
ref={containerRef}
|
||||
className="
|
||||
h-[600px]
|
||||
h-[400px]
|
||||
w-full
|
||||
mx-auto
|
||||
rounded-2xl
|
||||
|
||||
@ -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: '© <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='© <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} />
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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"
|
||||
/>
|
||||
|
||||
@ -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}
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
@ -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>
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user