Documentation Index
Fetch the complete documentation index at: https://mintlify.com/gus-16710/invitations/llms.txt
Use this file to discover all available pages before exploring further.
Every invitation in this platform lives at a dedicated Next.js app-router route. The workflow is always the same: pick a category namespace, scaffold the directory, create a layout.tsx for SEO metadata, build your section components, and drop your assets into public/. The sections are client components powered by Framer Motion and assembled by a central Main.tsx that wires them into a vertical scroll-snap slider via Splide.
Choose the category
Decide which URL namespace the invitation belongs to. The available categories are:| Category | URL prefix | Example |
|---|
| Bodas | /bodas | /bodas/maria-pedro |
| Quinceañeras | /quinces | /quinces/daniela |
| Bautizos | /bautizos | /bautizos/mateo |
| Escolar | /escolar | /escolar/prepa-2025 |
| Festejos | /festejos | /festejos/anell-joanha |
Create the base directory for your invitation:mkdir -p src/app/quinces/[name]/components
Create the layout file
The layout.tsx file defines the page metadata used by search engines and social-sharing previews. Keep the structure minimal — the layout itself renders only {children}.// src/app/quinces/[name]/layout.tsx
import type { Metadata } from "next";
export const metadata: Metadata = {
title: "Daniela | Mis XV Años",
description:
"Mis 15 primaveras han llegado y lo que más me llena de emoción es pasarlo con risas a tu lado.",
openGraph: {
title: "Daniela | Mis XV Años",
description:
"Mis 15 primaveras han llegado y lo que más me llena de emoción es pasarlo con risas a tu lado.",
images: [
`https://invitaciones.unaideamas.com/img/quinces/daniela/gallery-03.jpg`,
],
},
icons: {
icon: "https://invitaciones.unaideamas.com/img/favicon.png",
},
};
export default function RootLayout({
children,
}: {
children: React.ReactNode;
}) {
return <>{children}</>;
}
Key fields to customise for each invitation:
title — shown in browser tab and link previews.
description — shown in search results and social cards.
openGraph.images — the preview photo (use a portrait gallery image).
icons.icon — use the shared platform favicon or a custom one.
Create the components directory
Inside src/app/quinces/[name]/components/ you will create at minimum these files:components/
├── Fonts.ts # Font declarations (Google + local)
├── Animations.ts # Framer Motion variant objects
├── Header.tsx # Full-screen hero section
├── Main.tsx # Assembles all sections in the Splide slider
├── Presentation.tsx # Personal message / quote section
├── Ceremony.tsx # Church / venue details
├── Reception.tsx # Reception venue details
├── GodParents.tsx # Godparent carousel
├── Gallery.tsx # Photo gallery
├── Gifts.tsx # Gift registry / links
├── Confirm.tsx # RSVP / WhatsApp confirmation
└── AudioControl.tsx # Background music toggle
Add or remove sections freely — only Header.tsx, Main.tsx, Fonts.ts, and Animations.ts are required for every invitation. Define fonts
Create components/Fonts.ts and export typed font instances using next/font/google. Here is the complete file from the Daniela invitation as a template:// src/app/quinces/daniela/components/Fonts.ts
import {
Great_Vibes,
Playfair_Display,
Pinyon_Script,
Noto_Serif_Balinese,
Roboto,
Titillium_Web,
Oswald,
Clicker_Script,
Aref_Ruqaa,
Rajdhani,
Yeseva_One,
} from "next/font/google";
export const greatVibes = Great_Vibes({ subsets: ["latin"], weight: "400" });
export const roboto = Roboto({ subsets: ["latin"], weight: ["400"] });
export const pinyion = Pinyon_Script({ subsets: ["latin"], weight: ["400"] });
export const notoSerif = Noto_Serif_Balinese({
subsets: ["latin"],
weight: ["400"],
});
export const playFair = Playfair_Display({
subsets: ["latin"],
weight: ["400", "600", "700"],
});
export const notoSans = Titillium_Web({ subsets: ["latin"], weight: "400" });
export const oswald = Oswald({ subsets: ["latin"], weight: "400" });
export const clicker = Clicker_Script({ subsets: ["latin"], weight: "400" });
export const aref = Aref_Ruqaa({ subsets: ["latin"], weight: "400" });
export const rajdhani = Rajdhani({ subsets: ["latin"], weight: "400" });
export const yaseva = Yeseva_One({ subsets: ["latin"], weight: "400" });
See Fonts & Styling for the full guide, including how to load custom .otf/.ttf files from public/fonts/. Define animations
Create components/Animations.ts with named Framer Motion variant objects. Each section imports only the variants it needs.// src/app/quinces/daniela/components/Animations.ts
export const animation01 = {
hidden: { opacity: 0, scale: 0 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: 1, type: "spring" as const, stiffness: 70, delay: 0.4 },
},
};
export const animation02 = {
hidden: { y: 100, rotate: 180, opacity: 0 },
visible: {
y: 0,
rotate: 0,
opacity: 1,
transition: { duration: 1 },
},
};
export const animation03 = {
hidden: { y: 100, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { duration: 1, delay: 0.8 },
},
};
export const animation04 = {
hidden: { y: -100, opacity: 0 },
visible: {
y: 0,
opacity: 1,
transition: { duration: 1, delay: 1 },
},
};
export const animation05 = {
hidden: { scale: 2, opacity: 0 },
visible: {
scale: 1,
opacity: 1,
transition: { duration: 2 },
},
};
export const animation06 = {
hidden: { opacity: 0, scale: 0 },
visible: {
opacity: 1,
scale: 1,
transition: { duration: 1, delay: 0.6 },
},
};
Use animation01–animation06 as-is or add your own numbered variants following the same pattern. Create Main.tsx
Main.tsx is the heart of the invitation. It imports every section component and renders them as individual slides inside a Splide vertical slider, with AudioControl floating above. It also shows a brief swipe-hint modal on first mount, then fades in the slider once the hint dismisses.// src/app/quinces/daniela/components/Main.tsx
import { useEffect, useState } from "react";
// @ts-ignore
import { Splide, SplideSlide } from "@splidejs/react-splide";
import Header from "./Header";
import Presentation from "./Presentation";
import Ceremony from "./Ceremony";
import Reception from "./Reception";
import GodParents from "./GodParents";
import Gallery from "./Gallery";
import Gifts from "./Gifts";
import Confirm from "./Confirm";
import { Modal, ModalBody, ModalContent, useDisclosure } from "@nextui-org/react";
import { motion } from "framer-motion";
import { oswald } from "./Fonts";
import "@splidejs/react-splide/css";
import { animation05 } from "./Animations";
import AudioControl from "./AudioControl";
const ModalInstructions = ({
isOpen,
onOpenChange,
}: {
isOpen: boolean;
onOpenChange: () => void;
}) => (
<Modal
isOpen={isOpen}
onOpenChange={onOpenChange}
size="xs"
placement="center"
backdrop="blur"
className="bg-white/0 shadow-none"
hideCloseButton={true}
isDismissable={false}
>
<ModalContent>
{() => (
<ModalBody>
{/* animated swipe arrow SVG */}
<p className={`${oswald.className} text-white text-2xl text-center mt-5`}>
DESLIZA PARA VER EL CONTENIDO
</p>
</ModalBody>
)}
</ModalContent>
</Modal>
);
export default function Main() {
const { isOpen, onOpen, onOpenChange, onClose } = useDisclosure();
const [open, setOpen] = useState(false);
useEffect(() => {
onOpen();
setTimeout(() => {
onClose();
setOpen(true);
}, 2000);
}, []);
return (
<div className="max-w-3xl m-auto bg-[url('/img/quinces/[name]/background-main.jpg')] bg-center bg-cover bg-fixed shadow-large">
{open && (
<motion.div
initial={{ opacity: 0 }}
animate={{ opacity: 1 }}
transition={{ duration: 1 }}
className="relative overflow-clip"
>
<Splide
aria-label="[Name]"
options={{
rewind: true,
direction: "ttb",
height: "100svh",
wheel: true,
releaseWheel: true,
type: "loop",
waitForTransition: true,
arrows: false,
classes: {
page: "splide__pagination__page custom-class-page",
},
}}
className="z-10"
>
<SplideSlide><Header /></SplideSlide>
<SplideSlide><Presentation /></SplideSlide>
<SplideSlide><Ceremony /></SplideSlide>
<SplideSlide><Reception /></SplideSlide>
<SplideSlide><GodParents /></SplideSlide>
<SplideSlide><Gallery /></SplideSlide>
<SplideSlide><Gifts /></SplideSlide>
<SplideSlide><Confirm /></SplideSlide>
</Splide>
<AudioControl />
<motion.div
className="bg-[url('/img/quinces/[name]/header-01.png')] bg-cover bg-bottom absolute inset-0 z-0"
variants={animation05}
initial="hidden"
whileInView="visible"
/>
<motion.div
className="bg-[url('/img/quinces/[name]/header-02.png')] bg-cover bg-bottom absolute inset-0 z-0"
variants={animation05}
initial="hidden"
whileInView="visible"
/>
</motion.div>
)}
<ModalInstructions isOpen={isOpen} onOpenChange={onOpenChange} />
</div>
);
}
Key points:
ModalInstructions is an internal modal that auto-dismisses after 2 seconds, prompting the guest to swipe.
- The
open state gate ({open && ...}) ensures the Splide slider only mounts after the hint dismisses.
classes.page passes a custom CSS class to Splide pagination dots, styled via styles.css.
- The two decorative
motion.div overlays (header-01.png, header-02.png) sit behind the slider at z-0.
- Adjust the
SplideSlide list to match the sections you created. Remove slides for sections you skipped.
Create page.tsx
page.tsx is the entry point for the route. It renders an opening modal (ModalOpening) that shows the guest a preview of the invitation name, then — once they tap “Ver Invitación” — mounts <Main />. It also imports ./styles.css for per-invitation overrides.// src/app/quinces/[name]/page.tsx
"use client";
import { motion } from "framer-motion";
import {
Button, Modal, ModalBody, ModalContent,
ModalFooter, ModalHeader, useDisclosure,
} from "@nextui-org/react";
import { useEffect, useState, Dispatch, SetStateAction } from "react";
import { pinyion, playFair } from "./components/Fonts";
import { animation01, animation03 } from "./components/Animations";
import "./styles.css";
import Main from "./components/Main";
const ModalOpening = ({
isOpen, onOpenChange, setOpen,
}: {
isOpen: boolean;
onOpenChange: () => void;
setOpen: Dispatch<SetStateAction<boolean>>;
}) => (
<Modal isOpen={isOpen} onOpenChange={onOpenChange}
size="xs" placement="center" backdrop="blur"
isDismissable={false} hideCloseButton={true}>
<ModalContent>
{(onClose) => (
<>
<ModalHeader className="flex flex-col gap-1">
<motion.h1
className={`${playFair.className} text-2xl flex items-center justify-center text-zinc-600 z-20`}
variants={animation01} initial="hidden" whileInView="visible"
>
MIS <span className="text-5xl text-yellow-400">XV</span> AÑOS
</motion.h1>
</ModalHeader>
<ModalBody>
<motion.p
className={`${pinyion.className} flex justify-center text-7xl text-yellow-400 z-20`}
style={{ textShadow: "0px 1px 1px rgba(255,255,255, 1)" }}
variants={animation03} initial="hidden" whileInView="visible"
>
Daniela
</motion.p>
</ModalBody>
<ModalFooter className="flex justify-center">
<Button color="warning" variant="bordered"
onPress={() => { setOpen(true); onClose(); }}>
Ver Invitación
</Button>
</ModalFooter>
</>
)}
</ModalContent>
</Modal>
);
export default function Fifteen() {
const [open, setOpen] = useState(false);
const { isOpen, onOpen, onOpenChange } = useDisclosure();
useEffect(() => { onOpen(); }, []);
return (
<main className="background-class">
{open && <Main />}
<ModalOpening isOpen={isOpen} onOpenChange={onOpenChange} setOpen={setOpen} />
</main>
);
}
Add assets
Place image assets in public/img/[category]/[name]/ and any custom font files in public/fonts/. See Assets & Images for full naming conventions and sizing recommendations.public/
├── fonts/
│ └── MyCustomFont.otf
└── img/
└── quinces/
└── [name]/
├── header.jpg
├── background-main.jpg
├── gallery-01.jpg
├── gallery-02.jpg
└── preview.jpg
Run the dev server
Start the local development server and open your invitation:Then visit:http://localhost:3000/quinces/[name]
The opening modal will appear immediately. Tap “Ver Invitación” to enter the scrollable invitation. Use your browser’s device-simulation mode to preview at mobile widths (375–430 px), since the layout is optimised for portrait phones.
The fastest way to create a new invitation is to duplicate an existing one that shares the same event type and color palette, then replace the asset paths, text strings, venue details, and font selections. The Daniela invitation (src/app/quinces/daniela/) is a well-structured reference with all standard sections.
All <Image> components from next/image must use the unoptimized prop. This is configured globally in next.config.js via images: { unoptimized: true } — required because the platform is a static export and the built-in Next.js image optimisation API is not available on static hosts.