EmDash는 두 개의 파일을 통해 구성됩니다: 통합을 위한 astro.config.mjs와 콘텐츠 컬렉션을 위한 src/live.config.ts입니다.
Astro 통합
astro.config.mjs에서 EmDash를 Astro 통합으로 구성합니다:
import { defineConfig } from "astro/config";
import emdash, { local, s3 } from "emdash/astro";
import { sqlite, libsql } from "emdash/db";
export default defineConfig({
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
plugins: [],
}),
],
});
통합 옵션
database
필수. 데이터베이스 어댑터 구성입니다. 어댑터를 선택하세요:
// SQLite (Node.js)
database: sqlite({ url: "file:./data.db" });
// PostgreSQL
database: postgres({ connectionString: process.env.DATABASE_URL });
// libSQL
database: libsql({
url: process.env.LIBSQL_DATABASE_URL,
authToken: process.env.LIBSQL_AUTH_TOKEN,
});
// Cloudflare D1 (@emdash-cms/cloudflare에서 가져오기)
database: d1({ binding: "DB" });
자세한 내용은 데이터베이스 옵션을 참조하세요.
storage
필수. 미디어 저장소 어댑터 구성입니다. 어댑터를 선택하세요:
// 로컬 파일 시스템 (개발)
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
});
// R2 바인딩 (Cloudflare Workers)
storage: r2({
binding: "MEDIA",
publicUrl: "https://pub-xxxx.r2.dev", // 선택 사항
});
// S3 호환 (모든 플랫폼) — 모든 필드는 S3_* 환경 변수에서
storage: s3()
// 또는 명시적 값 사용
storage: s3({
endpoint: "https://s3.amazonaws.com",
bucket: "my-bucket",
accessKeyId: process.env.S3_ACCESS_KEY_ID,
secretAccessKey: process.env.S3_SECRET_ACCESS_KEY,
region: "us-east-1", // 선택 사항, 기본값: "auto"
publicUrl: "https://cdn.example.com", // 선택 사항
});
자세한 내용은 저장소 옵션을 참조하세요.
plugins
선택 사항. EmDash 플러그인 배열입니다. 다음 예제는 하나의 플러그인을 등록합니다:
import seoPlugin from "@emdash-cms/plugin-seo";
plugins: [seoPlugin()];
fonts
선택 사항. 관리자 UI 폰트 구성입니다.
기본적으로 EmDash는 Astro Font API를 통해 Noto Sans를 로드합니다. 폰트는 빌드 시 Google에서 다운로드되고 자체 호스팅되므로 런타임 CDN 요청이 없습니다. 기본 폰트는 라틴, 키릴, 그리스, 데바나가리 및 베트남 문자를 다룹니다.
추가 문자 체계에 대한 지원을 추가하려면 스크립트 이름을 전달하세요. 다음 예제는 아랍어와 일본어를 추가합니다:
emdash({
fonts: {
scripts: ["arabic", "japanese"],
},
})
사용 가능한 스크립트는 arabic, armenian, bengali, chinese-simplified, chinese-traditional, chinese-hongkong, devanagari, ethiopic, farsi, georgian, gujarati, gurmukhi, hebrew, japanese, kannada, khmer, korean, lao, malayalam, myanmar, oriya, sinhala, tamil, telugu, thai 및 tibetan입니다.
각 스크립트는 Google Fonts의 해당 Noto Sans 변형에 매핑됩니다(예: "arabic"은 Noto Sans Arabic을 로드합니다). 모든 폰트 페이스는 단일 font-family 이름을 공유하고 unicode-range를 사용하므로 브라우저는 페이지의 문자에 필요한 파일만 다운로드합니다.
폰트 주입을 완전히 비활성화하고 시스템 폰트를 사용하려면 false로 설정하세요:
emdash({
fonts: false,
})
관리자 CSS는 --font-emdash CSS 변수를 사용합니다. 이것은 위의 폰트 구성에 의해 자동으로 설정됩니다.
auth
선택 사항. 인증 어댑터입니다. EmDash의 내장 로그인은 패스키를 사용합니다. auth를 설정하면 외부 공급자로 대체됩니다. Cloudflare Access 어댑터인 access()는 @emdash-cms/cloudflare에서 제공됩니다:
import { access } from "@emdash-cms/cloudflare";
emdash({
auth: access({
teamDomain: "myteam.cloudflareaccess.com",
audience: "your-app-audience-tag",
roleMapping: {
Admins: 50,
Editors: 40,
},
}),
});
access()의 옵션:
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
teamDomain | string | 필수 | Cloudflare Access 팀 도메인 |
audience | string | — | 애플리케이션 Audience (AUD) 태그. Workers에서는 audienceEnvVar를 선호하세요. |
audienceEnvVar | string | "CF_ACCESS_AUDIENCE" | 런타임에 audience 태그를 읽을 환경 변수 |
autoProvision | boolean | true | 첫 로그인 시 EmDash 사용자 생성 |
defaultRole | number | 30 | roleMapping과 일치하지 않는 사용자의 역할 레벨 (사용자 역할 참조) |
syncRoles | boolean | false | 프로비저닝 시에만이 아니라 매 로그인마다 roleMapping을 재적용 |
roleMapping | object | — | IdP 그룹 이름을 EmDash 역할 레벨에 매핑; 첫 번째 일치가 우선 |
authProviders
선택 사항. 플러그 가능한 로그인 공급자 배열입니다 (최상위 레벨, auth와 함께). 각 항목은 아래와 같이 공급자 팩토리 호출의 결과입니다:
import { github } from "emdash/auth/providers/github";
import { google } from "emdash/auth/providers/google";
import { atproto } from "@emdash-cms/auth-atproto";
emdash({
authProviders: [github(), google(), atproto()],
});
내장 공급자:
github()—EMDASH_OAUTH_GITHUB_CLIENT_ID/EMDASH_OAUTH_GITHUB_CLIENT_SECRET를 읽습니다 (또는 접두사가 없는 폴백).google()—EMDASH_OAUTH_GOOGLE_CLIENT_ID/EMDASH_OAUTH_GOOGLE_CLIENT_SECRET를 읽습니다.atproto()— Atmosphere 계정 로그인 (Bluesky 및 더 넓은 AT 프로토콜 네트워크). 환경 변수가 필요하지 않습니다.{ allowedDIDs, allowedHandles, defaultRole }를 허용합니다. Atmosphere 로그인 가이드를 참조하세요.
타사 패키지는 동일한 AuthProviderDescriptor 형태를 사용하여 자체 공급자를 등록할 수 있습니다 — 로그인 공급자를 참조하세요.
siteUrl
선택 사항. 사이트의 공개 브라우저 대면 오리진입니다 (스킴 + 호스트 + 선택적 포트, 경로 없음).
TLS 종료 리버스 프록시 뒤에서 Astro.url은 공개 주소(https://cms.example.com) 대신 내부 주소(http://localhost:4321)를 반환합니다. 이것은 패스키, CSRF 오리진 매칭, OAuth 리디렉션, 로그인 리디렉션, MCP 발견, 스냅샷 내보내기, 사이트맵, robots.txt 및 JSON-LD 구조화 데이터를 손상시킵니다. 모든 것을 한 번에 수정하려면 siteUrl을 설정하세요.
통합은 로드 시 이 값을 검증합니다: http: 또는 https: 프로토콜을 가진 유효한 URL이어야 하며 오리진으로 정규화됩니다 (경로가 제거됨).
다음 예제는 공개 오리진을 설정합니다:
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
siteUrl: "https://cms.example.com",
});
구성에서 siteUrl이 설정되지 않은 경우 EmDash는 환경 변수를 순서대로 확인합니다: EMDASH_SITE_URL, 그 다음 SITE_URL. 이것은 런타임에 공개 URL이 설정되는 컨테이너 배포에 유용합니다.
다중 오리진 패스키 검증
siteUrl은 단일 정규 오리진을 정의합니다. 동일한 EmDash 배포가 등록 가능한 상위 도메인을 공유하는 여러 호스트 이름(예: https://example.com 및 https://preview.example.com)에서 액세스 가능한 경우, 패스키 검증은 오리진이 siteUrl과 정확히 일치하지 않는 어설션을 거부합니다 — WebAuthn이 동일한 rpId 아래 하위 도메인 간에 패스키가 유효하도록 허용하더라도.
astro.config.mjs의 allowedOrigins 또는 EMDASH_ALLOWED_ORIGINS 환경 변수를 통해 추가 허용 오리진을 선언하세요. 정규 siteUrl은 rpId의 소스로 유지됩니다; 여기에 나열된 항목은 검증 시 허용됩니다. 두 소스는 런타임에 병합되므로 구성은 안정적인 오리진(버전 관리됨, 코드 리뷰됨)을 선언할 수 있고 환경은 환경별 추가 항목(예: 임시 PR 미리보기)을 추가할 수 있습니다.
다음 예제는 구성에서 하나의 추가 오리진을 선언합니다:
emdash({
siteUrl: "https://example.com",
allowedOrigins: ["https://preview.example.com"],
})
동등한 값은 환경 변수에서도 올 수 있습니다:
EMDASH_SITE_URL=https://example.com
EMDASH_ALLOWED_ORIGINS=https://preview.example.com,https://staging.example.com
검증
EmDash는 브라우저가 절대 존중하지 않을 데드 구성을 방지하기 위해 이를 검증합니다:
- 각 항목은 후행 점이 없고 호스트 이름에 빈 레이블이 없는 파싱 가능한
http:또는https:URL이어야 합니다. allowedOrigins가 비어 있지 않은 경우siteUrl을 설정해야 하며(어느 소스든) IP 리터럴이거나 후행 점이 있는 호스트 이름이어서는 안 됩니다.- 각 오리진은
siteUrl과 동일한 호스트 이름이거나 그 하위 도메인이어야 합니다. (WebAuthn은rpId가 모든 오리진의 등록 가능한 접미사여야 함을 요구합니다.)
검증이 실패하면 EmDash config error in EMDASH_ALLOWED_ORIGINS: "https://other-site.com" is not a subdomain of siteUrl "https://example.com". Allowed origins must be the same hostname as siteUrl or a subdomain of it.와 같은 소스 속성 오류가 표시됩니다.
오류가 나타나는 위치는 값이 선언된 위치에 따라 다릅니다:
- Astro 시작 시,
config.allowedOrigins와config.siteUrl모두astro.config.mjs에서 오는 경우 — 코드의 오타는 빌드를 실패시킵니다. - 첫 번째 패스키 검증 시, 어느 값이든
EMDASH_ALLOWED_ORIGINS또는EMDASH_SITE_URL에서 오는 경우 — 환경 불일치는 첫 번째 검증 시도 시 500으로 나타납니다.
리버스 프록시 설정
Astro는 공개 호스트가 허용될 때만 **X-Forwarded-***를 반영합니다. 사용자가 도달하는 호스트 이름(및 스킴)에 대해 security.allowedDomains를 구성하세요. **astro dev**에서 Vite가 프록시의 Host 헤더를 수락하도록 일치하는 **vite.server.allowedHosts**를 추가하세요.
먼저 allowedDomains (및 전달된 헤더)를 수정하는 것을 선호하세요; 재구성된 URL이 브라우저 오리진과 여전히 다를 때(TLS가 앞에서 종료되고 업스트림 요청이 **http://**로 유지되는 경우 일반적) **siteUrl**을 사용하세요.
앞에 TLS가 있는 경우 개발 서버를 루프백에 바인딩하는 것(astro dev --host 127.0.0.1)으로 충분한 경우가 많습니다: 프록시는 로컬로 연결되고 **siteUrl**은 공개 HTTPS 오리진과 일치합니다.
프록시가 클라이언트 IP 헤더를 쓰는 경우 trustedProxyHeaders를 설정하여 EmDash의 속도 제한이 공유 “unknown” 키 아래 모든 요청을 버킷하는 대신 실제 클라이언트 IP를 사용할 수 있도록 하세요.
다음 구성은 리버스 프록시 배포를 위해 allowedDomains, vite.server.allowedHosts 및 siteUrl을 함께 설정합니다:
import { defineConfig } from "astro/config";
import emdash, { local } from "emdash/astro";
import { sqlite } from "emdash/db";
export default defineConfig({
security: {
allowedDomains: [
{ hostname: "cms.example.com", protocol: "https" },
{ hostname: "cms.example.com", protocol: "http" },
],
},
vite: {
server: {
allowedHosts: ["cms.example.com"],
},
},
integrations: [
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
siteUrl: "https://cms.example.com",
}),
],
});
trustedProxyHeaders
선택 사항. 제어하는 리버스 프록시 뒤에서 실행할 때 클라이언트 IP 해상도를 신뢰할 헤더입니다. 인증 속도 제한(매직 링크, 가입, 패스키, OAuth 장치 흐름) 및 공개 댓글 엔드포인트에서 사용됩니다.
Cloudflare에서는 요청에 첨부된 cf 객체가 자동으로 사용됩니다 — 일반적으로 이것을 설정할 필요가 없습니다. nginx, Caddy, Traefik, Fly, Railway 등의 뒤에 있는 자체 호스팅 배포에서는 프록시가 쓰는 헤더로 이것을 설정하여 속도 제한이 모든 요청을 “unknown”으로 처리하는 대신 실제 클라이언트 IP로 버킷할 수 있도록 하세요.
다음 예제는 nginx, Caddy 또는 Traefik에서 설정한 x-real-ip 헤더를 신뢰합니다:
emdash({
database: sqlite({ url: "file:./data.db" }),
trustedProxyHeaders: ["x-real-ip"],
});
헤더는 순서대로 시도됩니다. *-forwarded-for와 일치하는 값은 쉼표로 구분된 목록으로 파싱되고 첫 번째 항목이 사용됩니다. 다음 예제는 Fly.io의 헤더를 선호하고 x-forwarded-for로 폴백합니다:
emdash({
trustedProxyHeaders: ["fly-client-ip", "x-forwarded-for"],
});
구성에서 설정되지 않은 경우 EmDash는 EMDASH_TRUSTED_PROXY_HEADERS 환경 변수(쉼표로 구분)를 읽습니다. 구성의 명시적인 빈 배열은 환경 변수를 재정의합니다.
maxUploadSize
선택 사항. 허용되는 최대 미디어 파일 업로드 크기(바이트)입니다. 직접 멀티파트 업로드 및 서명된 URL 업로드 모두에 적용됩니다. 기본값은 52_428_800 (50 MB)입니다. 다음 예제는 제한을 100 MB로 올립니다:
emdash({
database: sqlite({ url: "file:./data.db" }),
storage: local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
}),
maxUploadSize: 100 * 1024 * 1024, // 100 MB
});
| 값 | 설명 |
|---|---|
number (바이트) | 양의 유한 정수여야 합니다 |
| 생략됨 | 기본값은 50 MB |
구성된 제한을 초과하는 업로드는 직접 업로드 경로에서 413 Payload Too Large 응답으로, 서명된 URL 경로에서 400 Validation Error로 거부됩니다.
데이터베이스 어댑터
emdash/db에서 어댑터를 가져옵니다:
import { sqlite, libsql, postgres } from "emdash/db";
sqlite(config)
better-sqlite3를 사용하는 SQLite 데이터베이스입니다. 다음 예제는 로컬 파일에 연결합니다:
| 옵션 | 타입 | 설명 |
|---|---|---|
url | string | file: 접두사가 있는 파일 경로 |
sqlite({ url: "file:./data.db" });
libsql(config)
libSQL 데이터베이스입니다. 다음 예제는 원격 libSQL 데이터베이스에 연결합니다:
| 옵션 | 타입 | 설명 |
|---|---|---|
url | string | 데이터베이스 URL |
authToken | string | 인증 토큰 (로컬 파일의 경우 선택 사항) |
libsql({
url: process.env.LIBSQL_DATABASE_URL,
authToken: process.env.LIBSQL_AUTH_TOKEN,
});
postgres(config)
연결 풀링을 사용하는 PostgreSQL 데이터베이스입니다.
| 옵션 | 타입 | 설명 |
|---|---|---|
connectionString | string | PostgreSQL 연결 URL |
host | string | 데이터베이스 호스트 |
port | number | 데이터베이스 포트 |
database | string | 데이터베이스 이름 |
user | string | 데이터베이스 사용자 |
password | string | 데이터베이스 비밀번호 |
ssl | boolean | SSL 활성화 |
pool.min | number | 최소 풀 크기 (기본값: 0) |
pool.max | number | 최대 풀 크기 (기본값: 10) |
다음 예제는 연결 문자열로 연결합니다:
postgres({ connectionString: process.env.DATABASE_URL });
d1(config)
Cloudflare D1 데이터베이스입니다. @emdash-cms/cloudflare에서 가져옵니다.
| 옵션 | 타입 | 기본값 | 설명 |
|---|---|---|---|
binding | string | — | wrangler.jsonc의 D1 바인딩 이름 |
session | string | "disabled" | 읽기 복제 모드: "disabled", "auto" 또는 "primary-first" |
bookmarkCookie | string | "__em_d1_bookmark" | 세션 북마크용 쿠키 이름 |
다음 예제는 기본 바인딩과 읽기 복제본이 활성화된 것을 보여줍니다:
// 기본
d1({ binding: "DB" });
// 읽기 복제본 사용
d1({ binding: "DB", session: "auto" });
session이 "auto" 또는 "primary-first"인 경우 EmDash는 D1 Sessions API를 사용하여 읽기 쿼리를 가까운 복제본으로 라우팅합니다. 인증된 사용자는 북마크 기반 읽기 후 쓰기 일관성을 얻습니다. 자세한 내용은 데이터베이스 옵션 — 읽기 복제본을 참조하세요.
저장소 어댑터
emdash/astro에서 local 및 s3를 가져옵니다. r2 어댑터는 @emdash-cms/cloudflare에서 가져옵니다:
import emdash, { local, s3 } from "emdash/astro";
import { r2 } from "@emdash-cms/cloudflare";
local(config)
로컬 파일 시스템 저장소입니다. 다음 예제는 로컬 디렉터리에서 업로드를 제공합니다:
| 옵션 | 타입 | 설명 |
|---|---|---|
directory | string | 디렉터리 경로 |
baseUrl | string | 파일 제공을 위한 기본 URL |
local({
directory: "./uploads",
baseUrl: "/_emdash/api/media/file",
});
r2(config)
Cloudflare R2 바인딩입니다. 다음 예제는 공개 URL이 있는 R2 바인딩을 사용합니다:
| 옵션 | 타입 | 설명 |
|---|---|---|
binding | string | R2 바인딩 이름 |
publicUrl | string | 선택적 공개 URL |
r2({
binding: "MEDIA",
publicUrl: "https://pub-xxxx.r2.dev",
});
s3(config?)
S3 호환 저장소입니다. 모든 구성 필드는 선택 사항입니다: s3({...})에서 생략된 필드는
Node 프로세스가 시작될 때 일치하는 S3_* 환경 변수에서 해석됩니다. 명시적 값은 항상 우선합니다.
전제 조건: 프로젝트에 @aws-sdk/client-s3 및 @aws-sdk/s3-request-presigner를 설치하세요.
EmDash 코어는 AWS SDK를 번들하지 않습니다. 자세한 내용은
저장소 옵션: S3 호환 저장소를 참조하세요.
| 옵션 | 타입 | 설명 |
|---|---|---|
endpoint | string | S3 엔드포인트 URL (S3_ENDPOINT) |
bucket | string | 버킷 이름 (S3_BUCKET) |
accessKeyId | string | 액세스 키 (S3_ACCESS_KEY_ID) |
secretAccessKey | string | 비밀 키 (S3_SECRET_ACCESS_KEY) |
region | string | 리전, 기본값 "auto" (S3_REGION) |
publicUrl | string | 선택적 CDN URL (S3_PUBLIC_URL) |
다음 예제는 모든 필드를 환경에서 해석하거나, 구성과 환경을 혼합하거나, 모든 필드를 명시적으로 전달합니다:
// 모든 필드를 S3_* 환경 변수에서 (Node 컨테이너 배포)
s3()
// 혼합: CDN은 구성에서, 나머지는 환경에서
s3({ publicUrl: "https://cdn.example.com" })
// 모두 명시적
s3({
endpoint: "https://xxx.r2.cloudflarestorage.com",
bucket: "media",
accessKeyId: process.env.R2_ACCESS_KEY_ID,
secretAccessKey: process.env.R2_SECRET_ACCESS_KEY,
publicUrl: "https://cdn.example.com",
})
런타임 환경 변수 해석은 Node 전용 기능입니다. Cloudflare
Workers에서 비밀 및 변수는 fetch 핸들러의 env 매개변수를 통해 노출되며,
process.env를 통해서는 노출되지 않으므로 S3_* 환경 변수는 선택되지 않습니다.
Workers 배포는 r2(config) 어댑터를 사용하거나
s3({...})에 명시적 값을 전달해야 합니다. 자세한 내용은
저장소 옵션을 참조하세요.
라이브 컬렉션
src/live.config.ts에서 EmDash 로더를 구성합니다:
import { defineLiveCollection } from "astro:content";
import { emdashLoader } from "emdash/runtime";
export const collections = {
_emdash: defineLiveCollection({
loader: emdashLoader(),
}),
};
로더 옵션
emdashLoader() 함수는 인수를 취하지 않습니다:
emdashLoader();
환경 변수
EmDash는 다음 환경 변수를 존중합니다:
| 변수 | 설명 |
|---|---|
EMDASH_SITE_URL | 공개 브라우저 대면 오리진 (SITE_URL로 폴백) |
EMDASH_ALLOWED_ORIGINS | 패스키 검증에서 허용되는 추가 오리진의 쉼표로 구분된 목록 (다중 하위 도메인 배포). |
EMDASH_DATABASE_URL | 데이터베이스 URL 재정의 |
EMDASH_ENCRYPTION_KEY | 플러그인 비밀을 저장 시 암호화하는 키. 운영자 제공 — 데이터베이스에 저장되지 않습니다. |
EMDASH_PREVIEW_SECRET | 미리보기 HMAC 비밀의 선택적 재정의. 설정되지 않으면 사이트별 안정 값이 생성되어 데이터베이스에 저장됩니다. |
EMDASH_IP_SALT | 댓글 작성자 IP 해시 솔트의 선택적 재정의. 설정되지 않으면 사이트별 안정 값이 생성되어 데이터베이스에 저장됩니다. |
EMDASH_AUTH_SECRET | 레거시. 설정된 경우 IP 솔트 소스로 사용됩니다; 기존 설치는 업그레이드를 통해 안정적인 댓글 작성자 IP 해시를 보존하기 위해 이를 유지해야 합니다. |
EMDASH_URL | 스키마 동기화를 위한 원격 EmDash URL |
다음 명령으로 암호화 키를 생성합니다:
npx emdash secrets generate
package.json 구성
템플릿과 사이트는 package.json의 emdash 키 아래 선택적 메타데이터를 선언할 수 있습니다:
{
"emdash": {
"label": "My Blog Template",
"seed": ".emdash/seed.json",
"url": "https://my-site.pages.dev"
}
}
| 옵션 | 설명 |
|---|---|
label | 표시용 템플릿 이름 |
seed | 시드 JSON 파일 경로 |
url | 스키마 동기화를 위한 원격 URL |
TypeScript 구성
EmDash는 .emdash/types.ts에 타입을 생성합니다. tsconfig.json에 경로 별칭을 추가하세요:
{
"compilerOptions": {
"paths": {
"@emdash-cms/types": ["./.emdash/types.ts"]
}
}
}
다음 명령으로 타입을 생성합니다:
npx emdash types