// 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, '"');
/* ---------- 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 ` `;
}
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 ` `;
}
/* ---------- 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 `
${label} `;
}).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 `
${esc(o.title)}
${og ? ` ` : ''}
${(o.styles || []).map(h => ` `).join('\n')}
${o.jsonld ? `` : ''}
Skip to content
${o.body}
${vendor.map(s => ``).join('\n')}
`;
}
const ctaBand = (h) => `
${h || 'A site, a brief, or just a first idea — bring it to the table. '}
`;
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 `
${picture(p.slug, hi, { sizes: sizesFor(i), alt: p.heroAlt || p.title })}
${esc(p.tileLabel || p.title)} ${p.place ? `${esc(p.place)} ` : `${esc(p.category || '')} `}
`;
}
/* ---------- 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 = `
Buildings with
a quiet sense
of belonging.
Architecture · Interiors · Project Management
Changanacherry & Kochi — since ${site.established}
scroll
${picture(home.hero.slug, home.hero.index, { sizes: '100vw', eager: true, alt: heroP.heroAlt || heroP.title })}
${esc(heroP.tileLabel || heroP.title)}${heroP.place ? ' — ' + esc(heroP.place) : ''}
The practice
Every project begins as a simple intention — a place that is right for its people, its climate and its time. We design it, detail it, and stay until it is delivered.
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.
Meet the studio
From the board
Walk around the drawing.
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.
Signature projects receive their own models here, exported
straight from the studio's working files.
${ctaBand()}
`;
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]) =>
`${label} `).join('\n ');
const flow = list.map((p, i) => piece(p, i)).join('');
const body = `
${String(list.length).padStart(2, '0')} projects
${o.h1}
${o.lede}
${filters}
${ctaBand()}
`;
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) => `
${picture(p.slug, idx, { sizes: '(max-width:860px) 100vw, 46vw', alt: `${p.title} — photograph ${n + 2}` })}
`).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]) => `${k} ${esc(v)} `).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 = `
${picture(p.slug, hi, { sizes: '92vw', eager: true, alt: p.heroAlt || p.title })}
${esc([p.category, p.place].filter(Boolean).join(' — '))}
${esc(p.title)}
${ctaBand('Planning something like this? Let us draw it with you. ')}
`;
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 `
${extraPic(key, { sizes: '(max-width:860px) 45vw, 12vw', alt: t.name })}
${esc(t.name)} ${esc(t.role)}
`;
}).join('');
const body = `
The studio — est. ${site.established}
A practice built on staying with the work.
Architecture, interiors and project management under one roof
in Changanacherry and Kochi — so what is designed is what gets built.
${extraPic('team-group', { sizes: '(max-width:860px) 100vw, 42vw', alt: 'The Art.n Architects team', dataAttrs: 'data-inner' })}
Our story
Drawing for Kerala since ${site.established}.
The practice opened its board in Changanacherry in ${site.established} , 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 Kochi .
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.
Today the studio takes projects from the first sketch to the handover key:
design, detailing, tendering and site supervision by one team.
Principal
${esc(site.principal.name)}
${esc(site.principal.role)}
${site.principal.bio.map(b => `
${esc(b)}
`).join('\n ')}
${extraPic('principal', { sizes: '(max-width:860px) 70vw, 25vw', alt: site.principal.name })}
The team — ${String(site.team.length + 1).padStart(2, '0')} people
${team}
${ctaBand('Want this team on your project? Start the conversation. ')}
`;
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) => `
`).join('');
const body = `
How we work
From the first line to the last key.
One studio carries the project the whole way — design,
detailing, tendering and site supervision. Six stages, one hand on the drawing.
${ctaBand('Curious where your project would begin? Ask us. ')}
`;
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 => `
${esc(of.name)}
${of.address.map(esc).join(' ')}
`).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 = `
Contact
A first conversation costs nothing.
Bring a site, a brief, or just an idea — it usually starts with a sketch.
`;
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 = `
page not found
404
This page was never drawn — or it has been filed elsewhere.
back to the start
`;
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 stays. ', 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) => `Moved Moved to ${to} .
`;
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', `
${urls.map(u => ` ${site.domain}${u} `).join('\n')}
`);
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)`);