|
|
// Static site generator for artnouveau.in — quiet-luxury editorial design.
|
|
|
// Usage: npm run build → regenerates dist/ from src/ + data.
|
|
|
const fs = require('fs');
|
|
|
const path = require('path');
|
|
|
|
|
|
const ROOT = path.join(__dirname, '..');
|
|
|
const SRC = path.join(ROOT, 'src');
|
|
|
const DIST = path.join(ROOT, 'dist');
|
|
|
|
|
|
const site = JSON.parse(fs.readFileSync(path.join(SRC, 'data', 'site.json'), 'utf8'));
|
|
|
const projects = JSON.parse(fs.readFileSync(path.join(SRC, 'data', 'projects.json'), 'utf8'));
|
|
|
const manifest = JSON.parse(fs.readFileSync(path.join(SRC, 'data', 'images.json'), 'utf8'));
|
|
|
const home = JSON.parse(fs.readFileSync(path.join(SRC, 'data', 'home.json'), 'utf8'));
|
|
|
const extra = fs.existsSync(path.join(SRC, 'data', 'images-extra.json'))
|
|
|
? JSON.parse(fs.readFileSync(path.join(SRC, 'data', 'images-extra.json'), 'utf8')) : {};
|
|
|
|
|
|
const esc = s => String(s == null ? '' : s)
|
|
|
.replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"');
|
|
|
|
|
|
/* ---------- image helpers ---------- */
|
|
|
function entryFor(slug, idx) {
|
|
|
const list = manifest[slug] || [];
|
|
|
const e = list[idx];
|
|
|
return e && e.variants ? e : null;
|
|
|
}
|
|
|
function validIndices(slug) {
|
|
|
const hidden = (projects.find(p => p.slug === slug) || {}).hiddenIndices || [];
|
|
|
return (manifest[slug] || []).map((e, i) => (e && e.variants ? i : -1))
|
|
|
.filter(i => i >= 0 && !hidden.includes(i));
|
|
|
}
|
|
|
function picture(slug, idx, o) {
|
|
|
const e = entryFor(slug, idx);
|
|
|
if (!e) return '';
|
|
|
o = o || {};
|
|
|
const webps = [], seen = new Set();
|
|
|
for (const v of e.variants) if (v.format === 'webp' && !seen.has(v.width)) { seen.add(v.width); webps.push(v); }
|
|
|
const jpg = e.variants.find(v => v.format === 'jpeg');
|
|
|
const srcset = webps.map(v => `/${v.file} ${v.width}w`).join(', ');
|
|
|
const attrs = [
|
|
|
`src="/${jpg.file}"`, `width="${jpg.width}"`, `height="${jpg.height}"`,
|
|
|
`alt="${esc(o.alt || '')}"`, `decoding="async"`,
|
|
|
o.eager ? 'fetchpriority="high"' : 'loading="lazy"',
|
|
|
o.dataAttrs || ''
|
|
|
].filter(Boolean).join(' ');
|
|
|
return `<picture><source type="image/webp" srcset="${srcset}" sizes="${o.sizes || '100vw'}"><img ${attrs}></picture>`;
|
|
|
}
|
|
|
function biggest(slug, idx) {
|
|
|
const e = entryFor(slug, idx);
|
|
|
if (!e) return '';
|
|
|
const w = e.variants.filter(v => v.format === 'webp').sort((a, b) => b.width - a.width)[0];
|
|
|
return '/' + w.file;
|
|
|
}
|
|
|
function extraPic(key, o) {
|
|
|
const e = extra[key];
|
|
|
if (!e || !e.variants) return '';
|
|
|
o = o || {};
|
|
|
const webps = [], seen = new Set();
|
|
|
for (const v of e.variants) if (v.format === 'webp' && !seen.has(v.width)) { seen.add(v.width); webps.push(v); }
|
|
|
const jpg = e.variants.find(v => v.format === 'jpeg') || webps[0];
|
|
|
const srcset = webps.map(v => `/${v.file} ${v.width}w`).join(', ');
|
|
|
return `<picture><source type="image/webp" srcset="${srcset}" sizes="${o.sizes || '100vw'}"><img src="/${jpg.file}" width="${jpg.width}" height="${jpg.height}" alt="${esc(o.alt || '')}" loading="lazy" decoding="async"${o.dataAttrs ? ' ' + o.dataAttrs : ''}></picture>`;
|
|
|
}
|
|
|
|
|
|
/* ---------- shared shell ---------- */
|
|
|
const NAV = [
|
|
|
['/projects/', 'projects'], ['/studio/', 'studio'], ['/process/', 'process'],
|
|
|
[site.tenderUrl, 'tender'], ['/contact/', 'contact'],
|
|
|
];
|
|
|
function navList(current) {
|
|
|
return NAV.map(([href, label]) => {
|
|
|
const ext = href.startsWith('http');
|
|
|
const cur = !ext && href === current ? ' aria-current="page"' : '';
|
|
|
const rel = ext ? ' rel="noopener"' : '';
|
|
|
return `<li><a href="${href}"${cur}${rel}>${label}</a></li>`;
|
|
|
}).join('\n ');
|
|
|
}
|
|
|
const FAVICON = `data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 32 32'%3E%3Crect width='32' height='32' fill='%23F4F1EA'/%3E%3Cpath d='M8 24 16 8l8 16' fill='none' stroke='%231C1A16' stroke-width='1.5'/%3E%3Ccircle cx='24' cy='24' r='2.5' fill='%237A4A32'/%3E%3C/svg%3E`;
|
|
|
|
|
|
function layout(o) {
|
|
|
// o: {path, title, desc, body, current, scripts:[], styles:[], jsonld, ogImage}
|
|
|
const url = site.domain + o.path;
|
|
|
const og = o.ogImage || (extra.og && '/' + extra.og.variants.find(v => v.format === 'jpeg').file) || '';
|
|
|
const vendor = ['/assets/vendor/gsap.min.js', '/assets/vendor/ScrollTrigger.min.js', '/assets/vendor/lenis.min.js']
|
|
|
.concat(o.scripts || []);
|
|
|
return `<!doctype html>
|
|
|
<html lang="en">
|
|
|
<head>
|
|
|
<meta charset="utf-8">
|
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
|
<title>${esc(o.title)}</title>
|
|
|
<meta name="description" content="${esc(o.desc)}">
|
|
|
<link rel="canonical" href="${url}">
|
|
|
<meta name="theme-color" content="#F4F1EA">
|
|
|
<meta property="og:type" content="website">
|
|
|
<meta property="og:site_name" content="${esc(site.name)}">
|
|
|
<meta property="og:title" content="${esc(o.title)}">
|
|
|
<meta property="og:description" content="${esc(o.desc)}">
|
|
|
<meta property="og:url" content="${url}">
|
|
|
${og ? `<meta property="og:image" content="${site.domain}${og}">` : ''}
|
|
|
<meta name="twitter:card" content="summary_large_image">
|
|
|
<link rel="icon" href="${FAVICON}">
|
|
|
<link rel="preload" href="/assets/fonts/cormorant-normal-300-latin.woff2" as="font" type="font/woff2" crossorigin>
|
|
|
<link rel="preload" href="/assets/fonts/archivo-normal-300-latin.woff2" as="font" type="font/woff2" crossorigin>
|
|
|
<link rel="stylesheet" href="/assets/css/site.css">
|
|
|
${(o.styles || []).map(h => `<link rel="stylesheet" href="${h}">`).join('\n')}
|
|
|
${o.jsonld ? `<script type="application/ld+json">${o.jsonld}</script>` : ''}
|
|
|
</head>
|
|
|
<body>
|
|
|
<a class="skip" href="#main">Skip to content</a>
|
|
|
<div class="veil-page" aria-hidden="true"></div>
|
|
|
|
|
|
<header class="site" id="hdr">
|
|
|
<nav class="nav" aria-label="Main">
|
|
|
<a class="wordmark" href="/">Art<span>.</span>n Architects</a>
|
|
|
<ul>
|
|
|
${navList(o.current)}
|
|
|
</ul>
|
|
|
<button class="menu-label" aria-expanded="false" aria-controls="site-menu">menu</button>
|
|
|
</nav>
|
|
|
</header>
|
|
|
|
|
|
<div class="menu-overlay" id="site-menu" hidden>
|
|
|
<button class="menu-close" aria-label="Close menu">close</button>
|
|
|
<nav aria-label="Mobile">
|
|
|
<a href="/">home</a>
|
|
|
${NAV.map(([href, label]) => `<a href="${href}"${href.startsWith('http') ? ' rel="noopener"' : ''}>${label}</a>`).join('\n ')}
|
|
|
</nav>
|
|
|
</div>
|
|
|
|
|
|
${o.body}
|
|
|
|
|
|
<footer class="site">
|
|
|
<div class="wrap">
|
|
|
<div class="foot-top">
|
|
|
<p class="wordmark">Art<span>.</span>n Architects</p>
|
|
|
<p class="eyebrow">Architecture · Interiors · Project Management</p>
|
|
|
</div>
|
|
|
<div class="foot-cols">
|
|
|
<div>
|
|
|
<span class="eyebrow">Changanacherry</span>
|
|
|
<p>${site.offices[0].address.map(esc).join('<br>')}<br><a href="${site.offices[0].phoneHref}">${esc(site.offices[0].phone)}</a></p>
|
|
|
</div>
|
|
|
<div>
|
|
|
<span class="eyebrow">Kochi</span>
|
|
|
<p>${site.offices[1].address.map(esc).join('<br>')}<br><a href="${site.offices[1].phoneHref}">${esc(site.offices[1].phone)}</a></p>
|
|
|
</div>
|
|
|
<div>
|
|
|
<span class="eyebrow">Write</span>
|
|
|
<p><a href="mailto:${site.email}">${esc(site.email)}</a><br>
|
|
|
<a href="${site.whatsapp}" rel="noopener">WhatsApp</a></p>
|
|
|
</div>
|
|
|
<div>
|
|
|
<span class="eyebrow">Elsewhere</span>
|
|
|
<p><a href="${site.instagram}" rel="noopener">Instagram</a><br>
|
|
|
<a href="${site.facebook}" rel="noopener">Facebook</a><br>
|
|
|
<a href="${site.tenderUrl}" rel="noopener">Tender portal</a></p>
|
|
|
</div>
|
|
|
</div>
|
|
|
<div class="foot-base">
|
|
|
<p>© ${esc(site.name)}, est. ${site.established}. All rights reserved.</p>
|
|
|
<p>Changanacherry · Kochi — Kerala, India</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</footer>
|
|
|
|
|
|
${vendor.map(s => `<script src="${s}" defer></script>`).join('\n')}
|
|
|
<script src="/assets/js/site.js" defer></script>
|
|
|
</body>
|
|
|
</html>`;
|
|
|
}
|
|
|
|
|
|
const ctaBand = (h) => `
|
|
|
<section class="cta">
|
|
|
<div class="wrap inner">
|
|
|
<h2 class="reveal">${h || 'A site, a brief, or just a first idea — <em>bring it to the table.</em>'}</h2>
|
|
|
<div class="actions reveal">
|
|
|
<a class="btn" href="mailto:${site.email}">write to us</a>
|
|
|
<a class="btn" href="${site.whatsapp}" rel="noopener">whatsapp</a>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>`;
|
|
|
|
|
|
const bySlug = Object.fromEntries(projects.map(p => [p.slug, p]));
|
|
|
const heroIdxOf = p => {
|
|
|
const valid = validIndices(p.slug);
|
|
|
if (!valid.length) return -1;
|
|
|
return valid.includes(p.heroIndex) ? p.heroIndex : valid[0];
|
|
|
};
|
|
|
const projUrl = p => `/projects/${p.slug}/`;
|
|
|
|
|
|
/* editorial flow tile; slot-position → sizes attr for the 6-slot cycle */
|
|
|
const SLOT_SIZES = ['66vw', '33vw', '42vw', '42vw', '66vw', '42vw'];
|
|
|
const sizesFor = i => `(max-width:860px) 100vw, ${SLOT_SIZES[i % 6]}`;
|
|
|
function piece(p, i, heroOverride) {
|
|
|
const hi = heroOverride != null ? heroOverride : heroIdxOf(p);
|
|
|
if (hi < 0) return '';
|
|
|
return `
|
|
|
<a class="piece reveal" href="${projUrl(p)}">
|
|
|
<figure>
|
|
|
<div class="ph">${picture(p.slug, hi, { sizes: sizesFor(i), alt: p.heroAlt || p.title })}</div>
|
|
|
<figcaption><span class="name">${esc(p.tileLabel || p.title)}</span>${p.place ? `<span class="where">${esc(p.place)}</span>` : `<span class="where">${esc(p.category || '')}</span>`}</figcaption>
|
|
|
</figure>
|
|
|
</a>`;
|
|
|
}
|
|
|
|
|
|
/* ---------- HOME ---------- */
|
|
|
function homePage() {
|
|
|
const heroP = bySlug[home.hero.slug];
|
|
|
const featured = [...home.reel.filter(r => r.slug !== home.hero.slug), ...home.grid]
|
|
|
.filter((v, i, a) => a.findIndex(x => x.slug === v.slug) === i)
|
|
|
.slice(0, 6);
|
|
|
const flow = featured.map((g, i) => piece(bySlug[g.slug], i, g.index)).join('');
|
|
|
|
|
|
const jsonld = JSON.stringify({
|
|
|
'@context': 'https://schema.org', '@type': 'Organization',
|
|
|
name: site.name, url: site.domain, email: site.email,
|
|
|
foundingDate: String(site.established),
|
|
|
sameAs: [site.instagram, site.facebook],
|
|
|
address: site.offices.map(of => ({
|
|
|
'@type': 'PostalAddress', streetAddress: of.address[0],
|
|
|
addressLocality: of.address[1].split(',')[0], addressRegion: 'Kerala', addressCountry: 'IN'
|
|
|
}))
|
|
|
});
|
|
|
|
|
|
const body = `
|
|
|
<main id="main">
|
|
|
<section class="hero-quiet wrap" aria-label="Art.n Architects">
|
|
|
<h1>
|
|
|
<span class="line"><span>Buildings with</span></span>
|
|
|
<span class="line"><span>a quiet sense</span></span>
|
|
|
<span class="line"><span>of <em>belonging.</em></span></span>
|
|
|
</h1>
|
|
|
<div class="meta">
|
|
|
<span class="eyebrow">Architecture · Interiors · Project Management</span>
|
|
|
<span class="eyebrow">Changanacherry & Kochi — since ${site.established}</span>
|
|
|
</div>
|
|
|
<p class="hero-scroll eyebrow" aria-hidden="true">scroll</p>
|
|
|
</section>
|
|
|
|
|
|
<section class="panel-full" aria-label="${esc(heroP.title)}">
|
|
|
<div class="photo">${picture(home.hero.slug, home.hero.index, { sizes: '100vw', eager: true, alt: heroP.heroAlt || heroP.title })}</div>
|
|
|
<p class="cap">${esc(heroP.tileLabel || heroP.title)}${heroP.place ? ' — ' + esc(heroP.place) : ''}</p>
|
|
|
</section>
|
|
|
|
|
|
<section class="statement wrap">
|
|
|
<span class="eyebrow reveal">The practice</span>
|
|
|
<p class="big reveal">Every project begins as a simple intention — a place that is right for its people, its climate and its time. <em>We design it, detail it, and stay until it is delivered.</em></p>
|
|
|
<div class="sub reveal">
|
|
|
<p>Art.n Architects is an architecture, interior design and project management
|
|
|
studio working from Changanacherry and Kochi since ${site.established} — residences,
|
|
|
hospitality, healthcare and public buildings across Kerala.</p>
|
|
|
<p><a href="/studio/"><span class="u">Meet the studio</span></a></p>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="wrap" aria-label="Selected work">
|
|
|
<div class="flow">${flow}
|
|
|
</div>
|
|
|
<p class="view-all reveal"><a href="/projects/"><span class="u">See all ${projects.length} projects</span></a></p>
|
|
|
</section>
|
|
|
|
|
|
<section class="threed wrap" id="threed">
|
|
|
<div class="threed-row">
|
|
|
<div class="reveal">
|
|
|
<model-viewer
|
|
|
src="/assets/3d/massing.glb"
|
|
|
alt="Conceptual massing model of a Kerala residence"
|
|
|
camera-controls auto-rotate rotation-per-second="10deg"
|
|
|
shadow-intensity="1" exposure="1.05"
|
|
|
data-orbit-scrub
|
|
|
loading="lazy" reveal="auto">
|
|
|
</model-viewer>
|
|
|
</div>
|
|
|
<div class="reveal">
|
|
|
<span class="eyebrow" style="display:block;margin-bottom:20px">From the board</span>
|
|
|
<h3>Walk around the drawing.</h3>
|
|
|
<p>Drag the model — or simply keep scrolling and watch it turn. This is the
|
|
|
massing study of a Kerala residence, the stage where every project begins
|
|
|
before a single wall goes up.</p>
|
|
|
<p class="note">Signature projects receive their own models here, exported
|
|
|
straight from the studio's working files.</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="studio-row wrap">
|
|
|
<a class="ph reveal" href="/studio/">${picture(home.studio.slug, home.studio.index, { sizes: '(max-width:860px) 100vw, 58vw', alt: 'Inside the Art.n Architects studio', dataAttrs: 'data-inner' })}</a>
|
|
|
<div class="reveal">
|
|
|
<span class="eyebrow" style="display:block;margin-bottom:20px">The studio</span>
|
|
|
<h3>One studio, drawing to handover.</h3>
|
|
|
<p>Architecture, interiors and project management under one roof — so what is
|
|
|
designed is what gets built. <a href="/process/"><span class="u">How we work</span></a></p>
|
|
|
<p class="addr">
|
|
|
${site.offices[0].address.map(esc).join(', ')}<br>
|
|
|
${site.offices[1].address.map(esc).join(', ')}
|
|
|
</p>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
${ctaBand()}
|
|
|
</main>`;
|
|
|
|
|
|
return layout({
|
|
|
path: '/', current: '/', body, jsonld,
|
|
|
title: 'Art.n Architects — Architecture, Interiors & Project Management, Kerala',
|
|
|
desc: `Architecture, interior design and project management studio in Changanacherry and Kochi, Kerala, since ${site.established}.`,
|
|
|
ogImage: (() => { const e = entryFor(home.hero.slug, home.hero.index); return e ? '/' + e.variants.find(v => v.format === 'jpeg').file : null; })()
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/* ---------- LISTINGS ---------- */
|
|
|
const FILTERS = [
|
|
|
['/projects/', 'all work'],
|
|
|
['/projects/architecture/', 'architecture'],
|
|
|
['/projects/interiors/', 'interiors'],
|
|
|
['/projects/pmc/', 'project management'],
|
|
|
];
|
|
|
function listingPage(list, o) {
|
|
|
const filters = FILTERS.map(([href, label]) =>
|
|
|
`<a href="${href}"${href === o.path ? ' aria-current="page"' : ''}>${label}</a>`).join('\n ');
|
|
|
const flow = list.map((p, i) => piece(p, i)).join('');
|
|
|
const body = `
|
|
|
<main id="main">
|
|
|
<section class="page-head wrap">
|
|
|
<span class="eyebrow reveal">${String(list.length).padStart(2, '0')} projects</span>
|
|
|
<h1 class="reveal">${o.h1}</h1>
|
|
|
<p class="lede reveal">${o.lede}</p>
|
|
|
<div class="filters reveal">
|
|
|
${filters}
|
|
|
</div>
|
|
|
</section>
|
|
|
<section class="wrap">
|
|
|
<div class="flow">${flow}
|
|
|
</div>
|
|
|
</section>
|
|
|
${ctaBand()}
|
|
|
</main>`;
|
|
|
return layout({
|
|
|
path: o.path, current: '/projects/', body,
|
|
|
title: o.title, desc: o.desc
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/* ---------- PROJECT PAGE ---------- */
|
|
|
function projectPage(p, prev, next) {
|
|
|
const hi = heroIdxOf(p);
|
|
|
const valid = validIndices(p.slug).filter(i => i !== hi);
|
|
|
const galleryItems = valid.map((idx, n) => `
|
|
|
<a href="${biggest(p.slug, idx)}" class="glightbox" data-gallery="proj" data-glightbox="title: ${esc(p.title)} — ${String(n + 2).padStart(2, '0')}">
|
|
|
${picture(p.slug, idx, { sizes: '(max-width:860px) 100vw, 46vw', alt: `${p.title} — photograph ${n + 2}` })}
|
|
|
</a>`).join('');
|
|
|
const spec = [
|
|
|
['Category', p.category],
|
|
|
['Client', p.client],
|
|
|
['Year', p.year],
|
|
|
['Place', p.place],
|
|
|
['Scope', p.discipline.map(d => ({ architecture: 'Architecture', interior: 'Interiors', pmc: 'Project Management' }[d] || d)).join(' · ')],
|
|
|
].filter(([, v]) => v).map(([k, v]) => `<li><span class="k">${k}</span><span class="v">${esc(v)}</span></li>`).join('\n ');
|
|
|
const blurb = p.blurb || `${p.title} — a ${(p.category || 'building').toLowerCase()} project by ${site.name}${p.place ? ' in ' + p.place : ''}.`;
|
|
|
const jsonld = JSON.stringify({
|
|
|
'@context': 'https://schema.org', '@type': 'BreadcrumbList',
|
|
|
itemListElement: [
|
|
|
{ '@type': 'ListItem', position: 1, name: 'Projects', item: site.domain + '/projects/' },
|
|
|
{ '@type': 'ListItem', position: 2, name: p.title, item: site.domain + projUrl(p) },
|
|
|
]
|
|
|
});
|
|
|
const body = `
|
|
|
<main id="main">
|
|
|
<section class="proj-hero wrap">
|
|
|
<div class="ph">${picture(p.slug, hi, { sizes: '92vw', eager: true, alt: p.heroAlt || p.title })}</div>
|
|
|
</section>
|
|
|
<section class="proj-title wrap">
|
|
|
<span class="eyebrow">${esc([p.category, p.place].filter(Boolean).join(' — '))}</span>
|
|
|
<h1>${esc(p.title)}</h1>
|
|
|
</section>
|
|
|
<div class="wrap">
|
|
|
<div class="proj-cols">
|
|
|
<ul class="spec reveal">
|
|
|
${spec}
|
|
|
</ul>
|
|
|
<div class="proj-blurb reveal">
|
|
|
<p>${esc(blurb)}</p>
|
|
|
</div>
|
|
|
</div>
|
|
|
${valid.length ? `<div class="gallery">${galleryItems}
|
|
|
</div>` : ''}
|
|
|
<nav class="proj-nav" aria-label="More projects">
|
|
|
<a href="${projUrl(prev)}"><span class="eyebrow">previous</span><span class="name u">${esc(prev.tileLabel || prev.title)}</span></a>
|
|
|
<a class="next" href="${projUrl(next)}"><span class="eyebrow">next</span><span class="name u">${esc(next.tileLabel || next.title)}</span></a>
|
|
|
</nav>
|
|
|
</div>
|
|
|
${ctaBand('Planning something like this? <em>Let us draw it with you.</em>')}
|
|
|
</main>`;
|
|
|
return layout({
|
|
|
path: projUrl(p), current: '/projects/', body, jsonld,
|
|
|
title: `${p.title} — ${[p.category, p.place].filter(Boolean).join(', ')} — ${site.name}`,
|
|
|
desc: blurb.length > 155 ? blurb.slice(0, 152) + '…' : blurb,
|
|
|
scripts: ['/assets/vendor/glightbox.min.js'],
|
|
|
styles: ['/assets/vendor/glightbox.min.css'],
|
|
|
ogImage: (() => { const e = entryFor(p.slug, hi); return e ? '/' + e.variants.find(v => v.format === 'jpeg').file : null; })()
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/* ---------- STUDIO ---------- */
|
|
|
function studioPage() {
|
|
|
const team = site.team.map(t => {
|
|
|
const key = 'team:' + t.name;
|
|
|
return `
|
|
|
<figure class="reveal">
|
|
|
<div class="ph">${extraPic(key, { sizes: '(max-width:860px) 45vw, 12vw', alt: t.name })}</div>
|
|
|
<figcaption><span class="nm">${esc(t.name)}</span><span class="rl">${esc(t.role)}</span></figcaption>
|
|
|
</figure>`;
|
|
|
}).join('');
|
|
|
const body = `
|
|
|
<main id="main">
|
|
|
<section class="page-head wrap">
|
|
|
<span class="eyebrow reveal">The studio — est. ${site.established}</span>
|
|
|
<h1 class="reveal">A practice built on <em>staying with the work.</em></h1>
|
|
|
<p class="lede reveal">Architecture, interiors and project management under one roof
|
|
|
in Changanacherry and Kochi — so what is designed is what gets built.</p>
|
|
|
</section>
|
|
|
|
|
|
<section class="wrap story">
|
|
|
<div class="ph reveal">${extraPic('team-group', { sizes: '(max-width:860px) 100vw, 42vw', alt: 'The Art.n Architects team', dataAttrs: 'data-inner' })}</div>
|
|
|
<div class="reveal">
|
|
|
<span class="eyebrow" style="display:block;margin-bottom:20px">Our story</span>
|
|
|
<h2>Drawing for Kerala since ${site.established}.</h2>
|
|
|
<p>The practice opened its board in <b>Changanacherry in ${site.established}</b>, working first on
|
|
|
residences and renovations, and grew project by project into hospitality, healthcare,
|
|
|
commercial and institutional work. In 2007 a second studio opened in <b>Kochi</b>.</p>
|
|
|
<p>The name on the door is a position: architecture rooted in Kerala's building
|
|
|
tradition — climate, courtyard, veranda, roof — drawn with a contemporary hand.
|
|
|
Vastu informs the plan when clients ask for it; the drawing board decides the rest.</p>
|
|
|
<p>Today the studio takes projects from the first sketch to the handover key:
|
|
|
design, detailing, tendering and site supervision by one team.</p>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="wrap people">
|
|
|
<div class="person">
|
|
|
<div class="reveal">
|
|
|
<span class="eyebrow" style="display:block;margin-bottom:20px">Principal</span>
|
|
|
<h3>${esc(site.principal.name)}</h3>
|
|
|
<span class="eyebrow role">${esc(site.principal.role)}</span>
|
|
|
${site.principal.bio.map(b => `<p>${esc(b)}</p>`).join('\n ')}
|
|
|
</div>
|
|
|
<div class="ph reveal">${extraPic('principal', { sizes: '(max-width:860px) 70vw, 25vw', alt: site.principal.name })}</div>
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
<section class="team wrap">
|
|
|
<span class="eyebrow reveal" style="display:block;margin-bottom:40px">The team — ${String(site.team.length + 1).padStart(2, '0')} people</span>
|
|
|
<div class="team-grid">${team}
|
|
|
</div>
|
|
|
</section>
|
|
|
|
|
|
${ctaBand('Want this team on your project? <em>Start the conversation.</em>')}
|
|
|
</main>`;
|
|
|
return layout({
|
|
|
path: '/studio/', current: '/studio/', body,
|
|
|
title: `Studio — ${site.name}`,
|
|
|
desc: `Art.n Architects: an architecture, interiors and project management studio in Changanacherry and Kochi, Kerala, led by principal architect ${site.principal.name} since ${site.established}.`
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/* ---------- PROCESS ---------- */
|
|
|
function processPage() {
|
|
|
const steps = [
|
|
|
['Brief & site', 'We begin where the project does: walking the land, reading its light, slope and neighbours, and listening to how you intend to live or work in it. The brief is written together — needs, budget and time, stated plainly.'],
|
|
|
['Concept', 'The first drawings test the big moves: how the building sits on its site, how rooms gather around light and air, how Kerala’s climate — sun, monsoon, breeze — shapes roof and verandah. We iterate until the plan feels inevitable.'],
|
|
|
['Detail & permits', 'The approved concept is drawn through to working detail: structure, materials, joinery, services. Statutory drawings and approvals are handled by the studio, so the project stays on one board.'],
|
|
|
['Tender', 'Contractors bid through our own online tender portal — transparent pricing, comparable bids, and a paper trail the client can read. We recommend; you decide.'],
|
|
|
['Site & supervision', 'Our engineers and project managers stay on the work: quality checks, bill certification, cost control and the weekly rhythm of site meetings, from foundation to finishes.'],
|
|
|
['Handover', 'The building is delivered as it was drawn — snagged, documented and photographed. Most of our work returns as the next project: an interior, an extension, a new site.'],
|
|
|
].map(([h, t], i) => `
|
|
|
<div class="step reveal">
|
|
|
<span class="no">0${i + 1}</span>
|
|
|
<h3>${h}</h3>
|
|
|
<p>${t}</p>
|
|
|
</div>`).join('');
|
|
|
const body = `
|
|
|
<main id="main">
|
|
|
<section class="page-head wrap">
|
|
|
<span class="eyebrow reveal">How we work</span>
|
|
|
<h1 class="reveal">From the first line <em>to the last key.</em></h1>
|
|
|
<p class="lede reveal">One studio carries the project the whole way — design,
|
|
|
detailing, tendering and site supervision. Six stages, one hand on the drawing.</p>
|
|
|
</section>
|
|
|
<section class="wrap steps">${steps}
|
|
|
</section>
|
|
|
${ctaBand('Curious where your project would begin? <em>Ask us.</em>')}
|
|
|
</main>`;
|
|
|
return layout({
|
|
|
path: '/process/', current: '/process/', body,
|
|
|
title: `Process — ${site.name}`,
|
|
|
desc: 'How Art.n Architects works: brief and site, concept, detail and permits, tendering through our online portal, site supervision, and handover — one studio the whole way.'
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/* ---------- CONTACT ---------- */
|
|
|
function contactPage() {
|
|
|
const offices = site.offices.map(of => `
|
|
|
<div class="office reveal">
|
|
|
<h2>${esc(of.name)}</h2>
|
|
|
<p>${of.address.map(esc).join('<br>')}</p>
|
|
|
<div class="links">
|
|
|
<a href="${of.phoneHref}">${esc(of.phone)}</a>
|
|
|
<a href="${of.maps}" rel="noopener" target="_blank"><span class="u">Open in Google Maps</span></a>
|
|
|
</div>
|
|
|
</div>`).join('');
|
|
|
const jsonld = JSON.stringify({
|
|
|
'@context': 'https://schema.org', '@type': 'ProfessionalService',
|
|
|
name: site.name, url: site.domain, email: site.email,
|
|
|
telephone: '+91 ' + site.offices[0].phone,
|
|
|
address: site.offices.map(of => ({
|
|
|
'@type': 'PostalAddress', streetAddress: of.address[0],
|
|
|
addressLocality: of.address[1].split(',')[0], addressRegion: 'Kerala', addressCountry: 'IN'
|
|
|
}))
|
|
|
});
|
|
|
const body = `
|
|
|
<main id="main">
|
|
|
<section class="page-head wrap">
|
|
|
<span class="eyebrow reveal">Contact</span>
|
|
|
<h1 class="reveal">A first conversation <em>costs nothing.</em></h1>
|
|
|
<p class="lede reveal">Bring a site, a brief, or just an idea — it usually starts with a sketch.</p>
|
|
|
</section>
|
|
|
<section class="wrap offices">${offices}
|
|
|
</section>
|
|
|
<section class="wrap reach">
|
|
|
<a class="reveal" href="mailto:${site.email}">
|
|
|
<span class="k">email</span>
|
|
|
<span class="v">${esc(site.email)}</span>
|
|
|
</a>
|
|
|
<a class="reveal" href="${site.whatsapp}" rel="noopener">
|
|
|
<span class="k">whatsapp</span>
|
|
|
<span class="v">+91 94471 12848</span>
|
|
|
</a>
|
|
|
<a class="reveal" href="${site.instagram}" rel="noopener">
|
|
|
<span class="k">instagram</span>
|
|
|
<span class="v">@art.n.architects</span>
|
|
|
</a>
|
|
|
<a class="reveal" href="${site.tenderUrl}" rel="noopener">
|
|
|
<span class="k">contractors</span>
|
|
|
<span class="v">Tender portal</span>
|
|
|
</a>
|
|
|
</section>
|
|
|
</main>`;
|
|
|
return layout({
|
|
|
path: '/contact/', current: '/contact/', body, jsonld,
|
|
|
title: `Contact — ${site.name}`,
|
|
|
desc: 'Contact Art.n Architects — Changanacherry 0481 241 2848 · Kochi 0484 234 1067 · artnove@gmail.com · WhatsApp +91 94471 12848.'
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/* ---------- 404 ---------- */
|
|
|
function notFoundPage() {
|
|
|
const body = `
|
|
|
<main id="main">
|
|
|
<section class="wrap lost">
|
|
|
<span class="eyebrow">page not found</span>
|
|
|
<h1>404</h1>
|
|
|
<p>This page was never drawn — or it has been filed elsewhere.</p>
|
|
|
<p><a class="btn" href="/">back to the start</a></p>
|
|
|
</section>
|
|
|
</main>`;
|
|
|
return layout({
|
|
|
path: '/404.html', current: '', body,
|
|
|
title: `Page not found — ${site.name}`, desc: 'Page not found.'
|
|
|
});
|
|
|
}
|
|
|
|
|
|
/* ---------- writing ---------- */
|
|
|
function write(rel, html) {
|
|
|
const f = path.join(DIST, rel);
|
|
|
fs.mkdirSync(path.dirname(f), { recursive: true });
|
|
|
fs.writeFileSync(f, html);
|
|
|
}
|
|
|
function copy(srcRel, destRel) {
|
|
|
const s = path.isAbsolute(srcRel) ? srcRel : path.join(ROOT, srcRel);
|
|
|
const d = path.join(DIST, destRel);
|
|
|
fs.mkdirSync(path.dirname(d), { recursive: true });
|
|
|
fs.copyFileSync(s, d);
|
|
|
}
|
|
|
|
|
|
write('index.html', homePage());
|
|
|
write('projects/index.html', listingPage(projects, {
|
|
|
path: '/projects/', h1: 'Work that <em>stays.</em>', lede:
|
|
|
'Every project the studio has drawn, detailed and delivered — residences, hospitality, healthcare and public buildings across Kerala.',
|
|
|
title: `Projects — ${site.name}`,
|
|
|
desc: `${projects.length} built and ongoing projects by Art.n Architects across Kerala: residences, resorts, hospitals, institutions and offices.`
|
|
|
}));
|
|
|
const disc = (d) => projects.filter(p => p.discipline.includes(d));
|
|
|
write('projects/architecture/index.html', listingPage(disc('architecture'), {
|
|
|
path: '/projects/architecture/', h1: 'Architecture', lede:
|
|
|
"Buildings designed from the ground up — concept, statutory drawings and detail for Kerala's climate and way of living.",
|
|
|
title: `Architecture projects — ${site.name}`,
|
|
|
desc: 'Architecture portfolio of Art.n Architects: residences, resorts, hospitals and institutional buildings across Kerala.'
|
|
|
}));
|
|
|
write('projects/interiors/index.html', listingPage(disc('interior'), {
|
|
|
path: '/projects/interiors/', h1: 'Interiors', lede:
|
|
|
'Interior architecture that carries the building through — homes, restaurants, offices and institutions.',
|
|
|
title: `Interior design projects — ${site.name}`,
|
|
|
desc: 'Interior design portfolio of Art.n Architects: homes, restaurants, offices and institutional interiors across Kerala.'
|
|
|
}));
|
|
|
write('projects/pmc/index.html', listingPage(disc('pmc'), {
|
|
|
path: '/projects/pmc/', h1: 'Project management', lede:
|
|
|
'Projects where the studio also ran the site — tendering, supervision and cost control from first drawing to handover.',
|
|
|
title: `Project management — ${site.name}`,
|
|
|
desc: 'Project management consultancy by Art.n Architects: tendering, site supervision and delivery across Kerala.'
|
|
|
}));
|
|
|
projects.forEach((p, i) => {
|
|
|
const prev = projects[(i - 1 + projects.length) % projects.length];
|
|
|
const next = projects[(i + 1) % projects.length];
|
|
|
write(path.join('projects', p.slug, 'index.html'), projectPage(p, prev, next));
|
|
|
});
|
|
|
write('studio/index.html', studioPage());
|
|
|
write('process/index.html', processPage());
|
|
|
write('contact/index.html', contactPage());
|
|
|
write('404.html', notFoundPage());
|
|
|
|
|
|
/* legacy-URL redirect stubs */
|
|
|
const redirect = (to) => `<!doctype html><html lang="en"><head><meta charset="utf-8"><meta http-equiv="refresh" content="0;url=${to}"><link rel="canonical" href="${site.domain}${to}"><title>Moved</title></head><body><p>Moved to <a href="${to}">${to}</a>.</p></body></html>`;
|
|
|
const legacy = { 'works.html': '/projects/', 'work architecture.html': '/projects/architecture/', 'work interior.html': '/projects/interiors/', 'work pmc.html': '/projects/pmc/', 'about.html': '/studio/', 'contact.html': '/contact/' };
|
|
|
for (const p of projects) legacy[p.sourcePage] = projUrl(p);
|
|
|
for (const [oldName, to] of Object.entries(legacy)) write(oldName, redirect(to));
|
|
|
|
|
|
/* sitemap + robots */
|
|
|
const urls = ['/', '/projects/', '/projects/architecture/', '/projects/interiors/', '/projects/pmc/', '/studio/', '/process/', '/contact/']
|
|
|
.concat(projects.map(projUrl));
|
|
|
write('sitemap.xml', `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
|
|
|
${urls.map(u => ` <url><loc>${site.domain}${u}</loc></url>`).join('\n')}
|
|
|
</urlset>
|
|
|
`);
|
|
|
write('robots.txt', `User-agent: *\nAllow: /\nSitemap: ${site.domain}/sitemap.xml\n`);
|
|
|
|
|
|
/* assets */
|
|
|
copy('src/css/site.css', 'assets/css/site.css');
|
|
|
copy('src/js/site.js', 'assets/js/site.js');
|
|
|
for (const f of fs.readdirSync(path.join(SRC, 'fonts'))) if (f.endsWith('.woff2')) copy(path.join('src', 'fonts', f), path.join('assets', 'fonts', f));
|
|
|
if (fs.existsSync(path.join(SRC, '3d', 'massing.glb'))) copy('src/3d/massing.glb', 'assets/3d/massing.glb');
|
|
|
const V = 'node_modules';
|
|
|
copy(`${V}/gsap/dist/gsap.min.js`, 'assets/vendor/gsap.min.js');
|
|
|
copy(`${V}/gsap/dist/ScrollTrigger.min.js`, 'assets/vendor/ScrollTrigger.min.js');
|
|
|
copy(`${V}/lenis/dist/lenis.min.js`, 'assets/vendor/lenis.min.js');
|
|
|
copy(`${V}/@google/model-viewer/dist/model-viewer.min.js`, 'assets/vendor/model-viewer.min.js');
|
|
|
copy(`${V}/glightbox/dist/js/glightbox.min.js`, 'assets/vendor/glightbox.min.js');
|
|
|
copy(`${V}/glightbox/dist/css/glightbox.min.css`, 'assets/vendor/glightbox.min.css');
|
|
|
|
|
|
console.log(`BUILD OK — ${urls.length} pages + ${Object.keys(legacy).length} redirects (quiet edition)`);
|