SEO 최적화 홍보 웹사이트(website/) 추가 - #10
Conversation
umsungjun
commented
Aug 6, 2026
- Next.js 15 App Router 기반 독립 홍보 사이트 신설 (npm 배포본 소비, Vercel Root Directory = website 배포)
- route group 이중 루트 레이아웃으로 영어(/)·한국어(/ko) 구성, hreflang/canonical/OG/JSON-LD/robots/sitemap 적용
- 3D 디바이스 쇼케이스(6종), SSR 계약 라이브 데모, npm/yarn/pnpm 클릭 복사 위젯, 사용 예시 섹션 구현
- 정적 OG 이미지(public/og.png)와 Google Search Console 인증 파일 추가
- 루트 website 스크립트, eslint ignore, CLAUDE.md, 양 README 반영
- Next.js 15 App Router 기반 독립 홍보 사이트 신설 (npm 배포본 소비, Vercel Root Directory = website 배포) - route group 이중 루트 레이아웃으로 영어(/)·한국어(/ko) 구성, hreflang/canonical/OG/JSON-LD/robots/sitemap 적용 - 3D 디바이스 쇼케이스(6종), SSR 계약 라이브 데모, npm/yarn/pnpm 클릭 복사 위젯, 사용 예시 섹션 구현 - 정적 OG 이미지(public/og.png)와 Google Search Console 인증 파일 추가 - 루트 website 스크립트, eslint ignore, CLAUDE.md, 양 README 반영
There was a problem hiding this comment.
🟡 Changes recommended
There are verified security and correctness issues (missing noopener on _blank links, unhandled clipboard failures, and a broken .next type reference) that should be fixed before merging.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Pull request overview
Adds a standalone Next.js (App Router) promo/landing website under website/ to showcase react-device-check with bilingual routing and SEO primitives, while keeping the library/examples workflow intact.
Changes:
- Introduces a self-contained
website/Next.js app (routes, components, styling, localized copy). - Implements SEO essentials (metadata builder, JSON-LD, sitemap/robots, OpenGraph/Twitter image references, Google Search Console verification file).
- Updates repo-level docs/scripts/config to reference and accommodate the website (README links,
pnpm website, ESLint ignores, CLAUDE.md).
File summaries
| File | Description |
|---|---|
| website/tsconfig.json | Website TypeScript configuration for Next.js App Router project. |
| website/public/google6b4dd67b252bbb17.html | Adds Google Search Console verification file. |
| website/pnpm-lock.yaml | Locks website dependencies for reproducible installs. |
| website/package.json | Defines website package, scripts, and dependencies. |
| website/next.config.ts | Next.js config: ESLint ignore + tracing root pinning. |
| website/next-env.d.ts | Next.js TypeScript environment references for the website. |
| website/lib/seo.ts | Centralizes SEO constants and metadata/JSON-LD builders. |
| website/lib/fonts.ts | Sets up self-hosted Google fonts via next/font. |
| website/content/types.ts | Strongly typed localization shape for visible strings. |
| website/content/ko.ts | Korean landing-page copy content. |
| website/content/en.ts | English landing-page copy content. |
| website/content/code.ts | Locale-independent install/usage snippet data for UI. |
| website/components/LiveDemo.tsx | Client component live demo powered by useDevice(). |
| website/components/Landing.tsx | Main landing-page composition + JSON-LD injection. |
| website/components/InstallTabs.tsx | Client install command tabs + click-to-copy behavior. |
| website/components/DeviceShowcase.tsx | Static multi-device showcase visuals and copy layout. |
| website/components/CodeBlock.tsx | Reusable code block component. |
| website/app/sitemap.ts | Sitemap generation for en/ko routes with alternates. |
| website/app/robots.ts | Robots metadata endpoint with sitemap reference. |
| website/app/icon.svg | App icon for the website. |
| website/app/globals.css | Complete styling + responsive layout + design tokens. |
| website/app/(ko)/layout.tsx | Korean route-group root layout with lang="ko". |
| website/app/(ko)/ko/page.tsx | Korean home page wiring to localized content + metadata. |
| website/app/(en)/page.tsx | English home page wiring to localized content + metadata. |
| website/app/(en)/layout.tsx | English route-group root layout with lang="en". |
| README.md | Adds website/live demo link and table formatting adjustments. |
| README.ko.md | Adds website/live demo link and table formatting adjustments. |
| package.json | Adds pnpm website convenience script. |
| eslint.config.mjs | Excludes website/ from repo-root ESLint. |
| CLAUDE.md | Documents the website architecture/deployment in repo guidance. |
Review details
Files not reviewed (1)
- website/pnpm-lock.yaml: Generated file
Suppressed comments (5)
website/components/Landing.tsx:95
- Links opened with target="_blank" should include rel="noopener" (in addition to noreferrer) to prevent the opened page from gaining access to window.opener (tabnabbing risk).
rel="noreferrer"
website/components/Landing.tsx:56
- Links opened with target="_blank" should include rel="noopener" (in addition to noreferrer) to prevent the opened page from gaining access to window.opener (tabnabbing risk).
<a href={NPM_URL} target="_blank" rel="noreferrer">
website/components/Landing.tsx:203
- Links opened with target="_blank" should include rel="noopener" (in addition to noreferrer) to prevent the opened page from gaining access to window.opener (tabnabbing risk).
<a href={readmeUrl} target="_blank" rel="noreferrer">
website/components/Landing.tsx:215
- Links opened with target="_blank" should include rel="noopener" (in addition to noreferrer) to prevent the opened page from gaining access to window.opener (tabnabbing risk).
<a href={GITHUB_URL} target="_blank" rel="noreferrer">
website/components/Landing.tsx:218
- Links opened with target="_blank" should include rel="noopener" (in addition to noreferrer) to prevent the opened page from gaining access to window.opener (tabnabbing risk).
<a href={NPM_URL} target="_blank" rel="noreferrer">
- Files reviewed: 27/31 changed files
- Comments generated: 3
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| const handleCopy = async () => { | ||
| await navigator.clipboard.writeText(INSTALL_COMMANDS[active].command); | ||
| setCopied(true); | ||
| }; |
|
|
||
| ### Website | ||
|
|
||
| `website/` is a standalone Next.js 15 promo/landing site (own lockfile, not a workspace member) consuming the **published npm package** — unlike both examples, it needs no root build. English at `/`, Korean at `/ko` via two route-group root layouts (each sets its own `<html lang>`); hreflang/canonical/OG metadata come from `website/lib/seo.ts` (`SITE_URL` is the single deploy-URL definition). The OG image is served by `app/og/route.tsx` (a plain route handler — the `opengraph-image` file convention does not inject meta tags across route-group root layouts). Deployed on Vercel with Root Directory = `website`; excluded from CI, lint, size-limit, and the Playwright E2E matrix. The examples' ports and `data-testid` contracts are untouched by it. |
… OG doc fix - Add noopener to rel of target="_blank" external links (noreferrer already blocks the opener; kept explicit for defense in depth) - Wrap clipboard copy in try/catch so permission denials and insecure contexts fail silently without unhandled rejections - Update the CLAUDE.md OG image description to match the actual implementation (static public/og.png)
There was a problem hiding this comment.
🟡 Changes recommended
The website’s TS path alias setup is incomplete (missing baseUrl), which can break @/ imports during typechecking/build.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
This review doesn't count toward merge requirements. Sign up for the private preview to control whether Copilot approvals count.
Review details
Files not reviewed (1)
- website/pnpm-lock.yaml: Generated file
Suppressed comments (2)
website/components/DeviceShowcase.tsx:159
- This relies on
device.valuesalways havingtypeandosas the first two entries; reordering that array will silently swap or break the displayed verdict. Looking up by key makes the component resilient to changes inDEVICES.
const meta = strings.devices[index];
const [[, type], [, os]] = device.values;
return (
website/components/Landing.tsx:146
- Indexing into
USAGE_EXAMPLESassumes it stays perfectly in sync withs.usage.examples; if they ever diverge, this will throw at runtime. Optional chaining with a safe fallback avoids a hard crash while still rendering the text content.
<CodeBlock
code={USAGE_EXAMPLES[index].code}
filename={USAGE_EXAMPLES[index].filename}
/>
- Files reviewed: 27/31 changed files
- Comments generated: 1
- Review effort level: Lite
We're testing this review assessment. Please use 👍 or 👎 to tell us if it's correct.
| "incremental": true, | ||
| "paths": { | ||
| "@/*": ["./*"] | ||
| }, |