라우팅
Astro는 프로젝트 src/pages/
디렉터리의 파일 레이아웃을 기반으로 빌드 URL을 생성하기 위해 파일 기반 라우팅을 사용합니다.
페이지 간 탐색
섹션 제목: 페이지 간 탐색Astro는 라우트 간 탐색을 위해 표준 HTML <a>
요소를 사용합니다. 제공되는 프레임워크 특정 <Link>
컴포넌트는 없습니다.
<p>Astro에 대해 더 <a href="/about/">자세히</a> 알아보세요!</p>
<!-- `base: "/docs"`가 구성된 경우 --><p><a href="/docs/reference/">참조</a> 섹션에서 더 자세히 알아보세요!</p>
정적 라우트
섹션 제목: 정적 라우트src/pages/
디렉터리의 .astro
페이지 컴포넌트와 Markdown 및 MDX 파일 (.md
, .mdx
)은 자동으로 웹사이트의 페이지가 됩니다. 각 페이지의 라우트는 src/pages/
디렉터리의 경로 및 파일 이름에 해당합니다.
# 예시: 정적 라우트src/pages/index.astro -> mysite.com/src/pages/about.astro -> mysite.com/aboutsrc/pages/about/index.astro -> mysite.com/aboutsrc/pages/about/me.astro -> mysite.com/about/mesrc/pages/posts/1.md -> mysite.com/posts/1
Astro 프로젝트에서 유지 관리할 별도의 “라우팅 구성”은 없습니다! src/pages/
디렉터리에 파일을 추가하면 새 라우트가 자동으로 생성됩니다. 정적 빌드에서 build.format
구성 옵션을 사용하여 파일 출력 형식을 사용자 정의할 수 있습니다.
동적 라우트
섹션 제목: 동적 라우트Astro 페이지 파일은 파일 이름에 동적 라우트 매개변수를 지정하여 일치하는 여러 페이지를 생성할 수 있습니다. 예를 들어, src/pages/authors/[author].astro
는 블로그의 모든 작성자에 대한 바이오 페이지를 생성합니다. author
는 페이지에서 액세스할 수 있는 매개변수가 됩니다.
Astro의 기본 정적 출력 모드에서는 이러한 페이지가 빌드 시 생성되므로 해당 파일을 가져오는 author
목록을 미리 결정해야 합니다. SSR 모드에서는 일치하는 모든 라우트에 대한 요청 시 페이지가 생성됩니다.
정적 (SSG) 모드
섹션 제목: 정적 (SSG) 모드모든 라우트는 빌드 시 결정되어야 하므로 동적 라우트는 params
속성이 있는 객체의 배열을 반환하는 getStaticPaths()
를 내보내야 합니다. 이러한 각 객체는 해당되는 라우트를 생성합니다.
[dog].astro
는 파일 이름에 동적 dog
매개변수를 정의하므로 getStaticPaths()
에서 반환된 객체는 params
에 dog
를 포함해야 합니다. 그러면 페이지는 Astro.params
를 사용하여 이 매개변수에 액세스할 수 있습니다.
---export function getStaticPaths() { return [ { params: { dog: "clifford" }}, { params: { dog: "rover" }}, { params: { dog: "spot" }}, ];}
const { dog } = Astro.params;---<div>{dog}는 귀여운 강아지입니다!</div>
이는 해당 강아지 이름을 각각 표시하는 세 개의 페이지 /dogs/clifford
, /dogs/rover
및 /dogs/spot
을 생성합니다.
파일 이름은 여러 매개변수를 포함할 수 있으며, 이 매개변수는 모두 getStaticPaths()
의 params
객체에 포함되어야 합니다.
---export function getStaticPaths() { return [ { params: { lang: "en", version: "v1" }}, { params: { lang: "fr", version: "v2" }}, ];}
const { lang, version } = Astro.params;---
이렇게 하면 /en-v1/info
및 /fr-v2/info
가 생성됩니다.
매개변수는 경로의 별도 부분에 포함될 수 있습니다. 예를 들어, 위의 동일한 getStaticPaths()
를 사용하는 파일 src/pages/[lang]/[version]/info.astro
는 /en/v1/info
및 /fr/v2/info
라우트를 생성합니다.
params
디코딩
섹션 제목: params 디코딩getStaticPaths()
함수에 제공된 params
는 디코딩되지 않습니다. 매개변수 값을 디코딩해야 하는 경우 decodeURI
함수를 사용하세요.
---export function getStaticPaths() { return [ { params: { slug: decodeURI("%5Bpage%5D") }}, // "[page]"로 디코딩됩니다. ]}---
getStaticPaths()
에 대해 자세히 알아보세요.

나머지 매개변수
섹션 제목: 나머지 매개변수URL 라우팅에서 더 많은 유연성이 필요한 경우 .astro
파일 이름에 나머지 매개변수 ([...path]
)를 사용하여 모든 깊이의 파일 경로와 일치시킬 수 있습니다.
---export function getStaticPaths() { return [ { params: { path: "one/two/three" }}, { params: { path: "four" }}, { params: { path: undefined }} ]}
const { path } = Astro.params;---
이렇게 하면 /sequences/one/two/three
, /sequences/four
및 /sequences
가 생성됩니다. (나머지 매개변수를 undefined
로 설정하면 최상위 페이지와 일치할 수 있습니다.)
나머지 매개변수는 명명된 다른 매개변수와 함께 사용할 수 있습니다. 예를 들어 GitHub의 파일 뷰어는 다음 동적 라우트로 나타낼 수 있습니다.
/[org]/[repo]/tree/[branch]/[...file]
이 예시에서 /withastro/astro/tree/main/docs/public/favicon.svg
에 대한 요청은 다음과 같은 명명된 매개변수로 분할됩니다.
{ org: "withastro", repo: "astro", branch: "main", file: "docs/public/favicon.svg"}
예시: 여러 레벨의 동적 페이지
섹션 제목: 예시: 여러 레벨의 동적 페이지다음 예시에서 나머지 매개변수 ([...slug]
)와 getStaticPaths()
의 props
기능을 사용하여 다양한 깊이의 슬러그에 대한 페이지를 생성합니다.
---export function getStaticPaths() { const pages = [ { slug: undefined, title: "Astro 스토어", text: "Astro 스토어에 오신 것을 환영합니다!", }, { slug: "products", title: "Astro 제품", text: "여러분을 위한 다양한 제품이 준비되어 있습니다.", }, { slug: "products/astro-handbook", title: "궁극적인 Astro 핸드북", text: "Astro를 배우고 싶다면 이 책을 꼭 읽어야 합니다.", }, ];
return pages.map(({ slug, title, text }) => { return { params: { slug }, props: { title, text }, }; });}
const { title, text } = Astro.props;---<html> <head> <title>{title}</title> </head> <body> <h1>{title}</h1> <p>{text}</p> </body></html>
서버 (SSR) 모드
섹션 제목: 서버 (SSR) 모드SSR 모드에서 동적 라우트는 동일한 방식으로 정의됩니다. 임의의 문자열 또는 경로와 일치하도록 파일 이름에 [param]
또는 [...path]
대괄호를 포함합니다. 그러나 라우트는 더 이상 미리 빌드되지 않으므로 일치하는 모든 라우트에 페이지가 제공됩니다. 이러한 라우트는 “정적” 라우트가 아니므로 getStaticPaths
를 사용하면 안됩니다.
요청 시 렌더링되는 라우트의 경우 파일 이름에 스프레드 표기법을 사용하는 나머지 매개변수를 하나만 사용할 수 있습니다. (예: src/pages/[locale]/[...slug].astro
또는 src/pages/[...locale]/[slug].astro
는 사용할 수 있지만, src/pages/[...locale]/[...slug].astro
는 사용할 수 없습니다.)
---const { resource, id } = Astro.params;---<h1>{resource}: {id}</h1>
이 페이지는 resource
및 id
의 모든 값에 대해 제공됩니다. 예: resources/users/1
, resources/colors/blue
등
[...slug]
예시를 SSR용으로 수정
섹션 제목: [...slug] 예시를 SSR용으로 수정SSR 페이지는 getStaticPaths()
를 사용할 수 없으므로 props를 받을 수 없습니다. 이전 예시는 객체에서 slug
매개변수의 값을 조회하여 SSR 모드에 맞게 조정할 수 있습니다. 라우트가 루트 (”/“)에 있으면 slug
매개변수는 undefined
가 됩니다. 값이 객체에 존재하지 않으면 404 페이지로 리디렉션합니다.
---const pages = [ { slug: undefined, title: "Astro 스토어", text: "Astro 스토어에 오신 것을 환영합니다!", }, { slug: "products", title: "Astro 제품", text: "여러분을 위한 다양한 제품이 준비되어 있습니다.", }, { slug: "products/astro-handbook", title: "궁극적인 Astro 핸드북", text: "Astro를 배우고 싶다면 이 책을 꼭 읽어야 합니다.", },];
const { slug } = Astro.params;const page = pages.find((page) => page.slug === slug);if (!page) return Astro.redirect("/404");const { title, text } = page;---<html> <head> <title>{title}</title> </head> <body> <h1>{title}</h1> <p>{text}</p> </body></html>
리디렉션
섹션 제목: 리디렉션사이트 구조가 변경되어 영구적으로 또는 인증된 라우트에 로그인하는 등의 작업에 대한 응답으로 독자를 새 페이지로 리디렉션해야 하는 경우가 있습니다.
Astro 구성에서 영구적으로 이동된 페이지로 사용자를 리디렉션하는 규칙을 정의할 수 있습니다. 또는 사용자가 사이트를 사용할 때 동적으로 사용자를 리디렉션할 수 있습니다.
구성된 리디렉션
섹션 제목: 구성된 리디렉션
추가된 버전:
astro@2.9.0
Astro 구성에서 redirects
값을 사용하여 영구 리디렉션 매핑을 지정할 수 있습니다.
내부 리디렉션의 경우, 이는 이전 라우트 경로에서 새 라우트로의 매핑입니다. Astro v5.2.0부터는 http
또는 https
로 시작하고 구문 분석할 수 있는 외부 URL로 리디렉션할 수도 있습니다.
import { defineConfig } from "astro/config";
export default defineConfig({ redirects: { "/old-page": "/new-page", "/blog": "https://example.com/blog" }});
이러한 리디렉션은 파일 기반 라우트와 동일한 우선 순위 규칙을 따르며 프로젝트에서 동일한 이름의 기존 페이지 파일보다 항상 낮은 우선 순위를 갖습니다. 예를 들어 프로젝트에 src/pages/old-page.astro
파일이 포함된 경우 /old-page
는 /new-page
로 리디렉션되지 않습니다.
새 라우트와 이전 라우트 모두 동일한 매개변수를 포함하는 한 동적 라우트가 허용됩니다. 예를 들면 다음과 같습니다.
{ "/blog/[...slug]": "/articles/[...slug]"}
SSR 또는 정적 어댑터를 사용하면 값을 객체로 제공하여 새 destination
외에 status
코드를 지정할 수도 있습니다.
import { defineConfig } from "astro/config";
export default defineConfig({ redirects: { "/old-page": { status: 302, destination: "/new-page" }, "/news": { status: 302, destination: "https://example.com/news" } }});
astro build
를 실행하면 Astro는 기본적으로 meta refresh 태그가 있는 HTML 파일을 출력합니다. 지원되는 어댑터는 대신 리디렉션과 함께 호스트의 구성 파일을 작성합니다.
상태 코드는 기본적으로 301
입니다. HTML 파일로 빌드하는 경우 상태 코드는 서버에서 사용되지 않습니다.
동적 리디렉션
섹션 제목: 동적 리디렉션Astro
전역에서 Astro.redirect
메서드를 사용하면 다른 페이지로 동적으로 리디렉션할 수 있습니다. 쿠키에서 세션을 가져와 사용자가 로그인했는지 확인한 후에 이 작업을 수행할 수 있습니다.
---import { isLoggedIn } from "../utils";
const cookie = Astro.request.headers.get("cookie");
// 사용자가 로그인하지 않은 경우 로그인 페이지로 리디렉션합니다.if (!isLoggedIn(cookie)) { return Astro.redirect("/login");}---
URL 재작성 (Rewrites)
섹션 제목: URL 재작성 (Rewrites)
추가된 버전:
astro@4.13.0
URL 재작성은 브라우저를 다른 페이지로 리디렉션하지 않고 다른 라우트를 제공할 수 있도록 합니다. 브라우저는 URL 표시줄에 원래 주소를 표시하지만, 대신 Astro.rewrite()
에 제공된 URL의 콘텐츠를 표시합니다.
영구적으로 이동한 콘텐츠 또는 새 URL이 있는 다른 페이지로 사용자를 안내하려면 (예: 로그인 후 사용자 대시보드), 대신 리디렉션을 사용하세요.
URL 재작성은 두 개의 다른 소스 파일을 유지 관리할 필요 없이 여러 경로 (예: /products/shoes/men/
및 /products/men/shoes/
)에서 동일한 콘텐츠를 표시하는 데 유용할 수 있습니다.
URL 재작성은 SEO 목적과 사용자 경험에도 유용합니다. 이를 통해 방문자를 다른 페이지로 리디렉션하거나 404 상태를 반환해야 하는 콘텐츠를 표시할 수 있습니다. URL 재작성의 일반적인 용도 중 하나는 다양한 언어 변형에 대해 동일한 현지화된 콘텐츠를 표시하는 것입니다.
다음 예제에서는 /es-CU/
(쿠바 스페인어) URL 경로를 방문했을 때 페이지의 /es/
버전을 렌더링하기 위해 URL 재작성을 사용합니다. 방문자가 /es-cu/articles/introduction
URL로 이동하면 Astro는 src/pages/es/articles/introduction.astro
파일에서 생성된 콘텐츠를 렌더링합니다.
---return Astro.rewrite("/es/articles/introduction");---
엔드포인트 파일에서 context.rewrite()
를 사용하여 다른 페이지로 경로를 재지정합니다.
export function GET(context) { if (!context.locals.allowed) { return context.rewrite("/"); }}
Astro.rewrite()
에 전달된 URL이 런타임 오류를 발생시키면 Astro는 개발 환경에서 오버레이 오류를 표시하고 프로덕션 환경에서는 500 상태 코드를 반환합니다. URL이 프로젝트에 존재하지 않으면 404 상태 코드가 반환됩니다.
예를 들어 이커머스의 제품이 더 이상 제공되지 않음을 나타내기 위해 /404
페이지를 렌더링하도록 의도적으로 URL 재작성을 생성할 수 있습니다.
---const { item } = Astro.params;
if (!itemExists(item)) { return Astro.rewrite("/404");}---
HTTP 응답 상태에 따라 조건부로 URL을 재작성하여 존재하지 않는 URL을 방문할 때 사이트의 특정 페이지를 표시할 수도 있습니다.
export const onRequest = async (context, next) => { const response = await next(); if (response.status === 404) { return context.rewrite("/"); } return response;}
지정된 URL 재작성 경로의 콘텐츠를 표시하기 전에 Astro.rewrite()
함수는 새로운 완전한 렌더링 단계를 트리거합니다. 이렇게 하면 새 라우트/요청에 대한 모든 미들웨어가 다시 실행됩니다.
Astro.rewrite()
API 참조를 확인하세요.
라우트 우선 순위
섹션 제목: 라우트 우선 순위정의된 여러 라우트가 동일한 URL 경로를 빌드하려고 시도할 수 있습니다. 예를 들어, 이러한 모든 라우트는 /posts/create
를 빌드할 수 있습니다.
디렉터리src/pages/
- […slug].astro
디렉터리posts/
- create.astro
- [page].astro
- [pid].ts
- […slug].astro
Astro는 페이지를 빌드하는 데 어떤 라우트를 사용해야 하는지 알아야 합니다. 이를 위해 다음 규칙에 따라 순서를 정렬합니다.
- Astro의 예약된 라우트
- 경로 세그먼트가 많은 라우트는 덜 구체적인 라우트보다 우선합니다. 위 예에서
/posts/
아래의 모든 라우트는 루트의/[...slug].astro
보다 우선합니다. - 경로 매개변수가 없는 정적 라우트는 동적 라우트보다 우선합니다. 예를 들어
/posts/create.astro
는 예시의 다른 모든 라우트보다 우선합니다. - 명명된 매개변수를 사용하는 동적 라우트는 나머지 매개변수보다 우선합니다. 예를 들어
/posts/[page].astro
는/posts/[...slug].astro
보다 우선합니다. - 미리 렌더링된 동적 라우트는 서버의 동적 라우트보다 우선합니다.
- 엔드포인트는 페이지보다 우선합니다.
- 파일 기반 라우트는 리디렉션보다 우선합니다.
- 위의 규칙 중 어느 것도 순서를 결정하지 않는 경우, 라우트는 Node 설치의 기본 로케일을 기준으로 알파벳순으로 정렬됩니다.
위 예시를 바탕으로, 요청된 URL을 HTML을 빌드하는 데 사용되는 라우트에 매칭하는 규칙의 몇 가지 예시는 다음과 같습니다.
pages/posts/create.astro
-/posts/create
만 빌드합니다.pages/posts/[pid].ts
-/posts/abc
,/posts/xyz
등을 빌드합니다. 하지만/posts/create
는 빌드하지 않습니다.pages/posts/[page].astro
-/posts/1
,/posts/2
등을 빌드합니다. 하지만/posts/create
,/posts/abc
또는/posts/xyz
는 빌드하지 않습니다.pages/posts/[...slug].astro
-/posts/1/2
,/posts/a/b/c
등을 빌드합니다. 하지만/posts/create
,/posts/1
,/posts/abc
등은 빌드하지 않습니다.pages/[...slug].astro
-/abc
,/xyz
,/abc/xyz
등을 빌드합니다. 하지만/posts/create
,/posts/1
,/posts/abc
등은 빌드하지 않습니다.
예약된 라우트
섹션 제목: 예약된 라우트내부 라우트가 사용자 정의 또는 통합 정의 라우트보다 우선해야 Astro 기능이 작동합니다. 다음은 Astro의 예약된 라우트입니다.
_astro/
: CSS 문서, 번들링된 클라이언트 스크립트, 최적화된 이미지 및 모든 Vite 자산을 포함하여 모든 정적 자산을 클라이언트에 제공합니다._server_islands/
: 서버 아일랜드로 연기된 동적 컴포넌트를 제공합니다._actions/
: 정의된 액션을 제공합니다.
페이지네이션
섹션 제목: 페이지네이션Astro는 여러 페이지로 분할해야 하는 대규모 데이터 컬렉션에 대한 내장 페이지네이션을 지원합니다. Astro는 이전/다음 페이지 URL, 총 페이지 수 등을 포함한 일반적인 페이지네이션 속성을 생성합니다.
페이지네이션된 라우트 이름은 표준 동적 라우트와 동일한 [대괄호]
구문을 사용해야 합니다. 예를 들어, 파일 이름 /astronauts/[page].astro
는 /astronauts/1
, /astronauts/2
등에 대한 라우트를 생성하며, 여기서 [page]
는 생성된 페이지 번호입니다.
다음과 같이 paginate()
함수를 사용하여 값 배열에 대한 이러한 페이지를 생성할 수 있습니다.
---export function getStaticPaths({ paginate }) { const astronautPages = [ { astronaut: "Neil Armstrong" }, { astronaut: "Buzz Aldrin" }, { astronaut: "Sally Ride" }, { astronaut: "John Glenn" }, ];
// astronauts 배열로부터 페이지를 생성하며, 각 페이지에는 2개의 항목이 표시됩니다. return paginate(astronautPages, { pageSize: 2 });}// 페이지네이션된 모든 데이터는 "page" prop으로 전달됩니다.const { page } = Astro.props;---<!-- 현재 페이지 번호를 표시합니다. `Astro.params.page`를 사용할 수도 있습니다! --><h1>Page {page.currentPage}</h1><ul> <!-- astronaut 정보 배열을 나열합니다. --> {page.data.map(({ astronaut }) => <li>{astronaut}</li>)}</ul>
이는 페이지당 2개의 항목으로 다음 페이지를 생성합니다.
/astronauts/1
- 1페이지: “Neil Armstrong” 및 “Buzz Aldrin” 표시/astronauts/2
- 2페이지: “Sally Ride” 및 “John Glenn” 표시
page
prop
섹션 제목: page proppaginate()
함수를 사용하면 각 페이지는 page
prop을 통해 데이터를 전달받습니다. page
prop에는 페이지를 빌드하고 페이지 간 링크를 생성하는 데 사용할 수 있는 유용한 속성이 많이 있습니다.
interface Page<T = any> { /** `paginate()` 함수에 전달한 데이터의 페이지 조각을 포함하는 배열입니다. */ data: T[]; /** 메타데이터 */ /** 0부터 시작하는 페이지의 첫 번째 항목 수입니다. */ start: number; /** 0부터 시작하는 페이지의 마지막 항목 수입니다. */ end: number; /** 총 결과 수입니다. */ total: number; /** 1부터 시작하는 현재 페이지 번호입니다. */ currentPage: number; /** 페이지당 항목 수입니다. (기본값: 10) */ size: number; /** 마지막 페이지 번호입니다. */ lastPage: number; url: { /** 현재 페이지의 URL입니다. */ current: string; /** 이전 페이지의 URL입니다. (있는 경우) */ prev: string | undefined; /** 다음 페이지의 URL입니다. (있는 경우) */ next: string | undefined; /** 첫 페이지의 URL입니다. (현재 페이지가 첫 페이지가 아닌 경우) */ first: string | undefined; /** 마지막 페이지의 URL입니다. (현재 페이지가 마지막 페이지가 아닌 경우) */ last: string | undefined; };}
다음 예제는 페이지 간 이동 링크와 함께 페이지의 현재 정보를 표시합니다.
---// 이전 예제와 동일한 `{ astronaut }` 객체 목록을 페이지로 나눕니다.export function getStaticPaths({ paginate }) { /* ... */ }const { page } = Astro.props;---<h1>현재 페이지: {page.currentPage}</h1><ul> {page.data.map(({ astronaut }) => <li>{astronaut}</li>)}</ul>{page.url.first ? <a href={page.url.first}>첫 페이지</a> : null}{page.url.prev ? <a href={page.url.prev}>이전 페이지</a> : null}{page.url.next ? <a href={page.url.next}>다음 페이지</a> : null}{page.url.last ? <a href={page.url.last}>마지막 페이지</a> : null}
page
prop에 대해 자세히 알아보세요.
중첩된 페이지네이션
섹션 제목: 중첩된 페이지네이션페이지네이션의 고급 사용 사례는 중첩된 페이지네이션입니다. 이는 페이지네이션이 다른 동적 라우트 매개변수와 결합될 때 발생합니다. 중첩된 페이지네이션을 사용하여 페이지네이션된 컬렉션을 일부 속성 또는 태그별로 그룹화할 수 있습니다.
예를 들어, 페이지네이션된 Markdown 게시물을 일부 태그별로 그룹화하려면 다음 URL과 일치하는 /src/pages/[tag]/[page].astro
페이지를 생성하여 중첩된 페이지네이션을 사용합니다.
/red/1
(tag=red)/red/2
(tag=red)/blue/1
(tag=blue)/green/1
(tag=green)
중첩된 페이지네이션은 getStaticPaths()
에서 각 그룹에 대해 paginate()
결과의 배열을 반환하여 작동합니다.
다음 예제에서는 위에 나열된 URL을 빌드하기 위해 중첩된 페이지네이션을 구현합니다.
---export function getStaticPaths({ paginate }) { const allTags = ["red", "blue", "green"]; const allPosts = Object.values(import.meta.glob("../pages/post/*.md", { eager: true })); // 모든 태그에 대해 `paginate()` 결과를 반환합니다. // Astro가 결과가 어떤 태그 그룹에 대한 것인지 알 수 있도록 // `{ params: { tag }}`를 `paginate()`에 전달해야 합니다. return allTags.flatMap((tag) => { const filteredPosts = allPosts.filter((post) => post.frontmatter.tag === tag); return paginate(filteredPosts, { params: { tag }, pageSize: 10 }); });}
const { page } = Astro.props;const params = Astro.params;
페이지 제외
섹션 제목: 페이지 제외src/pages
의 페이지 또는 디렉터리를 빌드에서 제외하려면 이름 앞에 밑줄 (_
)을 붙이면 됩니다. _
접두사가 붙은 파일은 라우터에서 인식되지 않으며 dist/
디렉터리에 배치되지 않습니다.
이를 사용하여 페이지를 일시적으로 비활성화하고, 테스트, 유틸리티 및 컴포넌트를 관련 페이지와 동일한 폴더에 넣을 수도 있습니다.
이 예시에서는 src/pages/index.astro
및 src/pages/projects/project1.md
만 페이지 라우트 및 HTML 파일로 빌드됩니다.
디렉터리src/pages/
디렉터리_hidden-directory/
- page1.md
- page2.md
- _hidden-page.astro
- index.astro
디렉터리projects/
- _SomeComponent.astro
- _utils.js
- project1.md