Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 10 additions & 12 deletions .agent/CURRENT.md
Original file line number Diff line number Diff line change
@@ -1,14 +1,12 @@
# Current focus

**Status:** in-progress
**Working on:** Migrating the registration backend from **Google Apps Script → Google Forms direct submit**. Replacing `GOOGLE_SCRIPT_URL` + Apps Script `doPost` with `GOOGLE_FORM_ID` + per-field `GOOGLE_FORM_ENTRY_*` IDs that POST straight to `formResponse`. Motivation: no OAuth, no Apps Script deploy dance, instructors can still see submissions in the Form's linked Sheet.
**Next step:** (1) finish editing `app/api/register/route.ts` to POST to `https://docs.google.com/forms/d/e/{GOOGLE_FORM_ID}/formResponse` with the entry-ID map, (2) rename / delete the stale `SETUP_GOOGLE_SHEETS.md`, (3) manually test one submission end-to-end, (4) ship.

### Uncommitted changes in working tree
- `.env.example` — migrated to `GOOGLE_FORM_ID` / `GOOGLE_FORM_ENTRY_*` vars
- `app/api/register/route.ts` — migration in progress (still check `git diff`)
- `SETUP_GOOGLE_FORMS.md` — new setup walkthrough (untracked)

When this ships, write an ADR at `.agent/decisions/YYYY-MM-DD-google-forms-backend.md` marking `decisions/2026-04-24-google-sheets-backend.md` as superseded, and append a JOURNAL entry.

Security maintenance: Next.js and its matching ESLint config are updated to16.3.8 for six Next.js advisories. Strict clean install, lint and production build pass. Unpatched braces in lint tooling remains separately unresolved.
**Status:** ready-to-ship
**Working on:** Added **AEO (AI Engine Optimization) & Blog System** to `eltango.vercel.app`:
1. `app/robots.ts`: Opened crawling to all AI bots (`GPTBot`, `ClaudeBot`, `Google-Extended`, `PerplexityBot`, `Yeti`, etc.).
2. `app/sitemap.ts`: Dynamic sitemap for static pages and AEO blog routes.
3. `components/json-ld.tsx`: Schema.org `DanceSchool` structured data for Gangnam studio, Director In-kyung Lee, 2023 Rome champion achievements, and links.
4. `app/[locale]/blog/`: Static blog index and individual post pages (`[slug]`) with `FAQPage` JSON-LD schema for AI answer engine citation.
5. `lib/blog-data.ts`: Initial 4 killer AEO articles (Cruise 2027 tango, shoes comparison, lesson costs, posture & wellness).
6. Updated metadata in `messages/ko.json` with keyword-rich titles and descriptions.

**Next step:** Push to `main` branch on GitHub (`minjungsung/eltango`) to trigger automatic Vercel production deployment.
156 changes: 156 additions & 0 deletions app/[locale]/blog/[slug]/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
import React from 'react';
import Link from 'next/link';
import { notFound } from 'next/navigation';
import { blogPosts } from '@/lib/blog-data';
import { Metadata } from 'next';

export function generateStaticParams() {
return blogPosts.map((post) => ({
slug: post.slug,
}));
}

export async function generateMetadata({
params,
}: {
params: Promise<{ locale: string; slug: string }>;
}): Promise<Metadata> {
const { slug } = await params;
const post = blogPosts.find((p) => p.slug === slug);
if (!post) return {};

return {
title: `${post.title} | 엘땅고 강남본점`,
description: post.description,
keywords: post.tags,
openGraph: {
title: post.title,
description: post.description,
type: 'article',
url: `https://eltango.vercel.app/ko/blog/${post.slug}`,
publishedTime: post.date,
authors: ['이인경 대표원장'],
},
};
}

export default async function BlogPostPage({
params,
}: {
params: Promise<{ locale: string; slug: string }>;
}) {
const { locale, slug } = await params;
const post = blogPosts.find((p) => p.slug === slug);

if (!post) {
notFound();
}

const faqSchema = post.faq
? {
'@context': 'https://schema.org',
'@type': 'FAQPage',
mainEntity: post.faq.map((item) => ({
'@type': 'Question',
name: item.question,
acceptedAnswer: {
'@type': 'Answer',
text: item.answer,
},
})),
}
: null;

return (
<article className="min-h-screen bg-background py-16 px-4 sm:px-6 lg:px-8">
{faqSchema && (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(faqSchema) }}
/>
)}

<div className="max-w-3xl mx-auto">
<div className="mb-8">
<Link
href={`/${locale}/blog`}
className="text-xs text-muted-foreground hover:text-primary transition-colors"
>
← 전체 칼럼 목록
</Link>
<div className="flex items-center gap-2 mt-4">
<span className="text-xs font-semibold px-2.5 py-0.5 rounded-full bg-primary/20 text-primary">
{post.category}
</span>
<time className="text-xs text-muted-foreground">{post.date}</time>
<span className="text-xs text-muted-foreground">· 작성자: 이인경 대표원장</span>
</div>
<h1 className="text-2xl sm:text-3xl md:text-4xl font-bold tracking-tight text-foreground mt-3 leading-snug">
{post.title}
</h1>
</div>

<div className="glass-card rounded-2xl p-6 sm:p-8 mb-10 text-foreground/90 leading-relaxed text-sm sm:text-base space-y-4 whitespace-pre-line">
{post.content}
</div>

{post.faq && post.faq.length > 0 && (
<div className="mt-12 mb-10">
<h3 className="text-xl font-bold text-foreground mb-6 flex items-center gap-2">
<span>❓</span> 자주 묻는 질문 (FAQ)
</h3>
<div className="space-y-4">
{post.faq.map((f, idx) => (
<div key={idx} className="glass-card rounded-xl p-5">
<p className="font-semibold text-primary text-sm sm:text-base mb-2">
Q. {f.question}
</p>
<p className="text-xs sm:text-sm text-muted-foreground leading-relaxed">
A. {f.answer}
</p>
</div>
))}
</div>
</div>
)}

{/* CTA Card */}
<div className="glass-card rounded-2xl p-8 text-center my-12 border-primary/40 bg-primary/5">
<h3 className="text-xl font-bold text-primary mb-2">
엘땅고 강남본점에서 우아한 라이프스타일을 시작해 보세요
</h3>
<p className="text-xs sm:text-sm text-muted-foreground mb-6">
17년 전통 · 2023 로마 챔피언 직강 · 신논현역 도보 3분 · 온누리상품권 10% 할인
</p>
<div className="flex flex-wrap justify-center gap-3">
<a
href="https://dulcenaleo.com/eltango"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center font-medium bg-primary text-primary-foreground hover:opacity-90 h-10 px-6 rounded-md text-sm"
>
2027 크루즈 & 클래스 신청하기
</a>
<a
href="https://m.place.naver.com/place/20526245"
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center justify-center font-medium border border-primary text-primary hover:bg-primary/10 h-10 px-6 rounded-md text-sm"
>
네이버 플레이스 정보 보기
</a>
</div>
</div>

<div className="border-t border-border/30 pt-6 flex justify-between items-center text-xs text-muted-foreground">
<Link href={`/${locale}/blog`} className="hover:text-primary">
← 목록으로
</Link>
<Link href={`/${locale}`} className="hover:text-primary">
엘땅고 메인 홈 →
</Link>
</div>
</div>
</article>
);
}
79 changes: 79 additions & 0 deletions app/[locale]/blog/page.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
import React from 'react';
import Link from 'next/link';
import { blogPosts } from '@/lib/blog-data';
import { Metadata } from 'next';

export const metadata: Metadata = {
title: '엘땅고 아르헨티나 탱고 칼럼 & AEO 가이드 | 강남본점',
description: '크루즈 선상 파티 댄스, 4050 척추 교정 웰니스, 탱고 슈즈 선택법, 수강료 및 기간 총정리. 24년 경력 이인경 원장의 전문 칼럼.',
};

export default async function BlogIndexPage({
params,
}: {
params: Promise<{ locale: string }>;
}) {
const { locale } = await params;

return (
<div className="min-h-screen bg-background py-16 px-4 sm:px-6 lg:px-8">
<div className="max-w-4xl mx-auto">
<div className="text-center mb-12">
<p className="text-sm font-serif italic text-primary mb-2">EL TANGO Column & Guide</p>
<h1 className="text-3xl sm:text-4xl font-bold tracking-tight text-foreground">
아르헨티나 탱고 전문 칼럼 & 입문 가이드
</h1>
<p className="mt-4 text-muted-foreground text-sm sm:text-base">
24년 차 챔피언 이인경 원장이 전하는 탱고의 품격, 체형 교정 웰니스, 2027 크루즈 여행 준비
</p>
</div>

<div className="grid gap-6">
{blogPosts.map((post) => (
<article
key={post.slug}
className="glass-card rounded-2xl p-6 transition-all hover:border-primary/50"
>
<div className="flex items-center gap-2 mb-2">
<span className="text-xs font-semibold px-2.5 py-0.5 rounded-full bg-primary/20 text-primary">
{post.category}
</span>
<time className="text-xs text-muted-foreground">{post.date}</time>
</div>
<h2 className="text-xl font-bold text-foreground hover:text-primary transition-colors mb-2">
<Link href={`/${locale}/blog/${post.slug}`}>{post.title}</Link>
</h2>
<p className="text-sm text-muted-foreground line-clamp-2 mb-4 leading-relaxed">
{post.description}
</p>
<div className="flex items-center justify-between">
<div className="flex flex-wrap gap-1.5">
{post.tags.map((tag) => (
<span key={tag} className="text-xs text-muted-foreground/80">
#{tag}
</span>
))}
</div>
<Link
href={`/${locale}/blog/${post.slug}`}
className="text-xs font-medium text-primary hover:underline"
>
칼럼 읽기 →
</Link>
</div>
</article>
))}
</div>

<div className="mt-16 text-center border-t border-border/30 pt-8">
<Link
href={`/${locale}`}
className="inline-flex items-center justify-center rounded-md text-sm font-medium border bg-background hover:bg-foreground/10 h-10 px-6 text-foreground"
>
← 엘땅고 메인으로 돌아가기
</Link>
</div>
</div>
</div>
);
}
4 changes: 4 additions & 0 deletions app/[locale]/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { notFound } from "next/navigation";
import { ThemeProvider } from "@/components/theme-provider";
import { Navbar } from "@/components/navbar";
import { MobileCTA } from "@/components/mobile-cta";
import { JsonLd } from "@/components/json-ld";
import { routing } from "@/i18n/routing";
import { Playfair_Display } from "next/font/google";

Expand Down Expand Up @@ -80,6 +81,9 @@ export default async function LocaleLayout({

return (
<html lang={locale} suppressHydrationWarning>
<head>
<JsonLd />
</head>
<body className={`${playfair.variable} font-sans antialiased`}>
<NextIntlClientProvider>
<ThemeProvider attribute="class" forcedTheme="dark">
Expand Down
25 changes: 25 additions & 0 deletions app/robots.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { MetadataRoute } from 'next';

export default function robots(): MetadataRoute.Robots {
return {
rules: [
{
userAgent: '*',
allow: '/',
},
{
userAgent: [
'GPTBot',
'ChatGPT-User',
'ClaudeBot',
'Google-Extended',
'PerplexityBot',
'Applebot-Extended',
'Yeti',
],
allow: '/',
},
],
sitemap: 'https://eltango.vercel.app/sitemap.xml',
};
}
37 changes: 37 additions & 0 deletions app/sitemap.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { MetadataRoute } from 'next';
import { blogPosts } from '@/lib/blog-data';

export default function sitemap(): MetadataRoute.Sitemap {
const baseUrl = 'https://eltango.vercel.app';
const lastModified = new Date();

const staticRoutes = [
{
url: `${baseUrl}/ko`,
lastModified,
changeFrequency: 'weekly' as const,
priority: 1.0,
},
{
url: `${baseUrl}/en`,
lastModified,
changeFrequency: 'weekly' as const,
priority: 0.8,
},
{
url: `${baseUrl}/ko/blog`,
lastModified,
changeFrequency: 'daily' as const,
priority: 0.9,
},
];

const blogRoutes = blogPosts.map((post) => ({
url: `${baseUrl}/ko/blog/${post.slug}`,
lastModified: new Date(post.date),
changeFrequency: 'monthly' as const,
priority: 0.8,
}));

return [...staticRoutes, ...blogRoutes];
}
55 changes: 55 additions & 0 deletions components/json-ld.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import React from 'react';

export function JsonLd() {
const schema = {
'@context': 'https://schema.org',
'@type': 'DanceSchool',
name: '엘땅고 강남본점 (El Tango)',
alternateName: '서울탱고아카데미',
description: '17년 전통 강남 아르헨티나 탱고 전문 아카데미. 2023 로마 세계대회 챔피언 이인경 원장 직강. 성인 직장인 취미, 5060 크루즈 선상 파티 대비반, 원데이 체험 클래스.',
url: 'https://eltango.vercel.app',
telephone: '+82-10-2415-0563',
address: {
'@type': 'PostalAddress',
streetAddress: '서초구 주흥길 12 2층',
addressLocality: '서초구',
addressRegion: '서울특별시',
postalCode: '06544',
addressCountry: 'KR',
},
geo: {
'@type': 'GeoCoordinates',
latitude: 37.5055,
longitude: 127.0245,
},
openingHoursSpecification: [
{
'@type': 'OpeningHoursSpecification',
dayOfWeek: ['Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday', 'Sunday'],
opens: '10:00',
closes: '23:00',
},
],
priceRange: '₩₩',
founder: {
'@type': 'Person',
name: '이인경',
jobTitle: '대표원장',
description: '24년 경력 탱고 마에스트라, 전 KBS 방송작가, 2023 로마 세계탱고대회 챔피언',
},
sameAs: [
'https://m.place.naver.com/place/20526245',
'https://cafe.naver.com/eltango2009',
'https://www.instagram.com/eltango_seoultangoacademy',
'https://www.youtube.com/@seoultango',
'https://dulcenaleo.com/eltango',
],
};

return (
<script
type="application/ld+json"
dangerouslySetInnerHTML={{ __html: JSON.stringify(schema) }}
/>
);
}
Loading
Loading