Skip to content

Commit 5263000

Browse files
authored
[운영] SEO 및 성능 개선 (#148)
* [fix] 전역 스타일 설정 * [fix] Breadcrumb 개선 : 데이터 구조 변경 및 적용 * [ops] Task 139,140 : Font Awesome CDN 제거 * [ops] Task 143 : 문서 메타 설명 추가 * [ops] Task 144 : a태그를 button태그로 변경. href속성으로 인해 크롤링이 안된다는 분석 문제 해결 * [ops] Task 142 : button요소에 aria-label 속성 추가 * ESLint 및 Prettier 규칙 적용으로 코드 포맷팅 통일 * button태그에 aria 속성 추가 * TableContents : link 변수에 속성 추가 * TableContents : 테스트 코드 수정 * 디테일 수정
1 parent ac23f3c commit 5263000

7 files changed

Lines changed: 228 additions & 121 deletions

File tree

.vscode/settings.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,5 +2,6 @@
22
"editor.formatOnSave": false,
33
"editor.codeActionsOnSave": {
44
"source.fixAll.eslint": "always"
5-
}
5+
},
6+
"css.lint.unknownAtRules": "ignore",
67
}

index.html

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@
44
<title>Docker Korea</title>
55
<meta charset="UTF-8" />
66
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
7-
<script type="text/javascript">
7+
<meta name="description" content="Docker 공식 문서의 한국어 번역 프로젝트. 컨테이너(Container)와 이미지(Image) 개념, 설치 방법, 실습 튜토리얼, CLI, Compose, Swarm 등 DevOps와 개발자를 위한 최신 Docker 가이드와 베스트 프랙티스, 실전 예제, 문제 해결, 오픈소스 커뮤니티 정보까지 모두 제공합니다. 초보자와 전문가 모두를 위한 학습 자료와 FAQ 수록." />
8+
<script type="text/javascript" async>
89
!(function (cfg){function e(){cfg.onInit&&cfg.onInit(n)}var x,w,D,t,E,n,C=window,O=document,b=C.location,q="script",I="ingestionendpoint",L="disableExceptionTracking",j="ai.device.";"instrumentationKey"[x="toLowerCase"](),w="crossOrigin",D="POST",t="appInsightsSDK",E=cfg.name||"appInsights",(cfg.name||C[t])&&(C[t]=E),n=C[E]||function(g){var f=!1,m=!1,h={initialize:!0,queue:[],sv:"8",version:2,config:g};function v(e,t){var n={},i="Browser";function a(e){e=""+e;return 1===e.length?"0"+e:e}return n[j+"id"]=i[x](),n[j+"type"]=i,n["ai.operation.name"]=b&&b.pathname||"_unknown_",n["ai.internal.sdkVersion"]="javascript:snippet_"+(h.sv||h.version),{time:(i=new Date).getUTCFullYear()+"-"+a(1+i.getUTCMonth())+"-"+a(i.getUTCDate())+"T"+a(i.getUTCHours())+":"+a(i.getUTCMinutes())+":"+a(i.getUTCSeconds())+"."+(i.getUTCMilliseconds()/1e3).toFixed(3).slice(2,5)+"Z",iKey:e,name:"Microsoft.ApplicationInsights."+e.replace(/-/g,"")+"."+t,sampleRate:100,tags:n,data:{baseData:{ver:2}},ver:undefined,seq:"1",aiDataContract:undefined}}var n,i,t,a,y=-1,T=0,S=["js.monitor.azure.com","js.cdn.applicationinsights.io","js.cdn.monitor.azure.com","js0.cdn.applicationinsights.io","js0.cdn.monitor.azure.com","js2.cdn.applicationinsights.io","js2.cdn.monitor.azure.com","az416426.vo.msecnd.net"],o=g.url||cfg.src,r=function(){return s(o,null)};function s(d,t){if((n=navigator)&&(~(n=(n.userAgent||"").toLowerCase()).indexOf("msie")||~n.indexOf("trident/"))&&~d.indexOf("ai.3")&&(d=d.replace(/(\/)(ai\.3\.)([^\d]*)$/,function(e,t,n){return t+"ai.2"+n})),!1!==cfg.cr)for(var e=0;e<S.length;e++)if(0<d.indexOf(S[e])){y=e;break}var n,i=function(e){var a,t,n,i,o,r,s,c,u,l;h.queue=[],m||(0<=y&&T+1<S.length?(a=(y+T+1)%S.length,p(d.replace(/^(.*\/\/)([\w\.]*)(\/.*)$/,function(e,t,n,i){return t+S[a]+i})),T+=1):(f=m=!0,s=d,!0!==cfg.dle&&(c=(t=function(){var e,t={},n=g.connectionString;if(n)for(var i=n.split(";"),a=0;a<i.length;a++){var o=i[a].split("=");2===o.length&&(t[o[0][x]()]=o[1])}return t[I]||(e=(n=t.endpointsuffix)?t.location:null,t[I]="https://"+(e?e+".":"")+"dc."+(n||"services.visualstudio.com")),t}()).instrumentationkey||g.instrumentationKey||"",t=(t=(t=t[I])&&"/"===t.slice(-1)?t.slice(0,-1):t)?t+"/v2/track":g.endpointUrl,t=g.userOverrideEndpointUrl||t,(n=[]).push((i="SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details)",o=s,u=t,(l=(r=v(c,"Exception")).data).baseType="ExceptionData",l.baseData.exceptions=[{typeName:"SDKLoadFailed",message:i.replace(/\./g,"-"),hasFullStack:!1,stack:i+"\nSnippet failed to load ["+o+"] -- Telemetry is disabled\nHelp Link: https://go.microsoft.com/fwlink/?linkid=2128109\nHost: "+(b&&b.pathname||"_unknown_")+"\nEndpoint: "+u,parsedStack:[]}],r)),n.push((l=s,i=t,(u=(o=v(c,"Message")).data).baseType="MessageData",(r=u.baseData).message='AI (Internal): 99 message:"'+("SDK LOAD Failure: Failed to load Application Insights SDK script (See stack for details) ("+l+")").replace(/\"/g,"")+'"',r.properties={endpoint:i},o)),s=n,c=t,JSON&&((u=C.fetch)&&!cfg.useXhr?u(c,{method:D,body:JSON.stringify(s),mode:"cors"}):XMLHttpRequest&&((l=new XMLHttpRequest).open(D,c),l.setRequestHeader("Content-type","application/json"),l.send(JSON.stringify(s)))))))},a=function(e,t){m||setTimeout(function(){!t&&h.core||i()},500),f=!1},p=function(e){var n=O.createElement(q),e=(n.src=e,t&&(n.integrity=t),n.setAttribute("data-ai-name",E),cfg[w]);return!e&&""!==e||"undefined"==n[w]||(n[w]=e),n.onload=a,n.onerror=i,n.onreadystatechange=function(e,t){"loaded"!==n.readyState&&"complete"!==n.readyState||a(0,t)},cfg.ld&&cfg.ld<0?O.getElementsByTagName("head")[0].appendChild(n):setTimeout(function(){O.getElementsByTagName(q)[0].parentNode.appendChild(n)},cfg.ld||0),n};p(d)}cfg.sri&&(n=o.match(/^((http[s]?:\/\/.*\/)\w+(\.\d+){1,5})\.(([\w]+\.){0,2}js)$/))&&6===n.length?(d="".concat(n[1],".integrity.json"),i="@".concat(n[4]),l=window.fetch,t=function(e){if(!e.ext||!e.ext[i]||!e.ext[i].file)throw Error("Error Loading JSON response");var t=e.ext[i].integrity||null;s(o=n[2]+e.ext[i].file,t)},l&&!cfg.useXhr?l(d,{method:"GET",mode:"cors"}).then(function(e){return e.json()["catch"](function(){return{}})}).then(t)["catch"](r):XMLHttpRequest&&((a=new XMLHttpRequest).open("GET",d),a.onreadystatechange=function(){if(a.readyState===XMLHttpRequest.DONE)if(200===a.status)try{t(JSON.parse(a.responseText))}catch(e){r()}else r()},a.send())):o&&r();try{h.cookie=O.cookie}catch(k){}function e(e){for(;e.length;)!function(t){h[t]=function(){var e=arguments;f||h.queue.push(function(){h[t].apply(h,e)})}}(e.pop())}var c,u,l="track",d="TrackPage",p="TrackEvent",l=(e([l+"Event",l+"PageView",l+"Exception",l+"Trace",l+"DependencyData",l+"Metric",l+"PageViewPerformance","start"+d,"stop"+d,"start"+p,"stop"+p,"addTelemetryInitializer","setAuthenticatedUserContext","clearAuthenticatedUserContext","flush"]),h.SeverityLevel={Verbose:0,Information:1,Warning:2,Error:3,Critical:4},(g.extensionConfig||{}).ApplicationInsightsAnalytics||{});return!0!==g[L]&&!0!==l[L]&&(e(["_"+(c="onerror")]),u=C[c],C[c]=function(e,t,n,i,a){var o=u&&u(e,t,n,i,a);return!0!==o&&h["_"+c]({message:e,url:t,lineNumber:n,columnNumber:i,error:a,evt:C.event}),o},g.autoExceptionInstrumented=!0),h}(cfg.cfg),(C[E]=n).queue&&0===n.queue.length?(n.queue.push(e),n.trackPageView({})):e();})({
910
src: "https://js.monitor.azure.com/scripts/b/ai.3.gbl.min.js",
1011
// name: "appInsights", // Global SDK Instance name defaults to "appInsights" when not supplied
@@ -18,11 +19,7 @@
1819
connectionString: "InstrumentationKey=7bea0293-01dc-409c-9471-3a65567b11ed;IngestionEndpoint=https://koreacentral-0.in.applicationinsights.azure.com/;LiveEndpoint=https://koreacentral.livediagnostics.monitor.azure.com/;ApplicationId=ddbe985c-4d7a-41e4-80a8-a3961068933b"
1920
}});
2021
</script>
21-
<script
22-
src="https://kit.fontawesome.com/6984a5c98e.js"
23-
crossorigin="anonymous"
24-
></script>
25-
<script type="module" src="./src/scripts/main.ts"></script>
22+
<script type="module" src="./src/scripts/main.ts" defer></script>
2623
<!-- Application Insights 스크립트는 main.ts에서 운영 환경에서만 동적으로 삽입됩니다. -->
2724
<link
2825
rel="icon"
@@ -81,6 +78,8 @@
8178
</div>
8279
<button
8380
class="inline-flex h-7 w-7 items-center justify-center rounded hover:cursor-pointer hover:bg-gray-400 hover:dark:bg-gray-400"
81+
aria-label="Toggle section"
82+
aria-expanded="false"
8483
>
8584
<span>
8685
<svg
@@ -165,6 +164,8 @@
165164
</div>
166165
<button
167166
class="inline-flex h-7 w-7 items-center justify-center rounded hover:cursor-pointer hover:bg-gray-400 hover:dark:bg-gray-400"
167+
aria-label="Toggle section"
168+
aria-expanded="false"
168169
>
169170
<span>
170171
<svg
@@ -207,6 +208,8 @@
207208
</div>
208209
<button
209210
class="inline-flex h-7 w-7 items-center justify-center rounded hover:cursor-pointer hover:bg-gray-400 hover:dark:bg-gray-400"
211+
aria-label="Toggle section"
212+
aria-expanded="false"
210213
>
211214
<span>
212215
<svg
@@ -292,6 +295,8 @@
292295
</div>
293296
<button
294297
class="inline-flex h-7 w-7 items-center justify-center rounded hover:cursor-pointer hover:bg-gray-400 hover:dark:bg-gray-400"
298+
aria-label="Toggle section"
299+
aria-expanded="false"
295300
>
296301
<span>
297302
<svg
@@ -387,6 +392,8 @@
387392
</div>
388393
<button
389394
class="inline-flex h-7 w-7 items-center justify-center rounded hover:cursor-pointer hover:bg-gray-400 hover:dark:bg-gray-400"
395+
aria-label="Toggle section"
396+
aria-expanded="false"
390397
>
391398
<span>
392399
<svg
@@ -483,6 +490,8 @@
483490
</div>
484491
<button
485492
class="inline-flex h-7 w-7 items-center justify-center rounded hover:cursor-pointer hover:bg-gray-400 hover:dark:bg-gray-400"
493+
aria-label="Toggle section"
494+
aria-expanded="false"
486495
>
487496
<span>
488497
<svg

src/data/breadcrumb.json

Lines changed: 153 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,156 @@
11
{
2-
"breadcrumb": {
3-
"get-started": "시작하기",
4-
"get-docker": "Docker 설치",
5-
"docker-overview": "Docker 개요",
6-
"introduction": "소개",
7-
"build-and-push-first-image": "첫 번째 이미지 빌드 및 푸시",
8-
"develop-with-containers": "컨테이너로 개발",
9-
"get-docker-desktop": "Docker Desktop 설치",
10-
"whats-next": "다음 단계",
11-
"docker-concepts": "Docker 개념",
12-
"the-basics": "기본 사항",
13-
"what-is-a-container": "컨테이너란?",
14-
"what-is-a-registry": "레지스트리란?",
15-
"what-is-an-image": "이미지란?",
16-
"what-is-docker-compose": "Docker Compose란?",
17-
"building-images": "이미지 빌드",
18-
"understanding-image-layers": "이미지 레이어 이해",
19-
"writing-a-dockerfile": "Dockerfile 작성",
20-
"build-tag-and-publish-an-image": "이미지 빌드, 태그, 게시",
21-
"using-the-build-cache": "빌드 캐시 사용",
22-
"multi-stage-builds": "멀티 스테이지 빌드",
23-
"running-containers": "컨테이너 실행",
24-
"multi-container-applications": "멀티 컨테이너 애플리케이션",
25-
"overriding-container-defaults": "컨테이너 기본값 재정의",
26-
"persisting-container-data": "컨테이너 데이터 유지",
27-
"publishing-ports": "포트 게시",
28-
"sharing-local-files": "로컬 파일 공유",
29-
"docker-workshop": "Docker 워크샵",
30-
"resources": "리소스",
31-
"workshop": "워크샵",
32-
"02_our_app": "애플리케이션 컨테이너화",
33-
"03_updating_app": "애플리케이션 업데이트",
34-
"04_sharing_app": "애플리케이션 공유",
35-
"05_persisting_data": "데이터베이스 유지",
36-
"06_bind_mounts": "바인드 마운트 사용",
37-
"07_multi_container": "멀티 컨테이너 애플리케이션",
38-
"08_using_compose": "Docker Compose 사용",
39-
"09_image_best": "이미지 빌드 모범 사례",
40-
"10_what_next": "Docker 워크숍 이후 단계"
2+
"segments": {
3+
"get-started": {
4+
"name": "시작하기",
5+
"linkable": true
6+
},
7+
"get-docker": {
8+
"name": "Docker 설치",
9+
"linkable": true
10+
},
11+
"docker-overview": {
12+
"name": "Docker 개요",
13+
"linkable": true
14+
},
15+
"introduction": {
16+
"name": "소개",
17+
"linkable": true
18+
},
19+
"build-and-push-first-image": {
20+
"name": "첫 번째 이미지 빌드 및 푸시",
21+
"linkable": true
22+
},
23+
"develop-with-containers": {
24+
"name": "컨테이너로 개발",
25+
"linkable": true
26+
},
27+
"get-docker-desktop": {
28+
"name": "Docker Desktop 설치",
29+
"linkable": true
30+
},
31+
"whats-next": {
32+
"name": "다음 단계",
33+
"linkable": true
34+
},
35+
"docker-concepts": {
36+
"name": "Docker 개념",
37+
"linkable": false
38+
},
39+
"the-basics": {
40+
"name": "기본 사항",
41+
"linkable": false
42+
},
43+
"what-is-a-container": {
44+
"name": "컨테이너란?",
45+
"linkable": true
46+
},
47+
"what-is-a-registry": {
48+
"name": "레지스트리란?",
49+
"linkable": true
50+
},
51+
"what-is-an-image": {
52+
"name": "이미지란?",
53+
"linkable": true
54+
},
55+
"what-is-docker-compose": {
56+
"name": "Docker Compose란?",
57+
"linkable": true
58+
},
59+
"building-images": {
60+
"name": "이미지 빌드",
61+
"linkable": false
62+
},
63+
"understanding-image-layers": {
64+
"name": "이미지 레이어 이해",
65+
"linkable": true
66+
},
67+
"writing-a-dockerfile": {
68+
"name": "Dockerfile 작성",
69+
"linkable": true
70+
},
71+
"build-tag-and-publish-an-image": {
72+
"name": "이미지 빌드, 태그, 게시",
73+
"linkable": true
74+
},
75+
"using-the-build-cache": {
76+
"name": "빌드 캐시 사용",
77+
"linkable": true
78+
},
79+
"multi-stage-builds": {
80+
"name": "멀티 스테이지 빌드",
81+
"linkable": true
82+
},
83+
"running-containers": {
84+
"name": "컨테이너 실행",
85+
"linkable": false
86+
},
87+
"multi-container-applications": {
88+
"name": "멀티 컨테이너 애플리케이션",
89+
"linkable": true
90+
},
91+
"overriding-container-defaults": {
92+
"name": "컨테이너 기본값 재정의",
93+
"linkable": true
94+
},
95+
"persisting-container-data": {
96+
"name": "컨테이너 데이터 유지",
97+
"linkable": true
98+
},
99+
"publishing-ports": {
100+
"name": "포트 게시",
101+
"linkable": true
102+
},
103+
"sharing-local-files": {
104+
"name": "로컬 파일 공유",
105+
"linkable": true
106+
},
107+
"docker-workshop": {
108+
"name": "Docker 워크샵",
109+
"linkable": true
110+
},
111+
"resources": {
112+
"name": "리소스",
113+
"linkable": true
114+
},
115+
"workshop": {
116+
"name": "워크샵",
117+
"linkable": false
118+
},
119+
"02_our_app": {
120+
"name": "애플리케이션 컨테이너화",
121+
"linkable": true
122+
},
123+
"03_updating_app": {
124+
"name": "애플리케이션 업데이트",
125+
"linkable": true
126+
},
127+
"04_sharing_app": {
128+
"name": "애플리케이션 공유",
129+
"linkable": true
130+
},
131+
"05_persisting_data": {
132+
"name": "데이터베이스 유지",
133+
"linkable": true
134+
},
135+
"06_bind_mounts": {
136+
"name": "바인드 마운트 사용",
137+
"linkable": true
138+
},
139+
"07_multi_container": {
140+
"name": "멀티 컨테이너 애플리케이션",
141+
"linkable": true
142+
},
143+
"08_using_compose": {
144+
"name": "Docker Compose 사용",
145+
"linkable": true
146+
},
147+
"09_image_best": {
148+
"name": "이미지 빌드 모범 사례",
149+
"linkable": true
150+
},
151+
"10_what_next": {
152+
"name": "Docker 워크숍 이후 단계",
153+
"linkable": true
154+
}
41155
}
42156
}

src/scripts/breadcrumb.ts

Lines changed: 29 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,22 +1,31 @@
11
import translations from '../data/breadcrumb.json';
22

3-
interface BreadcrumbItem {
3+
interface SegmentData {
44
name: string;
5+
linkable: boolean;
6+
}
7+
8+
interface BreadcrumbItem extends SegmentData {
59
path: string;
610
}
711

812
interface TranslationData {
9-
breadcrumb: Record<string, string>;
13+
segments: Record<string, SegmentData>;
1014
}
1115

1216
/**
13-
* 경로 세그먼트를 번역합니다.
17+
* 경로 세그먼트를 번역하고 링크 가능 여부를 확인합니다.
1418
* @param segment 번역할 세그먼트
15-
* @returns 번역된 텍스트 또는 원본 텍스트
19+
* @returns 세그먼트 데이터 또는 기본값
1620
*/
17-
function translatePathSegment(segment: string): string {
21+
function getSegmentData(segment: string): SegmentData {
1822
const translationData = translations as TranslationData;
19-
return translationData.breadcrumb[segment] || segment;
23+
return (
24+
translationData.segments[segment] || {
25+
name: segment,
26+
linkable: false,
27+
}
28+
);
2029
}
2130

2231
/**
@@ -26,23 +35,26 @@ function generateBreadcrumbItems(): BreadcrumbItem[] {
2635
const hash = window.location.hash.slice(1); // # 제거
2736

2837
if (!hash || hash === '/') {
29-
return [{ name: '홈', path: '#/' }];
38+
return [{ name: '홈', path: '#/', linkable: true }];
3039
}
3140

3241
const pathSegments = hash.split('/').filter((segment) => segment !== '');
33-
const breadcrumbItems: BreadcrumbItem[] = [{ name: '홈', path: '#/' }];
42+
const breadcrumbItems: BreadcrumbItem[] = [
43+
{ name: '홈', path: '#/', linkable: true },
44+
];
3445

3546
let currentPath = '';
3647

3748
pathSegments.forEach((segment) => {
3849
currentPath += `/${segment}`;
3950

40-
// 세그먼트 이름을 한국어로 변환
41-
const displayName = translatePathSegment(segment);
51+
// 세그먼트 데이터 가져오기 (이름과 링크 가능 여부)
52+
const segmentData = getSegmentData(segment);
4253

4354
breadcrumbItems.push({
44-
name: displayName,
55+
name: segmentData.name,
4556
path: `#${currentPath}`,
57+
linkable: segmentData.linkable,
4658
});
4759
});
4860

@@ -66,8 +78,13 @@ function createBreadcrumbElement(items: BreadcrumbItem[]): HTMLElement {
6678
if (isLast) {
6779
// 현재 페이지는 span으로 표시
6880
return `<span class="truncate">${item.name}</span>`;
81+
}
82+
83+
if (!item.linkable) {
84+
// linkable이 false인 경우 span으로 표시 (링크 없음)
85+
return `<span class="truncate text-blue-500">${item.name}</span> / `;
6986
} else {
70-
// 이전 페이지들은 링크로 표시 + 구분자
87+
// 링크 가능한 이전 페이지들은 링크로 표시 + 구분자
7188
return `<a href="${item.path}" class="link truncate">${item.name}</a> / `;
7289
}
7390
})

src/scripts/table-contents.ts

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,11 +42,20 @@ export const initializeTableContents = () => {
4242
'font-extralight',
4343
'hover:bg-gray-300',
4444
'hover:font-semibold',
45+
'cursor-pointer'
46+
);
47+
const link = document.createElement('button');
48+
link.classList.add(
49+
'flex',
50+
'justify-start',
51+
'items-stretch',
52+
'p-1',
4553
'cursor-pointer',
54+
'w-full',
4655
'truncate'
4756
);
48-
const link = document.createElement('a');
49-
link.classList.add('flex', 'justify-start', 'items-stretch', 'p-1');
57+
link.setAttribute('aria-label', heading.textContent || 'Heading Link');
58+
link.setAttribute('role', 'link');
5059

5160
const headingText = heading.textContent || '';
5261
link.textContent =

0 commit comments

Comments
 (0)