diff --git a/index.html b/index.html index 75c26b9..6c20d1b 100644 --- a/index.html +++ b/index.html @@ -2,109 +2,367 @@ + + Worknot: Notion Custom Domain with Cloudflare Workers - + + + + + + + - - - - Worknot - Host Notion Sites on Your Custom Domain | Cloudflare - Workers - - - - + - + - + + - - + - - - - - - - - - - - - - - -
+ + + + +
+
+
+
+

Free and open source

+

Host your Notion Site on your own domain

+

+ Worknot generates a Cloudflare Worker script that serves + your published Notion pages on your custom domain, with + pretty URLs, SEO metadata and analytics. +

+ +
    +
  • MIT License
  • +
  • Runs in your Cloudflare account
  • +
  • Notion changes checked daily
  • +
+
+ +
+
+

Your visitors open

+

https://www.example.com/about

+
+ +
+

Cloudflare Worker generated by Worknot

+
    +
  • Pretty URLs
  • +
  • SEO tags
  • +
  • Analytics
  • +
  • Image optimization
  • +
+
+ +
+

Your page on Notion Sites

+

https://example.notion.site/About-1a2b3c

+
+
+ Requests to your domain go through the Worker, which loads + the page from Notion Sites and serves it under your address. +
+
+
+
+ +
+
+

What Worknot adds to your Notion Site

+

+ Configure each option in the generator below. Worknot is + maintained by + Classmethod (Europe) GmbH, + a Cloudflare and Notion partner. +

+
+
+

Custom domain and pretty URLs

+

Serve your Notion Site on your own domain and map pages to readable paths such as /about or /blog.

+
+
+

SEO metadata

+

Set page titles, descriptions, Open Graph tags, JSON-LD, hreflang, a sitemap.xml and an RSS feed.

+
+
+

Analytics

+

Add Google Analytics 4 or a Facebook Pixel without changing your Notion pages.

+
+
+

Image optimization

+

Resize, convert and compress Notion images at the edge with Cloudflare Image Transformations.

+
+
+

Branding and custom code

+

Add custom CSS and JavaScript, a Google Font, your favicon, header HTML and a custom 404 page.

+
+
+

Daily compatibility checks

+

Every day at 06:00 UTC, a scheduled job checks Notion's client code and opens a GitHub issue when something changes.

+
+
+
+
+ +
+
+

How it works

+

+ You need a page published with Notion Sites and a domain + whose DNS is managed by Cloudflare. +

+
    +
  1. +

    Publish your page

    +

    In Notion, open Share, choose Publish and copy the notion.site URL of your page.

    +
  2. +
  3. +

    Generate the script

    +

    Enter your domain and the Notion URL below, add pretty links and optional settings, then copy the script.

    +
  4. +
  5. +

    Deploy the Worker

    +

    In the Cloudflare dashboard, create a Worker, replace its code with the script and deploy it.

    +
  6. +
  7. +

    Connect your domain

    +

    Add your domain to the Worker as a Custom Domain. Your Notion Site is now available at your own address.

    +
  8. +
+
+
+ +
+
+

Generate your Worker script

+

+ The script is generated in your browser as you type. Your + settings are not sent anywhere. +

+
+ +
+
+
+ +
+
+

Frequently asked questions

+
+

Is Worknot free to use?

+

Yes. Worknot is open source under the MIT License. You need a Cloudflare account to run the Worker. A first visit to a Notion Site makes roughly 700 Worker requests, so the Workers Free plan (100,000 requests per day) suits small sites. Use Workers Paid for sites with regular traffic.

+
+
+

Do I need a paid Notion plan?

+

No. Worknot works with any page you publish to the web with Notion Sites. It does not use Notion's own custom domain feature.

+
+
+

What happens when Notion changes its client?

+

A GitHub Actions workflow checks Notion's client code and renders a test site every day at 06:00 UTC, and opens an issue when something changes. When a fix is released, generate the script again here and redeploy your Worker.

+
+
+

Does Worknot store my settings or content?

+

No. The generator runs entirely in your browser and does not send your settings anywhere. Your site's traffic goes through the Worker in your own Cloudflare account.

+
+
+

How is Worknot related to Fruition?

+

Worknot started as a fork of Fruition by Stephen Ou. Classmethod (Europe) GmbH maintains it and keeps it compatible with current Notion Sites.

+
+
+
+
+ + + diff --git a/ja/index.html b/ja/index.html new file mode 100644 index 0000000..dee42c9 --- /dev/null +++ b/ja/index.html @@ -0,0 +1,359 @@ + + + + + + Notion Sites を独自ドメインで公開 | Worknot + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+
+
+
+

無料・オープンソース

+

Notion Sites を独自ドメインで公開

+

+ Worknot は、Notion Sites で公開したページを独自ドメインで配信する + Cloudflare Worker のスクリプトを生成します。読みやすい URL、SEO + メタデータ、アナリティクスにも対応しています。 +

+ +
    +
  • MIT ライセンス
  • +
  • お使いの Cloudflare アカウントで動作
  • +
  • Notion の変更を毎日チェック
  • +
+
+ +
+
+

訪問者が開く URL

+

https://www.example.com/about

+
+ +
+

Worknot が生成する Cloudflare Worker

+
    +
  • 読みやすい URL
  • +
  • SEO タグ
  • +
  • アナリティクス
  • +
  • 画像の最適化
  • +
+
+ +
+

Notion Sites のページ

+

https://example.notion.site/About-1a2b3c

+
+
+ 独自ドメインへのリクエストは Worker を経由します。Worker が Notion + Sites からページを取得し、お使いのドメインで配信します。 +
+
+
+
+ +
+
+

Worknot でできること

+

+ 各機能は下のジェネレーターで設定できます。Worknot は、Cloudflare と + Notion のパートナーである + Classmethod (Europe) GmbH + がメンテナンスしています。 +

+
+
+

独自ドメインと読みやすい URL

+

Notion Sites のページを独自ドメインで配信し、/about や /blog のような読みやすいパスに割り当てられます。

+
+
+

SEO メタデータ

+

ページタイトル、説明文、Open Graph タグ、JSON-LD、hreflang、sitemap.xml、RSS フィードを設定できます。

+
+
+

アナリティクス

+

Notion のページを変更せずに、Google Analytics 4 や Facebook Pixel を追加できます。

+
+
+

画像の最適化

+

Cloudflare Image Transformations で、Notion の画像をエッジでリサイズ、変換、圧縮します。

+
+
+

ブランディングとカスタムコード

+

カスタム CSS と JavaScript、Google Fonts のフォント、ファビコン、ヘッダー HTML、カスタム 404 ページを追加できます。

+
+
+

毎日の互換性チェック

+

毎日 06:00 UTC に Notion のクライアントコードを自動でチェックし、変更があれば GitHub に Issue を作成します。

+
+
+
+
+ +
+
+

使い方

+

+ Notion Sites で公開したページと、DNS を Cloudflare で管理しているドメインが必要です。 +

+
    +
  1. +

    ページを公開する

    +

    Notion で「共有」を開いて「公開」を選び、ページの notion.site の URL をコピーします。

    +
  2. +
  3. +

    スクリプトを生成する

    +

    下のフォームにドメインと Notion の URL を入力し、必要に応じて Pretty Link やオプションを設定して、スクリプトをコピーします。

    +
  4. +
  5. +

    Worker をデプロイする

    +

    Cloudflare のダッシュボードで Worker を作成し、コードをスクリプトに置き換えてデプロイします。

    +
  6. +
  7. +

    ドメインを接続する

    +

    Worker にドメインをカスタムドメインとして追加します。これで Notion Sites のページが独自ドメインで表示されます。

    +
  8. +
+
+
+ +
+
+

Worker スクリプトを生成

+

+ スクリプトは入力に合わせてブラウザー内で生成され、設定内容はどこにも送信されません。フォームは英語表記です。 +

+
+ +
+
+
+ +
+
+

よくある質問

+
+

Worknot は無料で使えますか?

+

はい。Worknot は MIT ライセンスのオープンソースです。Worker を動かすには Cloudflare のアカウントが必要です。Notion Sites のページを初めて開くと Worker へのリクエストが約 700 件発生するため、Workers Free プラン(1 日 100,000 リクエスト)は小規模なサイト向けです。継続的にアクセスがあるサイトでは Workers Paid をお使いください。

+
+
+

Notion の有料プランは必要ですか?

+

いいえ。Notion Sites で Web に公開したページであれば利用できます。Notion の独自ドメイン機能は使用しません。

+
+
+

Notion のクライアントが変更されたらどうなりますか?

+

GitHub Actions のワークフローが毎日 06:00 UTC に Notion のクライアントコードのチェックとテストサイトの表示確認を行い、変更を検出すると Issue を作成します。修正がリリースされたら、このページでスクリプトを生成し直し、Worker を再デプロイしてください。

+
+
+

Worknot は設定やコンテンツを保存しますか?

+

いいえ。ジェネレーターはすべてブラウザー内で動作し、設定内容をどこにも送信しません。サイトへのアクセスは、お使いの Cloudflare アカウントの Worker を経由します。

+
+
+

Worknot と Fruition の関係は?

+

Worknot は Stephen Ou 氏が開発した Fruition のフォークとして始まりました。現在は Classmethod (Europe) GmbH がメンテナンスし、最新の Notion Sites に対応させています。

+
+
+
+
+ + + + + + diff --git a/public/apple-touch-icon.png b/public/apple-touch-icon.png index 3b49ae7..f6b00e4 100644 Binary files a/public/apple-touch-icon.png and b/public/apple-touch-icon.png differ diff --git a/public/brand/classmethod-europe-black.svg b/public/brand/classmethod-europe-black.svg new file mode 100644 index 0000000..a631ec3 --- /dev/null +++ b/public/brand/classmethod-europe-black.svg @@ -0,0 +1,25 @@ + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/brand/classmethod-europe-white.svg b/public/brand/classmethod-europe-white.svg new file mode 100644 index 0000000..f47fa58 --- /dev/null +++ b/public/brand/classmethod-europe-white.svg @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/public/favicon.svg b/public/favicon.svg index 2477514..7fb3282 100644 --- a/public/favicon.svg +++ b/public/favicon.svg @@ -1,4 +1,4 @@ - - - W + + + diff --git a/public/fonts/OFL.txt b/public/fonts/OFL.txt new file mode 100644 index 0000000..c35c4c6 --- /dev/null +++ b/public/fonts/OFL.txt @@ -0,0 +1,93 @@ +Copyright © 2017 IBM Corp. with Reserved Font Name "Plex" + +This Font Software is licensed under the SIL Open Font License, Version 1.1. + +This license is copied below, and is also available with a FAQ at: http://scripts.sil.org/OFL + + +----------------------------------------------------------- +SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 +----------------------------------------------------------- + +PREAMBLE +The goals of the Open Font License (OFL) are to stimulate worldwide +development of collaborative font projects, to support the font creation +efforts of academic and linguistic communities, and to provide a free and +open framework in which fonts may be shared and improved in partnership +with others. + +The OFL allows the licensed fonts to be used, studied, modified and +redistributed freely as long as they are not sold by themselves. The +fonts, including any derivative works, can be bundled, embedded, +redistributed and/or sold with any software provided that any reserved +names are not used by derivative works. The fonts and derivatives, +however, cannot be released under any other type of license. The +requirement for fonts to remain under this license does not apply +to any document created using the fonts or their derivatives. + +DEFINITIONS +"Font Software" refers to the set of files released by the Copyright +Holder(s) under this license and clearly marked as such. This may +include source files, build scripts and documentation. + +"Reserved Font Name" refers to any names specified as such after the +copyright statement(s). + +"Original Version" refers to the collection of Font Software components as +distributed by the Copyright Holder(s). + +"Modified Version" refers to any derivative made by adding to, deleting, +or substituting -- in part or in whole -- any of the components of the +Original Version, by changing formats or by porting the Font Software to a +new environment. + +"Author" refers to any designer, engineer, programmer, technical +writer or other person who contributed to the Font Software. + +PERMISSION & CONDITIONS +Permission is hereby granted, free of charge, to any person obtaining +a copy of the Font Software, to use, study, copy, merge, embed, modify, +redistribute, and sell modified and unmodified copies of the Font +Software, subject to the following conditions: + +1) Neither the Font Software nor any of its individual components, +in Original or Modified Versions, may be sold by itself. + +2) Original or Modified Versions of the Font Software may be bundled, +redistributed and/or sold with any software, provided that each copy +contains the above copyright notice and this license. These can be +included either as stand-alone text files, human-readable headers or +in the appropriate machine-readable metadata fields within text or +binary files as long as those fields can be easily viewed by the user. + +3) No Modified Version of the Font Software may use the Reserved Font +Name(s) unless explicit written permission is granted by the corresponding +Copyright Holder. This restriction only applies to the primary font name as +presented to the users. + +4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font +Software shall not be used to promote, endorse or advertise any +Modified Version, except to acknowledge the contribution(s) of the +Copyright Holder(s) and the Author(s) or with their explicit written +permission. + +5) The Font Software, modified or unmodified, in part or in whole, +must be distributed entirely under this license, and must not be +distributed under any other license. The requirement for fonts to +remain under this license does not apply to any document created +using the Font Software. + +TERMINATION +This license becomes null and void if any of the above conditions are +not met. + +DISCLAIMER +THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT +OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE +COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, +INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL +DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING +FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM +OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/public/fonts/ibm-plex-sans-latin.woff2 b/public/fonts/ibm-plex-sans-latin.woff2 new file mode 100644 index 0000000..e5ebabb Binary files /dev/null and b/public/fonts/ibm-plex-sans-latin.woff2 differ diff --git a/public/og-image-ja.png b/public/og-image-ja.png new file mode 100644 index 0000000..4f24eb8 Binary files /dev/null and b/public/og-image-ja.png differ diff --git a/public/og-image.png b/public/og-image.png index 4713acf..50aae3e 100644 Binary files a/public/og-image.png and b/public/og-image.png differ diff --git a/public/og-image.svg b/public/og-image.svg deleted file mode 100644 index 14b17a1..0000000 --- a/public/og-image.svg +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - - - - Worknot - Host Notion Sites on Your Custom Domain - Powered by Cloudflare Workers - diff --git a/public/sitemap.xml b/public/sitemap.xml index 21f4dce..91deed5 100644 --- a/public/sitemap.xml +++ b/public/sitemap.xml @@ -1,8 +1,17 @@ - + https://worknot.classmethod.live/ - weekly - 1.0 + 2026-10-28 + + + + + + https://worknot.classmethod.live/ja/ + 2026-10-28 + + + diff --git a/src/App.tsx b/src/App.tsx index 37bbaa2..a7e3f96 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -3,7 +3,6 @@ import { useState, type ChangeEvent, type Dispatch, - type ReactNode, type SetStateAction, } from "react"; import { @@ -11,7 +10,6 @@ import { Collapse, InputAdornment, TextField, - Container, RadioGroup, Radio, Box, @@ -25,22 +23,15 @@ import { Typography, SelectChangeEvent, Paper, - Chip, Link, Switch, } from "@mui/material"; import { - Language as LanguageIcon, - Speed as SpeedIcon, - Image as ImageIcon, - Code as CodeIcon, ContentCopy as CopyIcon, Check as CheckIcon, Add as AddIcon, Delete as DeleteIcon, Settings as SettingsIcon, - Update as UpdateIcon, - Verified as VerifiedIcon, ExpandMore as ExpandMoreIcon, } from "@mui/icons-material"; import code, { @@ -63,9 +54,8 @@ import code, { I18nOptions, OgImageGenerationOptions, } from "./code"; -import "./styles.css"; -const DEFAULT_DOMAIN = "worknot.classmethod.cf"; +const DEFAULT_DOMAIN = "www.example.com"; const DEFAULT_NOTION_URL = "https://succinct-scar-f20.notion.site/Sample-Web-Site-148f2fc322e74473a91fb4d90836e3ce"; @@ -224,39 +214,6 @@ interface SlugRow { expanded: boolean; } -interface FeatureCardProps { - icon: ReactNode; - title: string; - description: string; -} - -function FeatureCard({ icon, title, description }: FeatureCardProps) { - return ( - - {icon} - - {title} - - - {description} - - - ); -} - export default function App() { const [slugRows, setSlugRows] = useState([]); const nextRowId = useRef(0); @@ -680,1608 +637,1370 @@ export default function App() { } return ( - - {/* Hero Section */} - + https:// + ), + }, }} - > - + + + {slugRows.map((row, index) => ( + - - - - - } - label="Maintained by Classmethod" - sx={{ - backgroundColor: "rgba(255,255,255,0.2)", - color: "white", - fontWeight: 500, - }} - /> - - - Worknot - - - Host your Notion Site on a custom domain with Cloudflare Workers - - - Full SEO control, custom branding, analytics, and automatic - compatibility with the latest Notion updates - - - - - {/* Features Section */} - - - - } - title="Custom Domain" - description="Host your Notion Site on your own domain with pretty URLs" - /> - } - title="Auto-Compatible" - description="Adapts to Notion client updates with daily monitoring" - /> - } - title="SEO & Analytics" - description="Open Graph, JSON-LD, sitemap, GA4, and Facebook Pixel" - /> - } - title="Image Optimization" - description="Resize, convert, and compress via Cloudflare CDN" - /> - } - title="Full Customization" - description="Custom CSS, JS, fonts, favicon, headers, and 404 pages" - /> - } - title="Partner Maintained" - description="By Classmethod, a Cloudflare and Notion partner" - /> - - - - {/* Generator Section */} - - - Generate Your Worker Script - - https:// + {`${domain}/`} ), }, }} + label="Pretty Link" + margin="normal" + placeholder="about" + error={!!slugErrors[index].slug} + helperText={slugErrors[index].slug} + onChange={(e) => handleCustomURL(e.target.value, row.id)} + value={row.slug} + variant="outlined" + size="small" /> handleNotionPageURL(e.target.value, row.id)} + value={row.notionUrl} variant="outlined" + size="small" /> - - {slugRows.map((row, index) => ( - + + {row.slug && ( + + )} + + + + + Custom metadata for /{row.slug} + {`${domain}/`} - ), - }, - }} - label="Pretty Link" - margin="normal" - placeholder="about" - error={!!slugErrors[index].slug} - helperText={slugErrors[index].slug} - onChange={(e) => handleCustomURL(e.target.value, row.id)} - value={row.slug} + label="Page Title" + margin="dense" + placeholder={pageTitle || "Custom title for this page"} + onChange={(e) => + handlePageMetadata(row.id, "title", e.target.value) + } + value={row.metadata.title || ""} variant="outlined" size="small" /> handleNotionPageURL(e.target.value, row.id)} - value={row.notionUrl} + label="Page Description" + margin="dense" + placeholder={ + pageDescription || "Custom description for this page" + } + onChange={(e) => + handlePageMetadata(row.id, "description", e.target.value) + } + value={row.metadata.description || ""} variant="outlined" size="small" /> - - - {row.slug && ( - - )} - - - + + handlePageMetadata(row.id, "ogImage", e.target.value) + } + value={row.metadata.ogImage || ""} + variant="outlined" + size="small" + /> + {i18n.enabled && ( + - Custom metadata for /{row.slug} + Alternate Language Versions (hreflang) - - handlePageMetadata(row.id, "title", e.target.value) - } - value={row.metadata.title || ""} - variant="outlined" - size="small" - /> - - handlePageMetadata(row.id, "description", e.target.value) - } - value={row.metadata.description || ""} - variant="outlined" - size="small" - /> - - handlePageMetadata(row.id, "ogImage", e.target.value) - } - value={row.metadata.ogImage || ""} - variant="outlined" - size="small" - /> - {i18n.enabled && ( - - - Alternate Language Versions (hreflang) - - {(row.metadata.alternates || []).map( - (alt, altIndex) => ( - - - handlePageAlternate( - row.id, - altIndex, - "locale", - e.target.value, - ) - } - variant="outlined" - size="small" - sx={{ width: "100px" }} - /> - - handlePageAlternate( - row.id, - altIndex, - "slug", - e.target.value, - ) - } - variant="outlined" - size="small" - sx={{ flex: 1 }} - /> - - - ), - )} - - + + handlePageAlternate( + row.id, + altIndex, + "locale", + e.target.value, + ) + } + variant="outlined" + size="small" + sx={{ width: "100px" }} + /> + + handlePageAlternate( + row.id, + altIndex, + "slug", + e.target.value, + ) + } + variant="outlined" + size="small" + sx={{ flex: 1 }} + /> + + + ), )} + - - - ))} + )} + + + + ))} - - + } + > + {optional ? "Hide" : "Show"} Advanced Settings + + + + + + + Page Metadata + + + + + + + + + + Branding & Social + + handleBrandingChange("siteName", e.target.value)} + value={branding.siteName} + variant="outlined" size="small" + /> + + handleBrandingChange("brandReplacement", e.target.value) + } + value={branding.brandReplacement} variant="outlined" - startIcon={} - > - Add Pretty Link - - + + handleBrandingChange("twitterHandle", e.target.value) + } + value={branding.twitterHandle} variant="outlined" - color="secondary" - startIcon={} - > - {optional ? "Hide" : "Show"} Advanced Settings - - + size="small" + /> + + handleBrandingChange("faviconUrl", e.target.value) + } + value={branding.faviconUrl} + variant="outlined" + size="small" + /> + - - - - Page Metadata - - - - + + + Social Preview + + + handleSocialPreviewChange("defaultImage", e.target.value) + } + value={socialPreview.defaultImage} + variant="outlined" + size="small" + /> + + handleSocialPreviewChange( + "imageWidth", + Number(e.target.value), + ) + } + value={socialPreview.imageWidth} variant="outlined" size="small" + sx={{ flex: 1 }} /> + handleSocialPreviewChange( + "imageHeight", + Number(e.target.value), + ) + } + value={socialPreview.imageHeight} variant="outlined" size="small" + sx={{ flex: 1 }} /> + + + + Twitter Card Type + + + + + handleSocialPreviewChange("locale", e.target.value) + } + value={socialPreview.locale} + variant="outlined" + size="small" + /> + - - - Branding & Social - - - handleBrandingChange("siteName", e.target.value) - } - value={branding.siteName} - variant="outlined" - size="small" - /> - - handleBrandingChange("brandReplacement", e.target.value) - } - value={branding.brandReplacement} - variant="outlined" - size="small" - /> - - handleBrandingChange("twitterHandle", e.target.value) - } - value={branding.twitterHandle} + + + SEO & AI Attribution + + handleSeoChange("aiAttribution", e.target.value)} + value={seo.aiAttribution} + variant="outlined" + size="small" + /> + handleSeoChange("robotsRules", e.target.value)} + value={seo.robotsRules} + variant="outlined" + size="small" + /> + + + + + Analytics + + + handleAnalyticsChange("googleTagId", e.target.value) + } + value={analytics.googleTagId} + variant="outlined" + size="small" + /> + + handleAnalyticsChange("facebookPixelId", e.target.value) + } + value={analytics.facebookPixelId} + variant="outlined" + size="small" + /> + + + + + + + Cache-Control Headers + + + Improve performance with browser caching + + + + handleCachingChange("enabled", e.target.checked) + } + /> + + + + + handleCachingChange("htmlTtl", Number(e.target.value)) + } + value={caching.htmlTtl} variant="outlined" size="small" /> - handleBrandingChange("faviconUrl", e.target.value) + handleCachingChange( + "staticAssetsTtl", + Number(e.target.value), + ) } - value={branding.faviconUrl} + value={caching.staticAssetsTtl} variant="outlined" size="small" /> - - - - - Social Preview - - handleSocialPreviewChange("defaultImage", e.target.value) + handleCachingChange("imageTtl", Number(e.target.value)) } - value={socialPreview.defaultImage} + value={caching.imageTtl} variant="outlined" size="small" /> - + + + + + + + Custom Header HTML + + + Home + About +`} + helperText="HTML injected at the top of page body (e.g., navigation, announcements)" + onChange={(e) => + handleCustomHtmlChange("headerHtml", e.target.value) + } + value={customHtml.headerHtml} + variant="outlined" + size="small" + /> + `} + helperText="HTML injected at the start of (e.g., site verification meta tags, consent scripts)" + onChange={(e) => + handleCustomHtmlChange("headHtml", e.target.value) + } + value={customHtml.headHtml} + variant="outlined" + size="small" + /> + + + + + Custom 404 Page + + + handleCustom404Change("notionUrl", e.target.value) + } + value={custom404.notionUrl} + variant="outlined" + size="small" + /> + + + + + Subdomain Redirects + + + Redirect subdomains (e.g., www) to the main domain or other URLs + + {subdomainRedirects.map((redirect, index) => ( + + - handleSocialPreviewChange( - "imageWidth", - Number(e.target.value), + handleSubdomainRedirectChange( + index, + "subdomain", + e.target.value, ) } - value={socialPreview.imageWidth} variant="outlined" size="small" sx={{ flex: 1 }} /> - handleSocialPreviewChange( - "imageHeight", - Number(e.target.value), + handleSubdomainRedirectChange( + index, + "redirectUrl", + e.target.value, ) } - value={socialPreview.imageHeight} + variant="outlined" + size="small" + sx={{ flex: 2 }} + /> + + + + ))} + + + + + + URL Redirect Rules + + + Redirect specific paths to other URLs (301 permanent / 302 + temporary). End a path with * to match a prefix, e.g. + /blog/* to /posts/*. For thousands of URLs, use{" "} + + Cloudflare Bulk Redirects + + . + + {redirectRules.map((rule, index) => ( + + + + handleRedirectRuleChange(index, "from", e.target.value) + } variant="outlined" size="small" sx={{ flex: 1 }} /> + + handleRedirectRuleChange(index, "to", e.target.value) + } + variant="outlined" + size="small" + sx={{ flex: 2 }} + /> + + handleRedirectRuleChange( + index, + "permanent", + e.target.checked, + ) + } + size="small" + /> + } + label="301" + sx={{ minWidth: 70 }} + /> + + + ))} + + + + + + + + JSON-LD Structured Data + + + Enable rich snippets in search results + + + + handleStructuredDataChange("enabled", e.target.checked) + } + /> + + + - - Twitter Card Type - + Schema Type - handleSocialPreviewChange("locale", e.target.value) + handleStructuredDataChange( + "organizationName", + e.target.value, + ) } - value={socialPreview.locale} + value={structuredData.organizationName} variant="outlined" size="small" /> - - - - - SEO & AI Attribution - - handleSeoChange("aiAttribution", e.target.value) + handleStructuredDataChange("logoUrl", e.target.value) } - value={seo.aiAttribution} - variant="outlined" - size="small" - /> - handleSeoChange("robotsRules", e.target.value)} - value={seo.robotsRules} + value={structuredData.logoUrl} variant="outlined" size="small" /> + + Structured data helps search engines understand your content + and display rich results. Validate with{" "} + + Google Rich Results Test + + . + + + - - - Analytics + + + + + RSS Feed + + Generate RSS 2.0 feed at /rss.xml + + + handleRssChange("enabled", e.target.checked)} + /> + + + - handleAnalyticsChange("googleTagId", e.target.value) - } - value={analytics.googleTagId} + placeholder="My Blog" + helperText="Title shown in RSS readers (defaults to domain)" + onChange={(e) => handleRssChange("title", e.target.value)} + value={rss.title} variant="outlined" size="small" /> - handleAnalyticsChange("facebookPixelId", e.target.value) + handleRssChange("description", e.target.value) } - value={analytics.facebookPixelId} + value={rss.description} variant="outlined" size="small" /> - - - - - - - Cache-Control Headers - - - Improve performance with browser caching - - - + Language + + + + RSS feed will include all pages defined in Pretty Links. Add + page metadata for better feed content. + + + - - - Custom Header HTML + + + + + Internationalization (i18n) - - Home - About -`} - helperText="HTML injected at the top of page body (e.g., navigation, announcements)" - onChange={(e) => - handleCustomHtmlChange("headerHtml", e.target.value) - } - value={customHtml.headerHtml} - variant="outlined" - size="small" - /> - `} - helperText="HTML injected at the start of (e.g., site verification meta tags, consent scripts)" - onChange={(e) => - handleCustomHtmlChange("headHtml", e.target.value) - } - value={customHtml.headHtml} - variant="outlined" - size="small" - /> - - - - - Custom 404 Page + + Add hreflang tags for multilingual SEO + + handleI18nChange("enabled", e.target.checked)} + /> + + + - handleCustom404Change("notionUrl", e.target.value) + handleI18nChange("defaultLocale", e.target.value) } - value={custom404.notionUrl} + value={i18n.defaultLocale} variant="outlined" size="small" /> + + Configure alternate language versions in each page's SEO + settings. Hreflang tags help search engines serve the correct + language version to users. + + + - - - Subdomain Redirects - - - Redirect subdomains (e.g., www) to the main domain or other - URLs - - {subdomainRedirects.map((redirect, index) => ( - - - - handleSubdomainRedirectChange( - index, - "subdomain", - e.target.value, - ) - } - variant="outlined" - size="small" - sx={{ flex: 1 }} - /> - - handleSubdomainRedirectChange( - index, - "redirectUrl", - e.target.value, - ) - } - variant="outlined" - size="small" - sx={{ flex: 2 }} - /> - - - - ))} - - - - - - URL Redirect Rules + + + + + Auto-Generate OG Images - Redirect specific paths to other URLs (301 permanent / 302 - temporary). End a path with * to match a prefix, e.g. - /blog/* to /posts/*. For thousands of URLs, use{" "} - - Cloudflare Bulk Redirects - - . + Create Open Graph images from page titles - {redirectRules.map((rule, index) => ( - - - - handleRedirectRuleChange( - index, - "from", - e.target.value, - ) - } - variant="outlined" - size="small" - sx={{ flex: 1 }} - /> - - handleRedirectRuleChange(index, "to", e.target.value) - } - variant="outlined" - size="small" - sx={{ flex: 2 }} - /> - - handleRedirectRuleChange( - index, - "permanent", - e.target.checked, - ) - } - size="small" - /> - } - label="301" - sx={{ minWidth: 70 }} - /> - - - - ))} - - - - - - - JSON-LD Structured Data - - - Enable rich snippets in search results - - - - handleStructuredDataChange("enabled", e.target.checked) - } - /> - - - - - Schema Type - - - - handleStructuredDataChange( - "organizationName", - e.target.value, - ) - } - value={structuredData.organizationName} - variant="outlined" - size="small" - /> - - handleStructuredDataChange("logoUrl", e.target.value) - } - value={structuredData.logoUrl} - variant="outlined" - size="small" - /> - - Structured data helps search engines understand your - content and display rich results. Validate with{" "} - - Google Rich Results Test - - . - - - - - - - - - - RSS Feed - - - Generate RSS 2.0 feed at /rss.xml - - - + handleOgImageGenerationChange("enabled", e.target.checked) + } + /> + + + + + - handleRssChange("enabled", e.target.checked) + handleOgImageGenerationChange( + "backgroundColor", + e.target.value, + ) } + value={ogImageGeneration.backgroundColor} + variant="outlined" + size="small" + sx={{ flex: 1 }} /> - - - - handleRssChange("title", e.target.value)} - value={rss.title} - variant="outlined" - size="small" - /> - - handleRssChange("description", e.target.value) - } - value={rss.description} - variant="outlined" - size="small" - /> - - Language - - - - RSS feed will include all pages defined in Pretty Links. - Add page metadata for better feed content. - - - - - - - - - - Internationalization (i18n) - - - Add hreflang tags for multilingual SEO - - - - handleI18nChange("enabled", e.target.checked) + handleOgImageGenerationChange("textColor", e.target.value) } + value={ogImageGeneration.textColor} + variant="outlined" + size="small" + sx={{ flex: 1 }} /> - - - - - handleI18nChange("defaultLocale", e.target.value) - } - value={i18n.defaultLocale} - variant="outlined" - size="small" - /> - - Configure alternate language versions in each page's SEO - settings. Hreflang tags help search engines serve the - correct language version to users. - - - - - - - - - - Auto-Generate OG Images - - - Create Open Graph images from page titles - - - - handleOgImageGenerationChange("enabled", e.target.checked) + handleOgImageGenerationChange( + "fontSize", + Number(e.target.value), + ) } + value={ogImageGeneration.fontSize} + variant="outlined" + size="small" + sx={{ width: "120px" }} /> - - - - - handleOgImageGenerationChange( - "backgroundColor", - e.target.value, - ) - } - value={ogImageGeneration.backgroundColor} - variant="outlined" - size="small" - sx={{ flex: 1 }} - /> - - handleOgImageGenerationChange( - "textColor", - e.target.value, - ) - } - value={ogImageGeneration.textColor} - variant="outlined" - size="small" - sx={{ flex: 1 }} - /> - - handleOgImageGenerationChange( - "fontSize", - Number(e.target.value), - ) - } - value={ogImageGeneration.fontSize} - variant="outlined" - size="small" - sx={{ width: "120px" }} - /> - - - handleOgImageGenerationChange("fontUrl", e.target.value) - } - value={ogImageGeneration.fontUrl} - variant="outlined" - size="small" - /> - - Auto-generated OG images are served at /og-image/[slug] - when no custom image is set. X, Facebook and LinkedIn - only show PNG images: add an{" "} - - Images binding - {" "} - named IMAGES to the Worker (Settings > Bindings) to - render PNG. Without it, or if rendering fails, the worker - serves SVG. - - - - - - - - - - Image Optimization - - handleImageOption(e.target)} - > - } label="None" /> - } - label="Enable Resizing" - /> - - - - handleImageOption(e.target)} - value={optionImage.imageWidth ?? ""} - variant="filled" - size="small" - sx={{ m: 0.5, width: "18ch" }} - /> handleImageOption(e.target)} - value={optionImage.imageHeight ?? ""} - variant="filled" - size="small" - sx={{ m: 0.5, width: "18ch" }} - /> - handleImageOption(e.target)} - value={optionImage.imageQuality ?? ""} - variant="filled" - size="small" - sx={{ m: 0.5, width: "12ch" }} - /> - - Format - - - - Fit - - - handleImageOption(e.target)} - value={optionImage.imageBlur ?? ""} - variant="filled" - size="small" - sx={{ m: 0.5, width: "12ch" }} - /> - - Metadata - - - { - setOptionImage({ ...optionImage, imageAnim: true }); - setCopied(false); - }} - size="small" - /> + fullWidth + label="Font URL" + margin="dense" + placeholder={DEFAULT_OG_FONT_URL} + error={!!ogFontUrlError} + helperText={ + ogFontUrlError || + "TTF, OTF, WOFF or WOFF2 file used to draw titles. Leave empty for the default, which covers Latin and Japanese." } - label="Anim: On" - sx={{ ml: 1 }} - /> - { - setOptionImage({ ...optionImage, imageAnim: false }); - setCopied(false); - }} - size="small" - /> + onChange={(e) => + handleOgImageGenerationChange("fontUrl", e.target.value) } - label="Anim: Off" + value={ogImageGeneration.fontUrl} + variant="outlined" + size="small" /> + + Auto-generated OG images are served at /og-image/[slug] + when no custom image is set. X, Facebook and LinkedIn + only show PNG images: add an{" "} + + Images binding + {" "} + named IMAGES to the Worker (Settings > Bindings) to render + PNG. Without it, or if rendering fails, the worker serves SVG. + - - Enable{" "} - - Image Transformations - {" "} - in your Cloudflare dashboard to use this feature. - - - - - {!noError && ( - - Fix the fields marked in red - {advancedError && !optional ? " (including Advanced Settings)" : ""}{" "} - to generate the worker script. - - )} - - - {noError && ( + + + + + + Image Optimization + + handleImageOption(e.target)} + > + } label="None" /> + } + label="Enable Resizing" + /> + + + handleImageOption(e.target)} + value={optionImage.imageWidth ?? ""} + variant="filled" + size="small" + sx={{ m: 0.5, width: "18ch" }} /> - )} - - - {/* Footer */} - - - Actively maintained by{" "} - handleImageOption(e.target)} + value={optionImage.imageHeight ?? ""} + variant="filled" + size="small" + sx={{ m: 0.5, width: "18ch" }} + /> + handleImageOption(e.target)} + value={optionImage.imageQuality ?? ""} + variant="filled" + size="small" + sx={{ m: 0.5, width: "12ch" }} + /> + - Classmethod - {" "} - | Fork of{" "} - Format + + + - Fruition - - - + Fit + + + handleImageOption(e.target)} + value={optionImage.imageBlur ?? ""} + variant="filled" + size="small" + sx={{ m: 0.5, width: "12ch" }} + /> + + Metadata + + + { + setOptionImage({ ...optionImage, imageAnim: true }); + setCopied(false); + }} + size="small" + /> + } + label="Anim: On" + sx={{ ml: 1 }} + /> + { + setOptionImage({ ...optionImage, imageAnim: false }); + setCopied(false); + }} + size="small" + /> + } + label="Anim: Off" + /> + + + Enable{" "} - View on GitHub - - - - - + Image Transformations + {" "} + in your Cloudflare dashboard to use this feature. + + + + + + {!noError && ( + + Fix the fields marked in red + {advancedError && !optional ? " (including Advanced Settings)" : ""}{" "} + to generate the worker script. + + )} + + + + {noError && ( + + )} + ); } diff --git a/src/index.tsx b/src/index.tsx index 0af03e9..b30ab12 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -1,11 +1,33 @@ import { StrictMode } from "react"; import { createRoot } from "react-dom/client"; +import { ThemeProvider, createTheme } from "@mui/material"; import App from "./App"; +const theme = createTheme({ + palette: { + primary: { main: "#1b6ec2", dark: "#165ea8" }, + text: { primary: "#242422", secondary: "#5c5c57" }, + divider: "#e2e2de", + }, + shape: { borderRadius: 8 }, + typography: { + fontFamily: + '"IBM Plex Sans", system-ui, -apple-system, "Segoe UI", sans-serif', + button: { textTransform: "none", fontWeight: 600 }, + }, + components: { + MuiButton: { defaultProps: { disableElevation: true } }, + // Generator section titles sit under the page's h2 + MuiTypography: { defaultProps: { variantMapping: { subtitle2: "h3" } } }, + }, +}); + const container = document.getElementById("root")!; createRoot(container).render( - + + + , ); diff --git a/src/styles.css b/src/styles.css index 7e28bd0..ceea872 100644 --- a/src/styles.css +++ b/src/styles.css @@ -1,37 +1,570 @@ +@font-face { + font-family: "IBM Plex Sans"; + font-style: normal; + font-weight: 400 700; + font-display: swap; + src: url("/fonts/ibm-plex-sans-latin.woff2") format("woff2"); +} + +:root { + --ink: #242422; + --muted: #5c5c57; + --bg: #f5f5f5; + --surface: #ffffff; + --line: #e2e2de; + --blue: #2d8de0; + --blue-strong: #1b6ec2; + --blue-dark: #165ea8; + --blue-tint: #eef5fc; + --footer: #1b1b1a; + --radius: 12px; +} + * { box-sizing: border-box; } +html { + scroll-behavior: smooth; + scroll-padding-top: 24px; +} + body { margin: 0; - padding: 0; font-family: - "Inter", + "IBM Plex Sans", + system-ui, -apple-system, - BlinkMacSystemFont, "Segoe UI", - Roboto, - Oxygen, - Ubuntu, sans-serif; - background-color: #fafbfc; + font-size: 17px; + line-height: 1.65; + color: var(--ink); + background-color: var(--bg); -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; } -section { - margin: 12px 0; +body:lang(ja) { + font-family: + "Noto Sans JP", + "Hiragino Sans", + "Hiragino Kaku Gothic ProN", + Meiryo, + sans-serif; + line-height: 1.8; +} + +:lang(ja) :is(h1, h2, h3) { + word-break: auto-phrase; +} + +:lang(ja) :is(.eyebrow, .flow-label) { + letter-spacing: 0.02em; + text-transform: none; +} + +h1, +h2, +h3, +p, +figure { + margin: 0; +} + +h1, +h2, +h3 { + font-weight: 700; + line-height: 1.2; +} + +img { + display: block; + max-width: 100%; } a { - color: #1976d2; - text-decoration: none; + color: var(--blue-strong); } a:hover { - text-decoration: underline; + color: var(--blue-dark); +} + +:focus-visible { + outline: 3px solid var(--blue); + outline-offset: 2px; } ::selection { - background-color: rgba(25, 118, 210, 0.2); + background-color: rgba(45, 141, 224, 0.2); +} + +.container { + width: min(1120px, 100% - 48px); + margin-inline: auto; +} + +.skip-link { + position: absolute; + left: -9999px; +} + +.skip-link:focus { + left: 16px; + top: 16px; + z-index: 10; + padding: 8px 16px; + background: var(--surface); +} + +/* Header */ + +.site-header { + background: var(--surface); + border-bottom: 1px solid var(--line); +} + +.header-inner { + display: flex; + align-items: center; + justify-content: space-between; + gap: 24px; + min-height: 88px; +} + +.brand { + display: flex; + align-items: center; + gap: 16px; +} + +.brand-logo img, +.footer-logo img { + width: auto; + height: 68px; +} + +.brand-divider { + width: 1px; + height: 32px; + background: var(--line); +} + +.product-name { + font-size: 1.25rem; + font-weight: 700; + color: var(--ink); + text-decoration: none; +} + +.site-nav { + display: flex; + align-items: center; + gap: 28px; +} + +.site-nav .nav-link { + font-size: 0.95rem; + font-weight: 500; + color: var(--ink); + text-decoration: none; +} + +.site-nav .nav-link:hover { + color: var(--blue-strong); +} + +.lang-switch { + padding: 4px 12px; + border: 1px solid var(--line); + border-radius: 999px; + font-size: 0.9rem; + font-weight: 500; + color: var(--ink); + text-decoration: none; + white-space: nowrap; +} + +.lang-switch:hover { + border-color: var(--blue-strong); +} + +/* Buttons */ + +.button { + display: inline-flex; + align-items: center; + justify-content: center; + min-height: 48px; + padding: 0 24px; + border: 2px solid var(--blue-strong); + border-radius: 8px; + font-size: 1rem; + font-weight: 600; + text-decoration: none; + transition: + background-color 0.15s, + border-color 0.15s, + color 0.15s; +} + +.button-primary { + background: var(--blue-strong); + color: #fff; +} + +.button-primary:hover { + background: var(--blue-dark); + border-color: var(--blue-dark); + color: #fff; +} + +.button-secondary { + background: var(--surface); + color: var(--blue-strong); +} + +.button-secondary:hover { + background: var(--blue-tint); +} + +.button-small { + min-height: 40px; + padding: 0 16px; + font-size: 0.95rem; +} + +/* Hero */ + +.hero { + padding: 72px 0 88px; +} + +.hero-grid { + display: grid; + grid-template-columns: 1.1fr 0.9fr; + gap: 56px; + align-items: center; +} + +.eyebrow { + margin-bottom: 16px; + font-size: 0.875rem; + font-weight: 600; + letter-spacing: 0.08em; + text-transform: uppercase; + color: var(--blue-strong); +} + +.hero h1 { + margin-bottom: 20px; + font-size: clamp(2.25rem, 4.5vw, 3.5rem); + letter-spacing: -0.02em; +} + +.lead { + max-width: 36em; + margin-bottom: 32px; + font-size: 1.15rem; + color: var(--muted); +} + +.actions { + display: flex; + flex-wrap: wrap; + gap: 12px; + margin-bottom: 28px; +} + +.facts { + display: flex; + flex-wrap: wrap; + gap: 8px 24px; + margin: 0; + padding: 0; + list-style: none; + font-size: 0.95rem; + color: var(--muted); +} + +.facts li::before { + content: ""; + display: inline-block; + width: 8px; + height: 8px; + margin-right: 8px; + border-radius: 50%; + background: var(--blue); + vertical-align: middle; +} + +/* Request flow diagram */ + +.flow-node { + padding: 16px 20px; + border: 1px solid var(--line); + border-radius: var(--radius); + background: var(--surface); + box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); +} + +.flow-node.is-worker { + border: 2px solid var(--blue); +} + +.flow-label { + margin-bottom: 8px; + font-size: 0.8rem; + font-weight: 600; + letter-spacing: 0.06em; + text-transform: uppercase; + color: var(--muted); +} + +.flow-url { + padding: 8px 12px; + border-radius: 6px; + background: var(--bg); + font-family: ui-monospace, SFMono-Regular, Menlo, monospace; + font-size: 0.9rem; + overflow-wrap: anywhere; +} + +.flow-connector { + width: 2px; + height: 28px; + margin-left: 32px; + background: var(--blue); +} + +.tags { + display: flex; + flex-wrap: wrap; + gap: 6px; + margin: 0; + padding: 0; + list-style: none; +} + +.tags li { + padding: 2px 12px; + border-radius: 999px; + background: var(--blue-tint); + font-size: 0.85rem; + font-weight: 500; + color: var(--blue-dark); +} + +.flow figcaption { + margin-top: 16px; + font-size: 0.9rem; + color: var(--muted); +} + +/* Sections */ + +.section { + padding: 88px 0; +} + +.section-alt { + background: var(--surface); + border-block: 1px solid var(--line); +} + +.section h2 { + margin-bottom: 12px; + font-size: clamp(1.75rem, 3vw, 2.25rem); + letter-spacing: -0.01em; +} + +.section-lead { + max-width: 40em; + margin-bottom: 40px; + color: var(--muted); +} + +.feature-grid { + display: grid; + grid-template-columns: repeat(3, 1fr); + gap: 20px; +} + +.feature { + padding: 24px; + border: 1px solid var(--line); + border-top: 3px solid var(--blue); + border-radius: var(--radius); + background: var(--surface); +} + +.feature h3, +.steps h3 { + margin-bottom: 8px; + font-size: 1.1rem; +} + +.feature p, +.steps p { + font-size: 1rem; + color: var(--muted); +} + +.steps { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 32px; + margin: 0; + padding: 0; + list-style: none; + counter-reset: step; +} + +.steps li { + counter-increment: step; +} + +.steps li::before { + content: counter(step); + display: grid; + place-items: center; + width: 40px; + height: 40px; + margin-bottom: 16px; + border-radius: 50%; + background: var(--ink); + font-weight: 700; + color: #fff; +} + +#root { + min-height: 480px; +} + +/* FAQ */ + +.faq details { + max-width: 840px; + border-bottom: 1px solid var(--line); +} + +.faq summary { + position: relative; + padding: 20px 48px 20px 0; + cursor: pointer; + list-style: none; +} + +.faq summary::-webkit-details-marker { + display: none; +} + +.faq summary::after { + content: "+"; + position: absolute; + top: 50%; + right: 8px; + transform: translateY(-50%); + font-size: 1.5rem; + color: var(--blue-strong); +} + +.faq details[open] summary::after { + content: "\2212"; +} + +.faq summary h3 { + font-size: 1.1rem; + font-weight: 600; +} + +.faq details p { + padding-bottom: 20px; + color: var(--muted); +} + +/* Footer */ + +.site-footer { + padding: 48px 0; + background: var(--footer); + font-size: 0.95rem; + color: #c9c9c4; +} + +.footer-inner { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: 24px; +} + +.footer-nav { + display: flex; + flex-wrap: wrap; + gap: 8px 28px; +} + +.site-footer a { + color: #fff; + text-decoration: none; +} + +.site-footer a:hover { + color: #fff; + text-decoration: underline; +} + +.legal { + width: 100%; + padding-top: 24px; + border-top: 1px solid #343432; + font-size: 0.875rem; + color: #a3a39e; +} + +@media (max-width: 900px) { + .site-nav .nav-link { + display: none; + } + + .hero { + padding: 48px 0 64px; + } + + .hero-grid { + grid-template-columns: 1fr; + } + + .feature-grid, + .steps { + grid-template-columns: repeat(2, 1fr); + } +} + +@media (max-width: 600px) { + .header-inner, + .brand { + gap: 12px; + } + + .product-name { + font-size: 1.1rem; + } + + .site-nav .button { + display: none; + } + + .section { + padding: 56px 0; + } + + .feature-grid, + .steps { + grid-template-columns: 1fr; + } } diff --git a/vite.config.ts b/vite.config.ts index 8e08d3f..dd9334c 100644 --- a/vite.config.ts +++ b/vite.config.ts @@ -1,3 +1,4 @@ +import { fileURLToPath } from "node:url"; import { defineConfig } from "vite"; import react from "@vitejs/plugin-react"; @@ -5,6 +6,12 @@ export default defineConfig({ plugins: [react()], build: { outDir: "build", + rolldownOptions: { + input: { + main: fileURLToPath(new URL("./index.html", import.meta.url)), + ja: fileURLToPath(new URL("./ja/index.html", import.meta.url)), + }, + }, }, server: { port: 3000,