diff --git a/messages/en.json b/messages/en.json index 5ac976c..c06ce60 100644 --- a/messages/en.json +++ b/messages/en.json @@ -221,6 +221,45 @@ "hire": "Work with me" } }, + "privacy": { + "title": "Privacy Policy", + "description": "What this site collects, who collects it, and how to switch it off.", + "metaTitle": "Privacy Policy", + "metaDescription": "How writingdeveloper.blog handles analytics, advertising and comment data through Google Analytics, Google AdSense and giscus, and how to opt out.", + "updatedLabel": "LAST UPDATED", + "updated": "20 August 2026", + "intro": "This site has no accounts, no sign-up and no contact form, so it asks you for nothing directly. Everything described below is collected by third-party services the site loads.", + "direct": { + "heading": "What I collect directly", + "body": "Nothing. There is no login, no newsletter, no form and no database of visitors. I never see your name, email address or postal address through this site." + }, + "analytics": { + "heading": "Analytics", + "body": "Google Analytics 4 uses cookies to record page views, referrers, approximate location at country level, and which outbound links get clicked. Vercel Analytics and Speed Insights record loading performance without cookies and without identifying you.", + "idLabel": "MEASUREMENT ID" + }, + "ads": { + "heading": "Advertising", + "body": "This site participates in Google AdSense. Third-party vendors, including Google, use cookies to serve ads based on your prior visits to this site or other sites. Google's use of advertising cookies enables it and its partners to serve ads to you based on your visit to this site and/or other sites on the internet. You can opt out of personalised advertising in Google Ads Settings, or opt out of third-party vendors' use of cookies at aboutads.info.", + "idLabel": "PUBLISHER ID" + }, + "comments": { + "heading": "Comments", + "body": "Blog posts carry a comment box powered by giscus. Comments are stored as GitHub Discussions in the public repository writingdeveloper/portfolio. Leaving one requires signing in to GitHub and is handled entirely by GitHub under their privacy policy. Reading a page never requires an account." + }, + "hosting": { + "heading": "Hosting", + "body": "The site runs on Vercel, which keeps standard server logs including IP addresses for security and abuse prevention." + }, + "choices": { + "heading": "Your choices", + "body": "Blocking cookies in your browser, or using any content blocker, stops the analytics and advertising scripts entirely. Nothing on this site breaks if you do." + }, + "contact": { + "heading": "Questions", + "body": "Email me and I will answer." + } + }, "accessibility": { "toggleMenu": "Toggle menu", "toggleTheme": "Toggle theme", @@ -248,7 +287,8 @@ "tags": "Tags" }, "footer": { - "copyright": "All rights reserved." + "copyright": "All rights reserved.", + "privacy": "Privacy" }, "play": { "title": "Explore", diff --git a/messages/ko.json b/messages/ko.json index 34cb7c5..64f1b62 100644 --- a/messages/ko.json +++ b/messages/ko.json @@ -221,6 +221,45 @@ "hire": "함께 일하기" } }, + "privacy": { + "title": "개인정보처리방침", + "description": "이 사이트가 무엇을 수집하고, 누가 수집하며, 어떻게 끌 수 있는지.", + "metaTitle": "개인정보처리방침", + "metaDescription": "writingdeveloper.blog가 Google Analytics, Google AdSense, giscus를 통해 다루는 분석·광고·댓글 데이터와 수집을 거부하는 방법을 정리합니다.", + "updatedLabel": "최종 수정", + "updated": "2026년 8월 20일", + "intro": "이 사이트에는 계정도, 가입도, 문의 양식도 없습니다. 그래서 방문자에게 직접 요구하는 정보가 없습니다. 아래 내용은 모두 사이트가 불러오는 외부 서비스가 수집하는 것입니다.", + "direct": { + "heading": "직접 수집하는 정보", + "body": "없습니다. 로그인도, 뉴스레터도, 입력 양식도, 방문자 데이터베이스도 없습니다. 이 사이트를 통해 여러분의 이름이나 이메일, 주소를 보는 일은 없습니다." + }, + "analytics": { + "heading": "분석", + "body": "Google Analytics 4가 쿠키를 사용해 페이지 조회수, 유입 경로, 국가 단위의 대략적인 위치, 외부 링크 클릭을 기록합니다. Vercel Analytics와 Speed Insights는 쿠키 없이, 개인을 식별하지 않고 로딩 성능만 기록합니다.", + "idLabel": "측정 ID" + }, + "ads": { + "heading": "광고", + "body": "이 사이트는 Google AdSense에 참여합니다. Google을 포함한 제3자 공급업체는 쿠키를 사용해 이 사이트 또는 다른 사이트의 이전 방문 기록을 바탕으로 광고를 게재합니다. Google이 광고 쿠키를 사용함으로써 Google과 그 파트너는 이 사이트 및 인터넷상의 다른 사이트 방문 기록을 근거로 광고를 게재할 수 있습니다. 맞춤 광고는 Google 광고 설정에서 사용 중지할 수 있고, 제3자 공급업체의 쿠키 사용은 aboutads.info에서 거부할 수 있습니다.", + "idLabel": "게시자 ID" + }, + "comments": { + "heading": "댓글", + "body": "블로그 글에는 giscus로 만든 댓글창이 있습니다. 댓글은 공개 저장소인 writingdeveloper/portfolio의 GitHub Discussions에 저장됩니다. 댓글을 남기려면 GitHub 로그인이 필요하고, 이 과정은 전적으로 GitHub가 자체 개인정보처리방침에 따라 처리합니다. 글을 읽는 데는 계정이 필요하지 않습니다." + }, + "hosting": { + "heading": "호스팅", + "body": "사이트는 Vercel에서 구동되며, Vercel은 보안과 남용 방지를 위해 IP 주소를 포함한 일반적인 서버 로그를 보관합니다." + }, + "choices": { + "heading": "선택권", + "body": "브라우저에서 쿠키를 차단하거나 콘텐츠 차단기를 사용하면 분석과 광고 스크립트는 완전히 멈춥니다. 그렇게 해도 이 사이트의 기능은 아무것도 망가지지 않습니다." + }, + "contact": { + "heading": "문의", + "body": "메일 주시면 답하겠습니다." + } + }, "accessibility": { "toggleMenu": "메뉴 열기/닫기", "toggleTheme": "테마 변경", @@ -248,7 +287,8 @@ "tags": "태그" }, "footer": { - "copyright": "모든 권리 보유." + "copyright": "모든 권리 보유.", + "privacy": "개인정보처리방침" }, "play": { "title": "탐험", diff --git a/src/app/[locale]/LedgerHome.tsx b/src/app/[locale]/LedgerHome.tsx index 80808f9..2f2f27b 100644 --- a/src/app/[locale]/LedgerHome.tsx +++ b/src/app/[locale]/LedgerHome.tsx @@ -309,9 +309,18 @@ export function LedgerHome({ projects, tombstones, posts, locale }: {
© 2026 WRITINGDEVELOPER · LEDGER NO. 001 - - EXPLORE 3D → - + {/* The immersive home renders without the site Footer, so the + policy link has to be repeated here — AdSense requires it to + be reachable from every page, and this is the likeliest + landing page of the lot. */} +
+ + PRIVACY + + + EXPLORE 3D → + +
diff --git a/src/app/[locale]/privacy/page.tsx b/src/app/[locale]/privacy/page.tsx new file mode 100644 index 0000000..82024ce --- /dev/null +++ b/src/app/[locale]/privacy/page.tsx @@ -0,0 +1,200 @@ +import type { ReactNode } from 'react' +import { useTranslations, useLocale } from 'next-intl' +import { setRequestLocale, getTranslations } from 'next-intl/server' +import type { Metadata } from 'next' +import { PageTransition } from '@/components/ui/PageTransition' +import { + SITE_URL, + CONTACT_EMAIL, + ADSENSE_CLIENT, + GA_MEASUREMENT_ID, +} from '@/lib/constants' +import { generateBreadcrumbJsonLd, safeJsonLd } from '@/lib/seo' + +// The copy is static: it only moves when the set of third-party scripts moves, +// and that is a code change anyway. Same cadence as /hire and /about. +export const revalidate = 3600 + +/** + * Opt-out and policy destinations named in the disclosure. + * + * AdSense requires the policy to *link* the opt-out, not merely mention it, so + * these are part of the compliance surface rather than decoration — if one dies + * the page stops satisfying the requirement it exists for. + */ +const ADS_SETTINGS_URL = 'https://myadcenter.google.com/' +const ABOUT_ADS_URL = 'https://www.aboutads.info/choices/' +const GISCUS_URL = 'https://giscus.app' +const GITHUB_PRIVACY_URL = + 'https://docs.github.com/en/site-policy/privacy-policies/github-general-privacy-statement' + +export async function generateMetadata({ + params, +}: { + params: Promise<{ locale: string }> +}): Promise { + const { locale } = await params + const t = await getTranslations({ locale, namespace: 'privacy' }) + const localePath = locale === 'ko' ? '' : `/${locale}` + const pageUrl = `${SITE_URL}${localePath}/privacy` + + return { + title: t('metaTitle'), + description: t('metaDescription'), + openGraph: { + url: pageUrl, + title: t('metaTitle'), + description: t('metaDescription'), + locale: locale === 'ko' ? 'ko_KR' : 'en_US', + alternateLocale: locale === 'ko' ? ['en_US'] : ['ko_KR'], + type: 'article', + images: [ + { + url: `${SITE_URL}/api/og?title=${encodeURIComponent(t('title'))}&description=${encodeURIComponent(t('description'))}`, + width: 1200, + height: 630, + alt: t('title'), + }, + ], + }, + alternates: { + canonical: pageUrl, + languages: { + ko: `${SITE_URL}/privacy`, + en: `${SITE_URL}/en/privacy`, + 'x-default': `${SITE_URL}/privacy`, + }, + }, + } +} + +export default async function PrivacyPage({ + params, +}: { + params: Promise<{ locale: string }> +}) { + const { locale } = await params + setRequestLocale(locale) + return +} + +/** An outbound link inside body copy. */ +function Ext({ href, children }: { href: string; children: ReactNode }) { + return ( + + {children} + + ) +} + +/** One disclosure block: a heading, the prose, and any identifier it names. */ +function Section({ + heading, + children, + idLabel, + idValue, +}: { + heading: string + children: ReactNode + idLabel?: string + idValue?: string +}) { + return ( +
+

{heading}

+

{children}

+ {idLabel && idValue && ( +

+ + {idLabel} + {' '} + {idValue} +

+ )} +
+ ) +} + +function PrivacyContent() { + const t = useTranslations('privacy') + const locale = useLocale() + const localePath = locale === 'ko' ? '' : '/en' + + const breadcrumbJsonLd = generateBreadcrumbJsonLd([ + { name: locale === 'ko' ? '홈' : 'Home', url: `${SITE_URL}${localePath}` }, + { name: t('title'), url: `${SITE_URL}${localePath}/privacy` }, + ]) + + return ( + +