Dokumentasi Partner
Pelajari cara membuat, mengunggah, dan menjual template undangan di InviteAround
Next.js 15+ Landing Page & Company Profile
Standar pembuatan template landing page profesional berbasis Next.js App Router, React 19, TypeScript, dan Tailwind CSS.
Template landing page InviteAround menggunakan arsitektur Static Export dengan runtime client @invitearound/template-runtime. Template menerima data dinamis melalui postMessage di dalam visual editor iframe tanpa reload halaman, serta otomatis fallback ke /public/assets/data.json saat mode standalone.
Atribut data-bind dan overlay pensil interaktif langsung membuka modal edit field.
Helper createWhatsAppUrl mengonversi nomor WhatsApp & konteks layanan jadi click-to-chat.
Sanitizer fallbackString & fallbackArray memastikan tampilan aman bila field kosong.
Struktur Folder Template
Setiap template landing page Next.js harus mengikuti struktur folder baku seperti template referensi 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.jsonInstalasi Dependency & package.json
Instal library wajib @invitearound/template-runtime dan icon set lucide-react:
# Menggunakan Bun (disarankan)
bun add @invitearound/template-runtime lucide-react
# Menggunakan npm
npm install @invitearound/template-runtime lucide-reactPastikan package.json memiliki properti "main": "app/page.tsx" dan "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"
}
}Konfigurasi Static Export (next.config.ts)
Template yang diunggah ke InviteAround akan dibuild menjadi file HTML/CSS/JS statis murni untuk hosting berkecepatan tinggi. Anda wajib menyetel output: 'export' dan 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;Tipe Data Props (types/index.ts)
Definisikan struktur props landing page secara menyeluruh. Kunci properti ini harus cocok 100% dengan field di data.json dan 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;
}Implementasi Halaman Utama (app/page.tsx)
Gunakan hook useAutoTemplateData, tambahkan atribut data-bind="namaKey" pada setiap elemen visual, dan pasang EditPencilOverlay di akhir dokumen:
'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>
);
}Schema Visual Form Editor (public/assets/template-form-schema.json)
File ini dibaca oleh InviteAround untuk merender panel form input pengguna ketika mengkustomisasi landing page. Tipe input yang didukung meliputi text, long_text, image, phone, url, dan group_array (untuk list berulang seperti layanan & portofolio):
{
"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"
}
]
}
]
}
]
}Tipe Atribut yang Didukung (TEMPLATE_ATTRIBUTE_TYPE)
InviteAround mendukung 12 tipe data atribut pada template-form-schema.json untuk mengontrol input editor dan sinkronisasi props:
| Tipe Atribut | Input Form Editor | Tipe TypeScript | Contoh Penggunaan |
|---|---|---|---|
text | Input teks satu baris | string | Nama brand, hero headline, label tombol CTA |
long_text | Textarea multi-baris | string | Deskripsi hero, paragraf tentang kami, subheadline |
number | Input angka numerik | number | Tahun pengalaman, jumlah cabang, rating angka |
date | Date picker kalender | string (YYYY-MM-DD) | Tanggal berdirinya perusahaan, promo berlaku |
datetime | Datetime-local picker | string (ISO 8601 UTC) | Waktu countdown peluncuran produk atau webinar |
image | Media uploader / picker | string (atau string[] jika multiple: true) | Logo brand, gambar hero cover, galeri foto |
music | Audio file uploader (MP3) | string | Background sound ambient |
email | Input email terintegrasi | string | Email kontak resmi, inbox reservasi |
phone | Input nomor telepon / WA | string | Nomor WhatsApp (gunakan createWhatsAppUrl) |
url | Input tautan web | string | Link Google Maps, profil LinkedIn, link eksternal |
video | Input URL video / embed | string | Video profil perusahaan YouTube atau direct MP4 |
group_array | Repeatable Group Manager | T[] (Array of Objects) | Koleksi berulang dinamis: layanan, portofolio, tim, FAQ |
Mekanisme & Cara Kerja GROUP_ARRAY
Tipe group_array memungkinkan pengguna menambah, mengubah, mengurutkan, dan menghapus item berulang dinamis di dalam landing page secara visual.
Tentukan type sebagai group_array, sertakan isRepeatableGroup: true, tentukan itemLabel untuk nama per item di UI editor, serta sediakan array fields untuk mendefinisikan sub-atribut setiap item.
Sediakan array of objects dengan properti yang sama persis seperti kunci fields pada schema. Beri id unik untuk setiap item agar rendering React stabil.
Buat interface representasi item (misal: ServiceItem) dan pasang sebagai array opsional pada interface utama CompanyProfileProps.
Gunakan helper fallbackArray(data.services) agar template tidak crash bila array bernilai null/kosong. Pasang atribut data-bind="services" pada elemen kontainer wrapper list.
// 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>
);- Tombol + Tambah Item: Pengguna dapat menambah item baru ke dalam list kapan saja.
- Drag & Drop Reorder: Urutan kartu/item dapat dipindahkan naik atau turun secara instan.
- Hapus Item: Pengguna dapat membuang item yang tidak ingin ditampilkan.
- Sub-fields Form: Setiap item memiliki form modal tersendiri sesuai definisi fields di skema.
Aturan data-bind & Visual Editor Pensil
Atribut data-bind memberitahu editor InviteAround elemen mana yang mewakili field tertentu:
- Field Teks & Gambar Tunggal: Letakkan data-bind="brandName" langsung pada elemen (misal: h1 data-bind="heroHeadline" atau img data-bind="brandLogo").
- List Berulang (Repeatable Group): Letakkan data-bind="services" pada elemen kontainer wrapper (misal: div data-bind="services").
- Komponen EditPencilOverlay: Selalu sertakan EditPencilOverlay dengan prop schema di JSX page. Komponen ini otomatis menghidupkan pensil edit ketika template dibuka di iframe editor InviteAround.
Validasi Build & Unggah ke Dashboard Partner
Sebelum mengunggah template, pastikan build statis Next.js berhasil tanpa kesalahan:
# 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!Langkah Upload ke InviteAround:
- Kompres seluruh folder proyek (termasuk app/, public/, package.json, dll.) menjadi file .zip.
- Buka Dashboard Partner → Upload Template.
- Pilih Kategori (misal: Company Profile / Landing Page).
- Unggah file screenshot thumbnail dan file .zip template Anda.