InviteAround/Dokumentasi Partner

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.

Live Visual Sync

Atribut data-bind dan overlay pensil interaktif langsung membuka modal edit field.

WhatsApp Dynamic URLs

Helper createWhatsAppUrl mengonversi nomor WhatsApp & konteks layanan jadi click-to-chat.

Zero Runtime Crashes

Sanitizer fallbackString & fallbackArray memastikan tampilan aman bila field kosong.

1

Struktur Folder Template

Setiap template landing page Next.js harus mengikuti struktur folder baku seperti template referensi cold-slate:

BASH
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.json
2

Instalasi Dependency & package.json

Instal library wajib @invitearound/template-runtime dan icon set lucide-react:

BASH
# Menggunakan Bun (disarankan)
bun add @invitearound/template-runtime lucide-react

# Menggunakan npm
npm install @invitearound/template-runtime lucide-react

Pastikan package.json memiliki properti "main": "app/page.tsx" dan "types": "app/page.tsx":

JSON
{
  "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"
  }
}
3

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:

TYPESCRIPT
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;
4

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:

TYPESCRIPT
// 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;
}
5

Implementasi Halaman Utama (app/page.tsx)

Gunakan hook useAutoTemplateData, tambahkan atribut data-bind="namaKey" pada setiap elemen visual, dan pasang EditPencilOverlay di akhir dokumen:

TSX
'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>
  );
}
6

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):

JSON
{
  "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 AtributInput Form EditorTipe TypeScriptContoh Penggunaan
textInput teks satu barisstringNama brand, hero headline, label tombol CTA
long_textTextarea multi-barisstringDeskripsi hero, paragraf tentang kami, subheadline
numberInput angka numeriknumberTahun pengalaman, jumlah cabang, rating angka
dateDate picker kalenderstring (YYYY-MM-DD)Tanggal berdirinya perusahaan, promo berlaku
datetimeDatetime-local pickerstring (ISO 8601 UTC)Waktu countdown peluncuran produk atau webinar
imageMedia uploader / pickerstring (atau string[] jika multiple: true)Logo brand, gambar hero cover, galeri foto
musicAudio file uploader (MP3)stringBackground sound ambient
emailInput email terintegrasistringEmail kontak resmi, inbox reservasi
phoneInput nomor telepon / WAstringNomor WhatsApp (gunakan createWhatsAppUrl)
urlInput tautan webstringLink Google Maps, profil LinkedIn, link eksternal
videoInput URL video / embedstringVideo profil perusahaan YouTube atau direct MP4
group_arrayRepeatable Group ManagerT[] (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.

1. Deklarasi di template-form-schema.json

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.

2. Struktur default di data.json

Sediakan array of objects dengan properti yang sama persis seperti kunci fields pada schema. Beri id unik untuk setiap item agar rendering React stabil.

3. Definisi Type Interface (types/index.ts)

Buat interface representasi item (misal: ServiceItem) dan pasang sebagai array opsional pada interface utama CompanyProfileProps.

4. Implementasi di React & Sanitasi fallbackArray

Gunakan helper fallbackArray(data.services) agar template tidak crash bila array bernilai null/kosong. Pasang atribut data-bind="services" pada elemen kontainer wrapper list.

TYPESCRIPT
// 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>
);
5. Perilaku di Editor Visual InviteAround
  • 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.
7

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.
8

Validasi Build & Unggah ke Dashboard Partner

Sebelum mengunggah template, pastikan build statis Next.js berhasil tanpa kesalahan:

BASH
# 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:

  1. Kompres seluruh folder proyek (termasuk app/, public/, package.json, dll.) menjadi file .zip.
  2. Buka Dashboard Partner → Upload Template.
  3. Pilih Kategori (misal: Company Profile / Landing Page).
  4. Unggah file screenshot thumbnail dan file .zip template Anda.