Partner Documentation
Learn how to create, upload, and sell invitation templates on InviteAround
Next.js 15+ Landing Page & Company Profile
Professional standard for building landing page templates with Next.js App Router, React 19, TypeScript, and Tailwind CSS.
InviteAround landing page templates use a Static Export architecture powered by @invitearound/template-runtime. Templates receive dynamic updates via postMessage inside the visual editor iframe without page reloads, and fall back to /public/assets/data.json in standalone mode.
data-bind attributes and interactive pencil overlays trigger field edit modals directly.
createWhatsAppUrl helper formats phone numbers and prefilled messages into click-to-chat links.
fallbackString and fallbackArray sanitizers prevent rendering errors when fields are empty.
Template Folder Structure
Every Next.js landing page template must adhere to the standard directory layout demonstrated in cold-slate:
cold-slate/
├── app/
│ ├── favicon.ico
│ ├── globals.css # Tailwind CSS v4 directives
│ ├── layout.tsx # Root layout Next.js
│ └── page.tsx # Entrypoint utama ('use client')
├── public/
│ └── assets/
│ ├── data.json # Data default saat pertama dibuka
│ └── template-form-schema.json # Definisi formulir visual editor
├── types/
│ └── index.ts # Definisi interface TypeScript
├── next.config.ts # Konfigurasi static HTML export
├── package.json # Entrypoint: "main": "app/page.tsx"
├── postcss.config.mjs
└── tsconfig.jsonDependencies & package.json
Install required @invitearound/template-runtime package and lucide-react icon set:
# Menggunakan Bun (disarankan)
bun add @invitearound/template-runtime lucide-react
# Menggunakan npm
npm install @invitearound/template-runtime lucide-reactEnsure package.json defines "main": "app/page.tsx" and "types": "app/page.tsx":
{
"name": "my-company-profile-template",
"version": "0.1.0",
"private": true,
"main": "app/page.tsx",
"types": "app/page.tsx",
"scripts": {
"dev": "next dev",
"build": "next build",
"start": "next start",
"lint": "eslint"
},
"dependencies": {
"@invitearound/template-runtime": "^1.0.3",
"lucide-react": "^1.16.0",
"next": "^16.2.1",
"react": "^19.0.0",
"react-dom": "^19.0.0"
},
"devDependencies": {
"@tailwindcss/postcss": "^4",
"@types/node": "^20",
"@types/react": "^19",
"@types/react-dom": "^19",
"tailwindcss": "^4",
"typescript": "^5"
}
}Static Export Configuration (next.config.ts)
Templates uploaded to InviteAround are compiled into pure static HTML/CSS/JS files for high-speed delivery. You must set output: 'export' and trailingSlash: true:
import type { NextConfig } from 'next';
const nextConfig: NextConfig = {
// WAJIB: Template InviteAround diekspor sebagai Static HTML
output: 'export',
trailingSlash: true,
images: {
// Agar gambar eksternal (Unsplash, Storage, Google) dapat di-render tanpa optimasi server
remotePatterns: [
{
protocol: 'https',
hostname: 'storage.invitearound.com',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'images.unsplash.com',
pathname: '/**',
},
{
protocol: 'https',
hostname: 'lh3.googleusercontent.com',
pathname: '/**',
},
],
},
};
export default nextConfig;TypeScript Prop Types (types/index.ts)
Define comprehensive prop interfaces for the landing page. Keys must strictly match data.json and template-form-schema.json:
// types/index.ts
export interface StatItem {
value: string;
suffix?: string;
label: string;
}
export interface ServiceItem {
title: string;
description: string;
icon: string; // nama icon dari ICON_MAP, misal: "Briefcase", "Scale", "Coffee"
tag?: string;
}
export interface PortfolioItem {
projectTitle: string;
category: string;
highlightMetric?: string;
previewImage: string;
completedYear?: string;
}
export interface TeamMemberItem {
fullName: string;
roleTitle: string;
avatarUrl: string;
linkedinUrl?: string;
instagramUrl?: string;
websiteUrl?: string;
}
export interface CompanyProfileProps {
// Brand Identity & WhatsApp Kontak
brandName: string;
brandTagline?: string;
brandLogo?: string;
whatsappNumber: string;
whatsappDefaultMessage?: string;
contactEmail?: string;
operatingHours?: string;
headerCtaText?: string;
floatingWaText?: string;
// Navigasi Labels
navServicesText?: string;
navPortfoliosText?: string;
navTeamText?: string;
navContactText?: string;
// Hero Section
heroBadge?: string;
heroHeadline: string;
heroSubheadline: string;
heroPrimaryCtaText?: string;
heroSecondaryCtaText?: string;
heroCoverImage?: string;
stats?: StatItem[];
// Services Bento Grid
servicesBadge?: string;
servicesHeadline?: string;
servicesSubheadline?: string;
servicesCtaText?: string;
services?: ServiceItem[];
// Portfolios
portfolioBadge?: string;
portfolioHeadline?: string;
portfolioSubheadline?: string;
portfolioAllLabel?: string;
portfolioCtaText?: string;
portfolios?: PortfolioItem[];
// Team
teamBadge?: string;
teamHeadline?: string;
teamSubheadline?: string;
teamMembers?: TeamMemberItem[];
// Legal & Footer
legalCompanyName: string;
legalRegistrationNumber?: string;
officeAddress: string;
googleMapsUrl?: string;
footerAddressHeading?: string;
footerContactHeading?: string;
footerMapLinkText?: string;
copyrightNotice?: string;
}Main Page Implementation (app/page.tsx)
Use useAutoTemplateData hook, add data-bind="fieldKey" attributes to all visual elements, and place EditPencilOverlay at the bottom of the document:
'use client';
import { useState, useMemo } from 'react';
import type { ElementType } from 'react';
import {
MessageCircle,
Briefcase,
Scale,
Coffee,
Award,
ArrowUpRight,
ChevronRight,
Menu,
X,
Phone,
Mail,
MapPin,
Clock,
} from 'lucide-react';
import type { CompanyProfileProps, ServiceItem, PortfolioItem } from '../types';
import defaultData from '../public/assets/data.json';
import schemaData from '../public/assets/template-form-schema.json';
import {
useAutoTemplateData,
fallbackString,
fallbackArray,
EditPencilOverlay,
createWhatsAppUrl,
} from '@invitearound/template-runtime';
// 1. Registrasi Icon Lucide untuk kategori bisnis dinamis
const ICON_MAP: Record<string, ElementType> = {
Briefcase,
Scale,
Coffee,
Award,
Phone,
Mail,
MapPin,
Clock,
};
export default function CompanyProfilePage() {
// 2. Hook runtime InviteAround: otomatis sinkronisasi realtime dengan editor iframe
const data: CompanyProfileProps = useAutoTemplateData<CompanyProfileProps>(
defaultData as CompanyProfileProps
);
// 3. Sanitasi fallback aman agar tidak terjadi error undefined saat field kosong
const brandName = fallbackString(data.brandName);
const brandTagline = fallbackString(data.brandTagline);
const brandLogo = fallbackString(data.brandLogo);
const whatsappNumber = fallbackString(data.whatsappNumber);
const whatsappDefaultMessage = fallbackString(data.whatsappDefaultMessage);
const heroBadge = fallbackString(data.heroBadge);
const heroHeadline = fallbackString(data.heroHeadline);
const heroSubheadline = fallbackString(data.heroSubheadline);
const heroPrimaryCtaText = fallbackString(data.heroPrimaryCtaText);
const heroCoverImage = fallbackString(data.heroCoverImage);
const services: ServiceItem[] = fallbackArray(data.services);
const portfolios: PortfolioItem[] = fallbackArray(data.portfolios);
// 4. Buat URL WhatsApp interaktif dengan helper createWhatsAppUrl
const defaultWaLink = whatsappNumber
? createWhatsAppUrl(whatsappNumber, whatsappDefaultMessage, {
type: 'general',
})
: '';
return (
<div className="min-h-screen bg-white text-slate-900 font-sans">
{/* HEADER / NAVBAR */}
<header className="sticky top-0 z-40 bg-white/90 backdrop-blur-md border-b border-slate-100">
<div className="max-w-7xl mx-auto px-4 h-20 flex items-center justify-between">
<div className="flex items-center gap-3">
{brandLogo && (
<img
data-bind="brandLogo"
src={brandLogo}
alt="Brand Logo"
className="w-10 h-10 rounded-xl object-cover"
/>
)}
<div>
<span data-bind="brandName" className="font-bold text-base block">
{brandName}
</span>
{brandTagline && (
<span data-bind="brandTagline" className="text-xs text-slate-400">
{brandTagline}
</span>
)}
</div>
</div>
{defaultWaLink && (
<a
data-bind="headerCtaText"
href={defaultWaLink}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-4 py-2 rounded-xl text-xs font-semibold text-white bg-amber-600 hover:bg-amber-700 transition"
>
<MessageCircle className="w-3.5 h-3.5" />
<span>{fallbackString(data.headerCtaText) || 'Hubungi Kami'}</span>
</a>
)}
</div>
</header>
{/* HERO SECTION */}
<section className="py-20 px-4 max-w-7xl mx-auto">
{heroBadge && (
<span
data-bind="heroBadge"
className="inline-block px-3 py-1 rounded-full text-xs font-semibold bg-amber-100 text-amber-800 mb-4"
>
{heroBadge}
</span>
)}
<h1
data-bind="heroHeadline"
className="text-4xl sm:text-6xl font-extrabold tracking-tight mb-6"
>
{heroHeadline}
</h1>
<p
data-bind="heroSubheadline"
className="text-lg text-slate-600 max-w-2xl mb-8 leading-relaxed"
>
{heroSubheadline}
</p>
{defaultWaLink && heroPrimaryCtaText && (
<a
data-bind="heroPrimaryCtaText"
href={defaultWaLink}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-2 px-6 py-3 rounded-xl text-sm font-semibold text-white bg-amber-600 hover:bg-amber-700 shadow-lg shadow-amber-600/20"
>
<MessageCircle className="w-4 h-4" />
<span>{heroPrimaryCtaText}</span>
</a>
)}
{heroCoverImage && (
<div className="mt-12 rounded-2xl overflow-hidden border border-slate-200">
<img
data-bind="heroCoverImage"
src={heroCoverImage}
alt="Hero Cover"
className="w-full h-auto aspect-video object-cover"
/>
</div>
)}
</section>
{/* SERVICES REPEATABLE BENTO */}
{services.length > 0 && (
<section className="py-16 bg-slate-50 border-t border-slate-100 px-4">
<div className="max-w-7xl mx-auto">
<h2 data-bind="servicesHeadline" className="text-3xl font-bold mb-8">
{fallbackString(data.servicesHeadline) || 'Layanan Kami'}
</h2>
<div data-bind="services" className="grid grid-cols-1 md:grid-cols-3 gap-6">
{services.map((service, idx) => {
const IconComponent = ICON_MAP[service.icon] || Briefcase;
const serviceWaLink = whatsappNumber
? createWhatsAppUrl(whatsappNumber, whatsappDefaultMessage, {
type: 'service',
title: service.title,
})
: '';
return (
<div
key={idx}
className="p-6 bg-white rounded-2xl border border-slate-200/80 shadow-sm"
>
<div className="w-10 h-10 rounded-xl bg-amber-50 text-amber-600 flex items-center justify-center mb-4">
<IconComponent className="w-5 h-5" />
</div>
<h3 className="font-bold text-lg mb-2">{service.title}</h3>
<p className="text-sm text-slate-500 leading-relaxed mb-4">
{service.description}
</p>
{serviceWaLink && (
<a
href={serviceWaLink}
target="_blank"
rel="noopener noreferrer"
className="text-xs font-semibold text-amber-600 hover:underline inline-flex items-center gap-1"
>
Tanya Layanan Ini <ArrowUpRight className="w-3.5 h-3.5" />
</a>
)}
</div>
);
})}
</div>
</div>
</section>
)}
{/* 5. Overlay pensil editor visual InviteAround di akhir halaman */}
<EditPencilOverlay schema={schemaData} />
</div>
);
}Visual Form Editor Schema (public/assets/template-form-schema.json)
This schema drives the InviteAround visual form editor. Supported field types include text, long_text, image, phone, url, and group_array (for repeatable collections like services & portfolios):
{
"title": "Konfigurasi Company Profile",
"sections": [
{
"title": "Identitas Brand",
"fields": [
{
"key": "brandName",
"label": "Nama Brand / Usaha",
"type": "text",
"required": true
},
{
"key": "brandTagline",
"label": "Tagline Singkat",
"type": "text"
},
{
"key": "brandLogo",
"label": "Logo Brand",
"type": "image"
},
{
"key": "whatsappNumber",
"label": "Nomor WhatsApp Kontak",
"type": "phone",
"required": true
},
{
"key": "whatsappDefaultMessage",
"label": "Template Pesan Default WhatsApp",
"type": "long_text"
}
]
},
{
"title": "Hero Section",
"fields": [
{
"key": "heroBadge",
"label": "Badge Kecil (di atas headline)",
"type": "text"
},
{
"key": "heroHeadline",
"label": "Judul Utama (Headline)",
"type": "text",
"required": true
},
{
"key": "heroSubheadline",
"label": "Deskripsi Subheadline",
"type": "long_text"
},
{
"key": "heroCoverImage",
"label": "Gambar Utama (Hero Cover)",
"type": "image"
}
]
},
{
"title": "Layanan (Services)",
"fields": [
{
"key": "servicesHeadline",
"label": "Judul Bagian Layanan",
"type": "text"
},
{
"key": "services",
"label": "Daftar Layanan",
"type": "group_array",
"itemLabel": "Layanan",
"fields": [
{
"key": "title",
"label": "Nama Layanan",
"type": "text"
},
{
"key": "description",
"label": "Deskripsi Layanan",
"type": "long_text"
},
{
"key": "icon",
"label": "Ikon",
"type": "text"
}
]
}
]
}
]
}Supported Attribute Types (TEMPLATE_ATTRIBUTE_TYPE)
InviteAround supports 12 attribute data types in template-form-schema.json to control editor inputs and prop synchronization:
| Attribute Type | Editor Input Control | TypeScript Type | Example Usage |
|---|---|---|---|
text | Single-line text input | string | Brand name, hero headline, CTA button label |
long_text | Multi-line textarea | string | Hero description, about paragraph, subheadline |
number | Numeric input | number | Years of experience, branch count, numeric rating |
date | Calendar date picker | string (YYYY-MM-DD) | Founding date, promo validity date |
datetime | Datetime-local picker | string (ISO 8601 UTC) | Webinar schedule or product launch countdown |
image | Media uploader / picker | string (or string[] if multiple: true) | Brand logo, hero cover image, gallery photos |
music | Audio file uploader (MP3) | string | Ambient background sound |
email | Integrated email input | string | Official contact email, reservation inbox |
phone | Phone / WhatsApp input | string | WhatsApp phone number (used with createWhatsAppUrl) |
url | Web link input | string | Google Maps link, LinkedIn profile, external URL |
video | Video URL / embed input | string | YouTube corporate video URL or direct MP4 link |
group_array | Repeatable Group Manager | T[] (Array of Objects) | Dynamic repeatable collections: services, portfolios, team, FAQ |
GROUP_ARRAY Mechanism & Behavior
The group_array type enables users to visually add, edit, reorder, and delete dynamic repeatable collections on the landing page.
Set type as group_array, define isRepeatableGroup: true, provide itemLabel for item title in the editor UI, and specify fields array for item sub-attributes.
Provide an array of objects matching the fields defined in the schema. Always assign a unique id to each item for reliable React rendering.
Create an interface representing the item (e.g. ServiceItem) and declare it as an optional array on the root CompanyProfileProps interface.
Always wrap collection with fallbackArray(data.services) to prevent crashes on null or empty arrays. Place data-bind="services" on the container wrapper element.
// 1. Skema di public/assets/template-form-schema.json
{
"key": "services",
"label": "Daftar Layanan Kedai",
"type": "group_array",
"isRepeatableGroup": true,
"itemLabel": "Layanan",
"fields": [
{ "key": "id", "label": "ID Item", "type": "text", "required": true },
{ "key": "title", "label": "Nama Layanan", "type": "text", "required": true },
{ "key": "description", "label": "Deskripsi", "type": "long_text" },
{ "key": "icon", "label": "Nama Icon Lucide", "type": "text" },
{ "key": "highlight", "label": "Tandai Populer", "type": "text" }
]
}
// 2. Data default di public/assets/data.json
{
"services": [
{
"id": "1",
"title": "Specialty Manual Brew",
"description": "Biji single-origin pilihan diseduh manual dengan rasio presisi.",
"icon": "Coffee",
"highlight": true
},
{
"id": "2",
"title": "Artisan Pastry & Bakery",
"description": "Croissant dan pastry renyah dipanggang segar setiap pagi.",
"icon": "Award",
"highlight": false
}
]
}
// 3. Tipe TypeScript di types/index.ts
export interface ServiceItem {
id: string;
title: string;
description?: string;
icon?: string;
highlight?: boolean;
}
export interface CompanyProfileProps {
services?: ServiceItem[];
// ... field lainnya
}
// 4. Implementasi di app/page.tsx (Wajib fallbackArray & data-bind)
const services = fallbackArray<ServiceItem>(data.services);
return (
<section className="py-12">
{/* Atribut data-bind="services" ditaruh pada kontainer pembungkus list */}
<div data-bind="services" className="grid grid-cols-1 md:grid-cols-2 gap-6">
{services.map((item, index) => (
<div key={item.id || index} className="p-6 rounded-2xl border border-slate-200">
<h3 className="text-lg font-bold">{item.title}</h3>
<p className="text-sm text-muted-foreground">{item.description}</p>
</div>
))}
</div>
</section>
);- + Add Item Button: Users can append new items to the collection anytime.
- Drag & Drop Reordering: Cards and items can be rearranged up or down instantly.
- Delete Item: Users can remove unwanted items with one click.
- Sub-fields Form: Each item gets its own edit modal matching schema fields.
data-bind Rules & Pencil Editor
The data-bind attribute tells the InviteAround editor which element maps to which field:
- Single Text & Image Fields: Place data-bind="brandName" directly on the element (e.g. h1 data-bind="heroHeadline" or img data-bind="brandLogo").
- Repeatable Collections (Group Array): Place data-bind="services" on the container wrapper element (e.g. div data-bind="services").
- EditPencilOverlay Component: Always include EditPencilOverlay with schema prop in page JSX. It activates edit pencils automatically inside the InviteAround editor iframe.
Build Validation & Upload to Partner Dashboard
Before uploading your template, verify the Next.js static build succeeds without errors:
# 1. Jalankan development server lokal
bun run dev
# 2. Uji build statis
bun run build
# Jika build selesai dengan output folder 'out/', template Anda valid dan siap dipaketkan!Steps to upload to InviteAround:
- Compress the entire project directory (including app/, public/, package.json, etc.) into a .zip file.
- Go to Partner Dashboard → Upload Template.
- Select Category (e.g. Company Profile / Landing Page).
- Upload your thumbnail screenshot and the template .zip file.