Skip to content

Commit 246cbe6

Browse files
authored
[25.02.26 / TASK-119] Feature - SEO 세팅 (#16)
* refactor: env 정리 * refactor: 규칙 완화 * feature: GA 설정 * feature: SEO 설정 * feature: robots 및 sitemap 생성 * feature: 놓친 env 값 설정 * modify: env 칭호 통일 * modify: 개선된 opengraph 이미지 * refactor: 명명 규칙 통일 * refactor: 필요 없는 유틸리티 타입 제거
1 parent 39abc77 commit 246cbe6

File tree

23 files changed

+144
-50
lines changed

23 files changed

+144
-50
lines changed

.env.sample

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
NEXT_PUBLIC_BASE_URL=<'server url here'>
44
NEXT_PUBLIC_VELOG_URL=https://velog.io
55
NEXT_PUBLIC_ABORT_MS=<'abort time(ms) for fetch here'>
6-
SENTRY_AUTH_TOKEN=<'sentry auth token here'>
6+
NEXT_PUBLIC_SENTRY_AUTH_TOKEN=<'sentry auth token here'>
77
NEXT_PUBLIC_CHANNELTALK_PLUGIN_KEY=<'channelTalk plugin key here'>
8+
NEXT_PUBLIC_GA_ID=<'Google Analytics ID here'>
89
NEXT_PUBLIC_EVENT_LOG=<'Whether to send an event log here (true | false)'>
9-
SENTRY_DSN=<'sentry dsn here'>
10+
NEXT_PUBLIC_SENTRY_DSN=<'sentry dsn here'>

.github/workflows/docker-publish.yaml

+3-2
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,10 @@ jobs:
3030
echo "NEXT_PUBLIC_VELOG_URL=${{ secrets.NEXT_PUBLIC_VELOG_URL }}" >> .env
3131
echo "NEXT_PUBLIC_ABORT_MS=${{ secrets.NEXT_PUBLIC_ABORT_MS }}" >> .env
3232
echo "NEXT_PUBLIC_EVENT_LOG=${{ secrets.NEXT_PUBLIC_EVENT_LOG }}" >> .env
33-
echo "SENTRY_AUTH_TOKEN=${{ secrets.SENTRY_AUTH_TOKEN }}" >> .env
33+
echo "NEXT_PUBLIC_SENTRY_AUTH_TOKEN=${{ secrets.NEXT_PUBLIC_SENTRY_AUTH_TOKEN }}" >> .env
3434
echo "NEXT_PUBLIC_CHANNELTALK_PLUGIN_KEY=${{ secrets.NEXT_PUBLIC_CHANNELTALK_PLUGIN_KEY }}" >> .env
35-
echo "SENTRY_DSN=${{ secrets.SENTRY_DSN }}" >> .env
35+
echo "NEXT_PUBLIC_GA_ID=${{ secrets.NEXT_PUBLIC_GA_ID }}" >> .env
36+
echo "NEXT_PUBLIC_SENTRY_DSN=${{ secrets.NEXT_PUBLIC_SENTRY_DSN }}" >> .env
3637
cp .env .env.production
3738
3839
- name: Build Next.js application

eslint.config.mjs

+1-1
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ export default [
4444
'@typescript-eslint/promise-function-async': 'error',
4545
'@typescript-eslint/consistent-type-assertions': 'error',
4646
'@typescript-eslint/naming-convention': 'off',
47-
'no-restricted-imports': ['error', { patterns: ['..*'] }],
47+
'no-restricted-imports': ['warn', { patterns: ['..*'] }],
4848
},
4949
languageOptions: {
5050
parserOptions: { project: true, tsconfigRootDir: import.meta.dirname },

package.json

+1
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
},
1515
"dependencies": {
1616
"@channel.io/channel-web-sdk-loader": "^2.0.0",
17+
"@next/third-parties": "^15.1.7",
1718
"@sentry/nextjs": "^8.47.0",
1819
"@tanstack/react-query": "^5.61.3",
1920
"@tanstack/react-query-devtools": "^5.62.11",

pnpm-lock.yaml

+20
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

public/opengraph-image.png

99.7 KB
Loading

sentry.client.config.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,18 @@
33
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
44

55
import * as Sentry from '@sentry/nextjs';
6+
import { env } from '@/constants';
67

78
Sentry.init({
8-
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
9+
dsn: env.SENTRY_DSN,
910

1011
// Add optional integrations for additional features
1112
integrations: [
1213
Sentry.replayIntegration({ maskAllText: false, blockAllMedia: false }),
1314
],
1415

1516
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
16-
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1,
17+
tracesSampleRate: env.NODE_ENV === 'production' ? 0.1 : 1,
1718

1819
// Define how likely Replay events are sampled.
1920
// This sets the sample rate to be 10%. You may want this to be 100% while

sentry.edge.config.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,13 @@
44
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
55

66
import * as Sentry from '@sentry/nextjs';
7+
import { env } from '@/constants';
78

89
Sentry.init({
9-
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
10+
dsn: env.SENTRY_DSN,
1011

1112
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
12-
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.05 : 1,
13+
tracesSampleRate: env.NODE_ENV === 'production' ? 0.05 : 1,
1314

1415
// Setting this option to true will print useful information to the console while you're setting up Sentry.
1516
debug: false,

sentry.server.config.ts

+3-2
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,13 @@
33
// https://docs.sentry.io/platforms/javascript/guides/nextjs/
44

55
import * as Sentry from '@sentry/nextjs';
6+
import { env } from '@/constants';
67

78
Sentry.init({
8-
dsn: process.env.NEXT_PUBLIC_SENTRY_DSN,
9+
dsn: env.SENTRY_DSN,
910

1011
// Define how likely traces are sampled. Adjust this value in production, or use tracesSampler for greater control.
11-
tracesSampleRate: process.env.NODE_ENV === 'production' ? 0.1 : 1,
12+
tracesSampleRate: env.NODE_ENV === 'production' ? 0.1 : 1,
1213

1314
// Setting this option to true will print useful information to the console while you're setting up Sentry.
1415
debug: false,

src/__mock__/handlers.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
import { http } from 'msw';
2-
import { PATHS } from '@/constants';
2+
import { env, PATHS } from '@/constants';
33
import { LoginVo } from '@/types';
44
import { BaseError, BaseSuccess } from './responses';
55

6-
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL + '/api';
6+
const BASE_URL = env.BASE_URL + '/api';
77

88
const login = http.post(`${BASE_URL}${PATHS.LOGIN}`, async ({ request }) => {
99
const { accessToken, refreshToken } = (await request.json()) as LoginVo;

src/apis/instance.request.ts

+5-15
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,8 @@
11
import returnFetch, { FetchArgs } from 'return-fetch';
22

33
import { captureException, setContext } from '@sentry/nextjs';
4-
import { EnvNotFoundError, ServerNotRespondingError } from '@/errors';
5-
6-
const BASE_URL = process.env.NEXT_PUBLIC_BASE_URL;
7-
const ABORT_MS = Number(process.env.NEXT_PUBLIC_ABORT_MS);
8-
9-
if (Number.isNaN(ABORT_MS)) {
10-
throw new EnvNotFoundError('ABORT_MS');
11-
}
12-
13-
if (!BASE_URL) {
14-
throw new EnvNotFoundError('BASE_URL');
15-
}
4+
import { ServerNotRespondingError } from '@/errors';
5+
import { env } from '@/constants';
166

177
type ErrorType = {
188
code: string;
@@ -37,7 +27,7 @@ const abortPolyfill = (ms: number) => {
3727
};
3828

3929
const fetch = returnFetch({
40-
baseUrl: BASE_URL,
30+
baseUrl: env.BASE_URL,
4131
headers: {
4232
Accept: 'application/json',
4333
'Content-Type': 'application/json',
@@ -76,8 +66,8 @@ export const instance = async <I, R>(
7666
: init?.headers,
7767
body: init?.body ? JSON.stringify(init.body) : undefined,
7868
signal: AbortSignal.timeout
79-
? AbortSignal.timeout(ABORT_MS)
80-
: abortPolyfill(ABORT_MS),
69+
? AbortSignal.timeout(Number(env.ABORT_MS))
70+
: abortPolyfill(Number(env.ABORT_MS)),
8171
credentials: 'include',
8272
cache: 'no-store',
8373
});
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,10 @@
1+
import { Metadata } from 'next';
12
import { ArriveSoon } from '@/components';
23

4+
export const metadata: Metadata = {
5+
title: '통계 비교',
6+
};
7+
38
export default function Page() {
49
return <ArriveSoon />;
510
}
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
1+
import { Metadata } from 'next';
12
import { ArriveSoon } from '@/components';
23
// import { Content } from './Content';
34

5+
export const metadata: Metadata = {
6+
title: '리더보드',
7+
};
8+
49
export default function Page() {
510
return <ArriveSoon />;
611
}

src/app/(with-tracker)/(auth-required)/main/page.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ import { Content } from './Content';
77

88
export const metadata: Metadata = {
99
title: '대시보드',
10-
description: '각종 Velog 통계를 볼 수 있는 대시보드',
1110
};
1211

1312
interface IProp {

src/app/(with-tracker)/(login)/page.tsx

-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@ import { Content } from './Content';
44

55
export const metadata: Metadata = {
66
title: '로그인',
7-
description: '대시보드 페이지에 진입하기 전 표시되는 로그인 페이지',
87
};
98

109
export default function Page() {

src/app/layout.tsx

+18-1
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,29 @@ import 'react-toastify/dist/ReactToastify.css';
44
import * as sentry from '@sentry/nextjs';
55
import type { Metadata } from 'next';
66
import { ReactNode } from 'react';
7+
import { GoogleAnalytics } from '@next/third-parties/google';
78
import './globals.css';
89
import { ChannelTalkProvider, QueryProvider } from '@/components';
10+
import { env } from '@/constants';
11+
12+
export const BASE = 'https://velog-dashboard.kro.kr/';
913

1014
export const metadata: Metadata = {
1115
title: 'Velog Dashboard',
12-
description: 'Velog 통계를 확인할 수 있는 Velog Dashboard',
16+
metadataBase: new URL(BASE),
17+
description: '어디서든 편리하게 확인하는 Velog 통계 서비스, Velog Dashboard',
1318
icons: { icon: '/favicon.png' },
19+
alternates: {
20+
canonical: 'https://velog-dashboard.kro.kr/',
21+
},
22+
openGraph: {
23+
siteName: 'Velog Dashboard',
24+
description:
25+
'어디서든 편리하게 확인하는 Velog 통계 서비스, Velog Dashboard',
26+
url: 'https://velog-dashboard.kro.kr/',
27+
images: [{ url: '/opengraph-image.png', alt: 'Velog Dashboard' }],
28+
type: 'website',
29+
},
1430
};
1531

1632
const NotoSansKr = Noto_Sans_KR({ subsets: ['latin'] });
@@ -30,6 +46,7 @@ export default function RootLayout({
3046
</QueryProvider>
3147
</sentry.ErrorBoundary>
3248
</body>
49+
<GoogleAnalytics gaId={env.GA_ID} />
3350
</html>
3451
);
3552
}

src/app/robots.ts

+12
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { MetadataRoute } from 'next';
2+
import { BASE } from './layout';
3+
4+
export default function robots(): MetadataRoute.Robots {
5+
return {
6+
rules: {
7+
userAgent: '*',
8+
allow: '/',
9+
},
10+
sitemap: BASE + '/sitemap.xml',
11+
};
12+
}

src/app/sitemap.ts

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { MetadataRoute } from 'next';
2+
import { BASE } from './layout';
3+
4+
export default function sitemap(): MetadataRoute.Sitemap {
5+
return [
6+
{ url: BASE, lastModified: new Date(), changeFrequency: 'monthly' },
7+
{
8+
url: BASE + '/main',
9+
lastModified: new Date(),
10+
changeFrequency: 'monthly',
11+
},
12+
{
13+
url: BASE + '/leaderboards',
14+
lastModified: new Date(),
15+
changeFrequency: 'monthly',
16+
},
17+
{
18+
url: BASE + '/compare',
19+
lastModified: new Date(),
20+
changeFrequency: 'monthly',
21+
},
22+
];
23+
}

src/components/auth-required/main/Section/index.tsx

+3-7
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,10 @@
22

33
import { useQueryClient } from '@tanstack/react-query';
44
import { useState } from 'react';
5-
import { EnvNotFoundError, UserNameNotFoundError } from '@/errors';
5+
import { UserNameNotFoundError } from '@/errors';
66
import { trackUserEvent, MessageEnum } from '@/utils/trackUtil';
77
import { parseNumber } from '@/utils/numberUtil';
8-
import { COLORS, PATHS } from '@/constants';
8+
import { COLORS, env, PATHS } from '@/constants';
99
import { PostType, UserDto } from '@/types';
1010
import { Icon } from '@/components';
1111
import { Graph } from './Graph';
@@ -15,16 +15,12 @@ export const Section = (p: PostType) => {
1515
const client = useQueryClient();
1616

1717
const { username } = client.getQueryData([PATHS.ME]) as UserDto;
18-
const URL = process.env.NEXT_PUBLIC_VELOG_URL;
18+
const URL = env.VELOG_URL;
1919

2020
if (!username) {
2121
throw new UserNameNotFoundError();
2222
}
2323

24-
if (!URL) {
25-
throw new EnvNotFoundError('NEXT_PUBLIC_VELOG_URL');
26-
}
27-
2824
const url = `${URL}/@${username}/${p.slug}`;
2925

3026
return (

src/components/common/ChannelTalkProvider.tsx

+2-6
Original file line numberDiff line numberDiff line change
@@ -2,15 +2,11 @@
22

33
import * as ChannelService from '@channel.io/channel-web-sdk-loader';
44
import { useEffect } from 'react';
5+
import { env } from '@/constants';
56

67
const ChannelTalkServiceLoader = () => {
7-
const CHANNELTALK_PLUGIN_KEY = process.env.NEXT_PUBLIC_CHANNELTALK_PLUGIN_KEY;
8-
if (!CHANNELTALK_PLUGIN_KEY) {
9-
throw new Error('CHANNELTALK_PLUGIN_KEY가 ENV에서 설정되지 않았습니다');
10-
}
11-
128
ChannelService.loadScript();
13-
ChannelService.boot({ pluginKey: CHANNELTALK_PLUGIN_KEY });
9+
ChannelService.boot({ pluginKey: env.CHANNELTALK_PLUGIN_KEY });
1410
};
1511

1612
export const ChannelTalkProvider = ({

src/constants/env.constant.ts

+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
import { EnvNotFoundError } from '../errors/fetch.error';
2+
3+
export const env = (() => {
4+
const requiredEnv = process?.env as Record<string, string>;
5+
6+
const env = {
7+
NODE_ENV: requiredEnv.NODE_ENV,
8+
BASE_URL: requiredEnv.NEXT_PUBLIC_BASE_URL,
9+
ABORT_MS: requiredEnv.NEXT_PUBLIC_ABORT_MS,
10+
EVENT_LOG: requiredEnv.NEXT_PUBLIC_EVENT_LOG,
11+
VELOG_URL: requiredEnv.NEXT_PUBLIC_VELOG_URL,
12+
CHANNELTALK_PLUGIN_KEY: requiredEnv.NEXT_PUBLIC_CHANNELTALK_PLUGIN_KEY,
13+
GA_ID: requiredEnv.NEXT_PUBLIC_GA_ID,
14+
SENTRY_AUTH_TOKEN: requiredEnv.NEXT_PUBLIC_SENTRY_AUTH_TOKEN,
15+
SENTRY_DSN: requiredEnv.NEXT_PUBLIC_SENTRY_DSN,
16+
} as const;
17+
18+
if (env.NODE_ENV) {
19+
Object.entries(env).forEach(([key, value]) => {
20+
if (!value) throw new EnvNotFoundError(key);
21+
});
22+
}
23+
24+
return env;
25+
})();

0 commit comments

Comments
 (0)