
HTML 양식을 사용하면 브라우저가 페이지를 새로 고치거나 새 페이지로 이동한다..!
-> 양식 제출을 JS로 가로채서 API 엔드포인트로 보내자!
//index.astro
---
import FeedbackForm from "../components/FeedbackForm"
---
<FeedbackForm client:load /> //SSR표시
//src/components/FeedbackForm.tsx
//양식 입력할 폼
import { useState } from "react";
export default function Form() {
const [responseMessage, setResponseMessage] = useState("");
async function submit(e: SubmitEvent) {
e.preventDefault(); //기존 동작 없애고
const formData = new FormData(e.target as HTMLFormElement);
//api 엔드포인트로 저장된 폼 데이터를 보내기
const response = await fetch("/api/feedback", {
method: "POST",
body: formData,
});
const data = await response.json();
if (data.message) setResponseMessage(data.message);
}
return (
<form onSubmit={submit}>
<label>
Name
<input type="text" id="name" name="name" required />
</label>
<label>
Email
<input type="email" id="email" name="email" required />
</label>
<label>
Message
<textarea id="message" name="message" required />
</label>
<button>Send</button>
{responseMessage && <p>{responseMessage}</p>}
</form>
);
}
//src/pages/api/feedback.ts
//가로챈 양식을 보낼 api 엔드포인트
import type { APIRoute } from "astro";
export const POST: APIRoute = async ({ request }) => {
const data = await request.formData();
const name = data.get("name");
const email = data.get("email");
const message = data.get("message");
// 데이터 유효성 검사
if (name && email && message)
return new Response(
JSON.stringify({
message: "Success!"
}),
{ status: 200 }
);
else {
return new Response(
JSON.stringify({
message: "Missing required fields",
}),
{ status: 400 }
);
}
};
Astro 페이지는 SSR에서 직접 양식을 표시하고 처리할 수 있다!
---
import { isRegistered, registerUser } from "../../data/users"
import { isValidEmail } from "../../utils/isValidEmail";
const errors = { username: "", email: "", password: "" };
if (Astro.request.method === "POST") {
try {
const data = await Astro.request.formData();
const name = data.get("username");
const email = data.get("email");
const password = data.get("password");
//유효성 검사
if (typeof name !== "string" || name.length < 1)
errors.username += "Please enter a username. ";
if (typeof email !== "string" || !isValidEmail(email))
errors.email += "Email is not valid. ";
else if (await isRegistered(email))
errors.email += "Email is already registered. ";
if (typeof password !== "string" || password.length < 6)
errors.password += "Password must be at least 6 characters. ";
const hasErrors = Object.values(errors).some(msg => msg)
if (!hasErrors) {
//작성된 양식을 db에 저장하고 login 페이지로 보내기
await registerUser({name, email, password});
return Astro.redirect("/login");
}
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
}
}
---
<h1>Register</h1>
<form method="POST"> //GET인지 POST인지는 여기서 명시
<label>
Username:
<input type="text" name="username" />
</label>
{errors.username && <p>{errors.username}</p>}
<label>
Email:
<input type="email" name="email" required />
</label>
{errors.email && <p>{errors.email}</p>}
<label>
Password:
<input type="password" name="password" required minlength="6" />
</label>
{errors.password && <p>{errors.password}</p>}
<button>Register</button>
</form>
구글 recaptcha api를 사용하여 인간인지 컴퓨터인지를 검증하자!
//src/pages/index.astro
<html>
<head>
//여긴 왜넣어주는거지
<script is:inline src="https://www.google.com/recaptcha/api.js"></script>
</head>
<body>
<button class="g-recaptcha"
data-sitekey="PUBLIC_SITE_KEY"
data-callback="onSubmit"
data-action="submit"> Click me to verify the captcha challenge! </button>
<script is:inline>
//Google reCAPTCHA API에서는 성공적으로 인증을 마치면 콜백 함수에 토큰이 전달됨!
function onSubmit(token) {
//여기서 token을 url로 보내 이게 통과인지 실패인지 확인해야 함!
fetch("/recaptcha", {
method: "POST",
body: JSON.stringify({ recaptcha: token })
})
.then((response) => response.json())
.then((gResponse) => {
if (gResponse.success) {
// Captcha 검증성공
} else {
// Captcha 검증실패
}
})
}
</script>
</body>
</html>
//src/pages/recaptcha.js
export async function POST({ request }) {
const data = await request.json();
const recaptchaURL = 'https://www.google.com/recaptcha/api/siteverify';
const requestHeaders = {
'Content-Type': 'application/x-www-form-urlencoded'
};
const requestBody = new URLSearchParams({
secret: "YOUR_SITE_SECRET_KEY", // 구글 captcha 서비스 사용시 같이 주는 시크릿키
response: data.recaptcha // captcha 테스트 후 받아온 토큰
});
const response = await fetch(recaptchaURL, {
method: "POST",
headers: requestHeaders,
body: requestBody.toString()
});
const responseData = await response.json();
return new Response(JSON.stringify(responseData), { status: 200 });
}
로컬 이미지는 .astro 파일로 가져와야 한다!
-> import.meta.glob 함수를 사용하면 assets 폴더 내 확장자에 해당하는 모든 콘텐츠를 가져온다.
//src/components/MyCustomCardComponent.astro
---
import type { ImageMetadata } from 'astro';
import { Image } from 'astro:assets';
const extensions='/src/assets/*.{jpeg,jpg,png,gif}';
const { imagePath } = Astro.props;
const images = import.meta.glob<{ default: ImageMetadata }>(extensions);
/*
이런 형태나 마찬가지!
const images = {
'./assets/avatar-1.jpg': () => import('./assets/avatar-1.jpg'),
'./assets/avatar-2.png': () => import('./assets/avatar-2.png'),
'./assets/avatar-3.jpeg': () => import('./assets/avatar-3.jpeg')
}
*/
//유효성 검사
if (!images[imagePath]) throw new Error(`"${imagePath}" does not exist in glob: ${extensions}`);
---
<div class="card">
<Image src={images[imagePath]()} /> //따라서 함수로 넣어야 함!
</div>
//src/pages/index.astro
---
import MyCustomCardComponent from '../components/MyCustomCardComponent.astro';
---
<MyCustomCardComponent
imagePath="/src/assets/avatar-1.jpg" //이미지 경로
/>
외부 사이트를 가리키는 Markdown 파일의 링크를 식별하고 수정할 수 있게 해주는 라이브러리
// astro.config.mjs
import rehypeExternalLinks from 'rehype-external-links';
export default defineConfig({
// ...
markdown: {
rehypePlugins: [
[
rehypeExternalLinks,
{
content: { type: 'text', value: ' 🔗' } //외부링크 끝에 해당 아이콘이 추가된다
}
],
]
},
});