Skip to content
Merged
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
2 changes: 1 addition & 1 deletion .husky/commit-msg
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
pnpm commitlint --edit "$1" || {
echo
echo "❌ 컀밋 λ©”μ‹œμ§€ κ·œμΉ™ μœ„λ°˜!"
echo " ν˜•μ‹: CDP-숫자 type이λͺ¨μ§€(scope): subject"
echo " ν˜•μ‹: CDP-숫자 type이λͺ¨μ§€ (scope): subject"
echo " μ˜ˆμ‹œ: CDP-31 fixπŸ›(ci): CI error resolution"
echo " ν—ˆμš©: feat✨, fixπŸ›, refactorπŸ”¨, style🎨, choreβš™οΈ, docsπŸ“, testπŸ§ͺ"
exit 1
Expand Down
5 changes: 2 additions & 3 deletions commitlint.config.cjs
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
/** commitlint.config.cjs */
module.exports = {
extends: ["@commitlint/config-conventional"],
parserPreset: {
parserOpts: {
// CDP-123 choreβš™οΈ(scope): subject
// CDP-123 choreβš™οΈ (scope): subject
headerPattern:
/^(CDP-\d+)\s(feat✨|fixπŸ›|refactorπŸ”¨|style🎨|choreβš™οΈ|docsπŸ“|testπŸ§ͺ)\(([^)]+)\):\s(.+)$/,
/^(CDP-\d+)\s(feat✨|fixπŸ›|refactorπŸ”¨|style🎨|choreβš™οΈ|docsπŸ“|testπŸ§ͺ)\s\(([^)]+)\):\s(.+)$/,
headerCorrespondence: ["ticket", "type", "scope", "subject"],
},
},
Expand Down
48 changes: 48 additions & 0 deletions next-sitemap.config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
const RAW_SITE_URL = process.env.SITE_URL ?? "https://myplanmate.vercel.app";
const siteUrl = RAW_SITE_URL.replace(/\/+$/, ""); // 끝 μŠ¬λž˜μ‹œ 제거

// "/" 이외 경둜의 끝 μŠ¬λž˜μ‹œ 제거
const strip = (p) => (p !== "/" && p.endsWith("/") ? p.slice(0, -1) : p);

const config = {
siteUrl,
Comment on lines +1 to +8

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Sitemap uses SITE_URL only and ignores NEXT_PUBLIC_SITE_URL

The sitemap configuration reads process.env.SITE_URL exclusively. Other modules (e.g. src/seo/constants.ts) still support NEXT_PUBLIC_SITE_URL, which was the only variable required before this commit. Environments that set only the public variable will now fall back to the hardcoded https://myplanmate.vercel.app, producing sitemaps and robots.txt pointing at the wrong domain. Consider checking both env names to keep generated URLs consistent with the rest of the app.

Useful? React with πŸ‘Β / πŸ‘Ž.

generateRobotsTxt: true,
outDir: "public",
sitemapSize: 5000,

// κΈ°μ‘΄ exclude κ·ΈλŒ€λ‘œ μœ μ§€
exclude: ["/api/*", "/admin/*", "/debug", "/lab/*"],

// <loc>이 canonicalκ³Ό 1:1둜 λ™μΌν•˜λ„λ‘ μ •κ·œν™”
transform: async (cfg, path) => {
const loc = strip(path);

// μš°μ„ μˆœμœ„ κ·œμΉ™
const priority = loc === "/" ? 1.0 : loc.startsWith("/blog") ? 0.8 : (cfg.priority ?? 0.7);

return {
loc, // βœ… canonicalκ³Ό λ™μΌν•œ λ¬Έμžμ—΄
changefreq: "daily",
priority,
lastmod: new Date().toISOString(),

// 언어별 νŽ˜μ΄μ§€κ°€ μžˆλ‹€λ©΄ 경둜 λ‹¨μœ„λ‘œ alternateRefs λ§€ν•‘
// 예) /todos β†’ /ko/todos, /en/todos
alternateRefs: [
{ href: `${siteUrl}/ko${loc === "/" ? "" : loc}`, hreflang: "ko" },
{ href: `${siteUrl}/en${loc === "/" ? "" : loc}`, hreflang: "en" },
],
};
},

// robots.txt
robotsTxtOptions: {
policies: [
{ userAgent: "*", allow: "/" },
{ userAgent: "*", disallow: ["/api/", "/admin/", "/debug", "/lab/"] },
],
additionalSitemaps: [`${siteUrl}/server-sitemap.xml`], // 동적 sitemap
},
};

export default config;
36 changes: 28 additions & 8 deletions next.config.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
// next.config.ts
import type { NextConfig } from "next";

/** λŒ€ν‘œ 도메인 (μ—†μœΌλ©΄ Vercel 프리뷰 κΈ°λ³Έκ°’) */
const isProd = process.env.NODE_ENV === "production";
const RAW = process.env.SITE_URL ?? "https://myplanmate.vercel.app";
const ORIGIN = new URL(RAW);
const NON_WWW_HOST = ORIGIN.hostname.replace(/^www\./, "");
const WWW_HOST = ORIGIN.hostname.startsWith("www.") ? ORIGIN.hostname : `www.${NON_WWW_HOST}`;
const DEST_ORIGIN = `${ORIGIN.protocol}//${NON_WWW_HOST}`;

const nextConfig: NextConfig = {
reactStrictMode: true,
poweredByHeader: false, // x-powered-by: Next.js 헀더 제거
Expand Down Expand Up @@ -29,14 +38,8 @@ const nextConfig: NextConfig = {
{
source: "/:path*",
headers: [
{
key: "X-Frame-Options",
value: "DENY", // ν΄λ¦­μž¬ν‚Ή λ°©μ§€
},
{
key: "X-Content-Type-Options",
value: "nosniff", // MIME νƒ€μž… μŠ€λ‹ˆν•‘ λ°©μ§€
},
{ key: "X-Frame-Options", value: "DENY" }, // ν΄λ¦­μž¬ν‚Ή λ°©μ§€
{ key: "X-Content-Type-Options", value: "nosniff" }, // MIME νƒ€μž… μŠ€λ‹ˆν•‘ λ°©μ§€
{
key: "Referrer-Policy",
value: "strict-origin-when-cross-origin", // μ•ˆμ „ν•œ referrer 전솑
Expand All @@ -49,6 +52,23 @@ const nextConfig: NextConfig = {
},
];
},

// βœ… μŠ¬λž˜μ‹œ μ •μ±…: λ¬΄μŠ¬λž˜μ‹œ
trailingSlash: false,

async redirects() {
// ⚠️ dev λͺ¨λ“œ(HMR)μ—μ„œλŠ” λΉ„ν™œμ„±ν™” (루프 λ°©μ§€)
if (!isProd) return [];

return [
{
source: "/:path*",
has: [{ type: "host", value: WWW_HOST }],
destination: `${DEST_ORIGIN}/:path*`,
permanent: false, // 검증 ν›„ true둜 승격
},
];
},
};

export default nextConfig;
3 changes: 3 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@
"dev": "next dev",
"build": "next build",
"start": "next start",
"postbuild": "next-sitemap",
"sitemap": "next-sitemap",
"lint": "eslint . --ext .js,.jsx,.ts,.tsx --max-warnings=0",
"lint:fix": "pnpm lint --fix",
"format": "prettier --write .",
Expand Down Expand Up @@ -55,6 +57,7 @@
"eslint-plugin-prettier": "^5.5.4",
"husky": "^9.1.7",
"lint-staged": "^16.2.0",
"next-sitemap": "^4.2.3",
"prettier": "^3.6.2",
"prettier-plugin-tailwindcss": "^0.6.14",
"tailwindcss": "^4",
Expand Down
37 changes: 37 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

17 changes: 17 additions & 0 deletions public/robots.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
# *
User-agent: *
Allow: /

# *
User-agent: *
Disallow: /api/
Disallow: /admin/
Disallow: /debug
Disallow: /lab/

# Host
Host: https://myplanmate.vercel.app

# Sitemaps
Sitemap: https://myplanmate.vercel.app/sitemap.xml
Sitemap: https://myplanmate.vercel.app/server-sitemap.xml
4 changes: 4 additions & 0 deletions public/sitemap-0.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<?xml version="1.0" encoding="UTF-8"?>
<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9" xmlns:news="http://www.google.com/schemas/sitemap-news/0.9" xmlns:xhtml="http://www.w3.org/1999/xhtml" xmlns:mobile="http://www.google.com/schemas/sitemap-mobile/1.0" xmlns:image="http://www.google.com/schemas/sitemap-image/1.1" xmlns:video="http://www.google.com/schemas/sitemap-video/1.1">
<url><loc>https://myplanmate.vercel.app</loc><lastmod>2025-10-21T17:20:15.421Z</lastmod><changefreq>daily</changefreq><priority>1</priority><xhtml:link rel="alternate" hreflang="ko" href="https://myplanmate.vercel.app/ko"/><xhtml:link rel="alternate" hreflang="en" href="https://myplanmate.vercel.app/en"/></url>
</urlset>
5 changes: 5 additions & 0 deletions public/sitemap.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
<?xml version="1.0" encoding="UTF-8"?>
<sitemapindex xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">
<sitemap><loc>https://myplanmate.vercel.app/sitemap-0.xml</loc></sitemap>
<sitemap><loc>https://myplanmate.vercel.app/server-sitemap.xml</loc></sitemap>
</sitemapindex>
22 changes: 11 additions & 11 deletions src/app/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,12 +9,12 @@ import {
LOCALE,
OG_DEFAULT_IMAGE,
SITE_NAME,
SITE_URL,
SITE_URL, // βœ… constantsμ—μ„œ SITE_URL μ‚¬μš©
TITLE_TEMPLATE,
} from "@/seo/constants";

export const metadata: Metadata = {
// μ ˆλŒ€ URL 기쀀점 (canonical/OG μ ˆλŒ€κ²½λ‘œ λ³€ν™˜μ— μ‚¬μš©)
// βœ… μ ˆλŒ€ URL 기쀀점 (canonical / OG μ ˆλŒ€κ²½λ‘œ λ³€ν™˜μš©)
metadataBase: new URL(SITE_URL),

// μ „μ—­ 타이틀 κ·œμΉ™
Expand Down Expand Up @@ -48,6 +48,15 @@ export const metadata: Metadata = {
twitter: {
card: "summary_large_image",
},

// canonical 및 언어별 hreflang
alternates: {
canonical: "/", // => https://myplanmate.vercel.app/
languages: {
ko: "/ko", // => https://myplanmate.vercel.app/ko
en: "/en", // => https://myplanmate.vercel.app/en
},
Comment on lines +52 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Avoid hreflang links to non-existent /ko and /en routes

Layout metadata advertises alternate language URLs (/ko and /en), but the project has no corresponding locale directories or i18n routing. Every rendered page will emit <link rel="alternate" hreflang> elements that lead to 404s, which search engines treat as errors and can harm canonicalization. Remove these alternates or implement the localized routes before exposing them.

Useful? React with πŸ‘Β / πŸ‘Ž.

},
};

export default function RootLayout({ children }: { children: React.ReactNode }) {
Expand All @@ -60,15 +69,6 @@ export default function RootLayout({ children }: { children: React.ReactNode })
- λͺ¨λ“  ν•˜μœ„ μ»΄ν¬λ„ŒνŠΈκ°€ λ™μΌν•œ client & cache 곡유
- useQuery, useMutation 훅이 μ–΄λ””μ„œλ“  정상 λ™μž‘
*/}
{/*
βœ… μΆ”ν›„ ν™•μž₯ μ˜ˆμ‹œ:
<AuthProvider>
<ThemeProvider>
<Providers>{children}</Providers>
</ThemeProvider>
</AuthProvider>
*/}

<Providers>{children}</Providers>
</body>
</html>
Expand Down
2 changes: 1 addition & 1 deletion src/seo/baseUrl.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
export const getBaseUrl = () => {
const fromEnv = process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "");
const fromEnv = process.env.SITE_URL?.replace(/\/$/, "");
return fromEnv ?? "http://localhost:3000";
};

Expand Down
2 changes: 1 addition & 1 deletion src/seo/constants.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
export const SITE_NAME = "MyPlanMate";

export const SITE_URL =
process.env.NEXT_PUBLIC_SITE_URL?.replace(/\/$/, "") ?? "http://localhost:3000";
process.env.NEXT_PUBLIC_SITE_URL ?? process.env.SITE_URL ?? "https://myplanmate.vercel.app";

export const DEFAULT_TITLE = `${SITE_NAME} - λ‚˜λ§Œμ˜ λ§žμΆ€ν˜• ν”Œλž˜λ„ˆ`;
export const TITLE_TEMPLATE = `%s | ${SITE_NAME}`;
Expand Down