InviteAround/Partner Documentation

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.

Live Visual Sync

data-bind attributes and interactive pencil overlays trigger field edit modals directly.

WhatsApp Dynamic URLs

createWhatsAppUrl helper formats phone numbers and prefilled messages into click-to-chat links.

Zero Runtime Crashes

fallbackString and fallbackArray sanitizers prevent rendering errors when fields are empty.

1

Template Folder Structure

Every Next.js landing page template must adhere to the standard directory layout demonstrated in 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

Dependencies & package.json

Install required @invitearound/template-runtime package and lucide-react icon set:

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

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

Ensure package.json defines "main": "app/page.tsx" and "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

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:

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

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:

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

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:

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

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

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"
            }
          ]
        }
      ]
    }
  ]
}

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 TypeEditor Input ControlTypeScript TypeExample Usage
textSingle-line text inputstringBrand name, hero headline, CTA button label
long_textMulti-line textareastringHero description, about paragraph, subheadline
numberNumeric inputnumberYears of experience, branch count, numeric rating
dateCalendar date pickerstring (YYYY-MM-DD)Founding date, promo validity date
datetimeDatetime-local pickerstring (ISO 8601 UTC)Webinar schedule or product launch countdown
imageMedia uploader / pickerstring (or string[] if multiple: true)Brand logo, hero cover image, gallery photos
musicAudio file uploader (MP3)stringAmbient background sound
emailIntegrated email inputstringOfficial contact email, reservation inbox
phonePhone / WhatsApp inputstringWhatsApp phone number (used with createWhatsAppUrl)
urlWeb link inputstringGoogle Maps link, LinkedIn profile, external URL
videoVideo URL / embed inputstringYouTube corporate video URL or direct MP4 link
group_arrayRepeatable Group ManagerT[] (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.

1. Declaration in template-form-schema.json

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.

2. Default Structure in data.json

Provide an array of objects matching the fields defined in the schema. Always assign a unique id to each item for reliable React rendering.

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

Create an interface representing the item (e.g. ServiceItem) and declare it as an optional array on the root CompanyProfileProps interface.

4. React Implementation & fallbackArray Sanitizer

Always wrap collection with fallbackArray(data.services) to prevent crashes on null or empty arrays. Place data-bind="services" on the container wrapper element.

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. Behavior in InviteAround Visual Editor
  • + 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.
7

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

Build Validation & Upload to Partner Dashboard

Before uploading your template, verify the Next.js static build succeeds without errors:

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!

Steps to upload to InviteAround:

  1. Compress the entire project directory (including app/, public/, package.json, etc.) into a .zip file.
  2. Go to Partner Dashboard → Upload Template.
  3. Select Category (e.g. Company Profile / Landing Page).
  4. Upload your thumbnail screenshot and the template .zip file.