diff --git a/app/(public)/loading.tsx b/app/(public)/loading.tsx
new file mode 100644
index 0000000..41183bb
--- /dev/null
+++ b/app/(public)/loading.tsx
@@ -0,0 +1,17 @@
+/**
+ * Skeleton generik untuk route group `(public)` — fallback streaming bagi
+ * halaman yang tidak punya `loading.tsx` sendiri (beranda, profil, dll).
+ */
+export default function Loading() {
+ return (
+
+ );
+}
diff --git a/app/(public)/trips/[id]/loading.tsx b/app/(public)/trips/[id]/loading.tsx
new file mode 100644
index 0000000..94bb4cb
--- /dev/null
+++ b/app/(public)/trips/[id]/loading.tsx
@@ -0,0 +1,31 @@
+/** Skeleton halaman detail trip — tampil instan saat data masih di-fetch. */
+export default function Loading() {
+ return (
+
+
+
+
+
+
+
+
+
+
+ {Array.from({ length: 4 }).map((_, i) => (
+
+ ))}
+
+
+
+
+
+
+
+ );
+}
diff --git a/app/(public)/trips/loading.tsx b/app/(public)/trips/loading.tsx
new file mode 100644
index 0000000..7cc829c
--- /dev/null
+++ b/app/(public)/trips/loading.tsx
@@ -0,0 +1,29 @@
+/** Skeleton daftar trip — tampil instan saat list masih di-fetch. */
+export default function Loading() {
+ return (
+
+
+
+
+
+ {Array.from({ length: 6 }).map((_, i) => (
+
+ ))}
+
+
+ );
+}
diff --git a/app/manifest.ts b/app/manifest.ts
new file mode 100644
index 0000000..ec5b1a6
--- /dev/null
+++ b/app/manifest.ts
@@ -0,0 +1,31 @@
+import type { MetadataRoute } from "next";
+import { siteConfig } from "@/lib/site";
+
+/**
+ * Web app manifest — dideteksi otomatis oleh Next App Router (`` di-inject). Mendukung "Add to Home Screen" di mobile.
+ */
+export default function manifest(): MetadataRoute.Manifest {
+ return {
+ name: `${siteConfig.name} — ${siteConfig.slogan}`,
+ short_name: siteConfig.name,
+ description: siteConfig.description,
+ start_url: "/",
+ display: "standalone",
+ background_color: "#f9fafb",
+ theme_color: "#16a34a",
+ icons: [
+ {
+ src: "/images/SeTrip.png",
+ sizes: "512x512",
+ type: "image/png",
+ purpose: "any",
+ },
+ {
+ src: "/SeTrip.ico",
+ sizes: "any",
+ type: "image/x-icon",
+ },
+ ],
+ };
+}
diff --git a/app/robots.ts b/app/robots.ts
index 2595504..d35f7cf 100644
--- a/app/robots.ts
+++ b/app/robots.ts
@@ -7,7 +7,14 @@ export default function robots(): MetadataRoute.Robots {
{
userAgent: "*",
allow: "/",
- disallow: ["/api/", "/profile", "/create-trip"],
+ disallow: [
+ "/api/",
+ "/admin",
+ "/profile",
+ "/create-trip",
+ "/verify",
+ "/trips/*/payment",
+ ],
},
],
sitemap: absoluteUrl("/sitemap.xml"),
diff --git a/app/sitemap.ts b/app/sitemap.ts
index 80c8be6..f56547b 100644
--- a/app/sitemap.ts
+++ b/app/sitemap.ts
@@ -24,6 +24,12 @@ export default async function sitemap(): Promise {
changeFrequency: "hourly",
priority: 0.9,
},
+ {
+ url: absoluteUrl("/people"),
+ lastModified: now,
+ changeFrequency: "daily",
+ priority: 0.7,
+ },
{
url: absoluteUrl("/register"),
lastModified: now,
diff --git a/next.config.ts b/next.config.ts
index 289ec70..1b2b1b8 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -3,6 +3,10 @@ import type { NextConfig } from "next";
const nextConfig: NextConfig = {
images: {
dangerouslyAllowSVG: true,
+ // AVIF didahulukan — ~30% lebih kecil dari WebP, didukung browser modern.
+ formats: ["image/avif", "image/webp"],
+ // Cache hasil optimasi minimal 1 hari supaya tidak re-optimize tiap request.
+ minimumCacheTTL: 86400,
remotePatterns: [
{
protocol: "https",
diff --git a/server/services/trip.service.ts b/server/services/trip.service.ts
index 52f3aad..d8a4973 100644
--- a/server/services/trip.service.ts
+++ b/server/services/trip.service.ts
@@ -1,3 +1,4 @@
+import { cache } from "react";
import { Prisma } from "@/app/generated/prisma/client";
import type { ActivityCategory, Vibe } from "@/app/generated/prisma/enums";
import { tripRepo, type TripFilters } from "@/server/repositories/trip.repo";
@@ -38,13 +39,18 @@ export const tripService = {
return tripRepo.findAll();
},
- async getTripById(id: string) {
+ /**
+ * Ambil trip by id. Dibungkus `React.cache()` — `generateMetadata`, body
+ * halaman, dan `opengraph-image` memanggil ini untuk id yang sama dalam satu
+ * request, jadi query Prisma yang berat ini cukup jalan sekali per request.
+ */
+ getTripById: cache(async (id: string) => {
const trip = await tripRepo.findById(id);
if (!trip) {
throw new Error("Trip tidak ditemukan");
}
return trip;
- },
+ }),
async createTrip(input: CreateTripInput) {
if (isTripDepartureDayPast(input.date)) {