Skip to content
File

Blob: src/sites/document.tsx

typescript306 lines
1import { renderToStaticMarkup } from "react-dom/server";
2 
3import {
4 DOC_PAGE_BALANCED_INLINE_OUTLINE_BEFORE_CLASS,
5 DocumentFrame,
6 PAGE_SHELL_CLASS,
7} from "@/shared/editor/presentation/document-layout";
8import { EditorMetricsPresentation } from "@/shared/editor/presentation/metrics";
9import { OutlinePresentation } from "@/shared/editor/presentation/outline";
10import { SiteOutlineControllerIslandHost } from "@/sites/island-hosts/site-outline-controller";
11import { SiteFooter } from "./footer";
12import { SiteHeader } from "./header";
13import { SiteIconMark } from "./icons";
14import {
15 createSitesApexRenderContext,
16 createSitesNotFoundRenderContext,
17 readSitesDocumentAssets,
18 readSitesHeaderRenderState,
19 readSitesPageRenderState,
20 runWithSitesReactRenderContext,
21} from "./react-render-context";
22import type { ApexDocumentProps, NotFoundDocumentProps, SiteOgImage, SitePageDocumentProps } from "./types";
23 
24function isGradient(cover: string): boolean {
25 return cover.startsWith("linear-gradient(");
26}
27 
28function PageIcon({ icon }: { icon: string }) {
29 return (
30 <SiteIconMark
31 icon={icon}
32 imageClassName="site-icon-img block h-7 w-7"
33 glyphClassName="site-icon-glyph text-[1.75rem] leading-none"
34 imageSize={28}
35 />
36 );
37}
38 
39function SiteHead({
40 title,
41 canonicalUrl,
42 ogTitle,
43 ogUrl,
44 ogType,
45 ogImage,
46 description,
47 includeModulePreloads = true,
48}: {
49 title: string;
50 canonicalUrl?: string;
51 ogTitle?: string;
52 ogUrl?: string;
53 ogType?: "article" | "website";
54 ogImage?: SiteOgImage | null;
55 description?: string | null;
56 includeModulePreloads?: boolean;
57}) {
58 const assets = readSitesDocumentAssets();
59 
60 return (
61 <head>
62 <meta charSet="utf-8" />
63 <meta name="viewport" content="width=device-width, initial-scale=1" />
64 <title>{title}</title>
65 <link rel="icon" href="/icons/favicon.ico" sizes="any" />
66 <link rel="icon" type="image/svg+xml" href="/icons/icon.svg" />
67 <link rel="icon" type="image/png" sizes="32x32" href="/icons/favicon-32x32.png" />
68 <link rel="icon" type="image/png" sizes="16x16" href="/icons/favicon-16x16.png" />
69 <link rel="apple-touch-icon" sizes="180x180" href="/icons/apple-touch-icon.png" />
70 <meta name="theme-color" content="#171717" />
71 <link rel="preload" as="style" href={assets.fontStylesheetHref} />
72 <link rel="stylesheet" href={assets.stylesheetHref} precedence="default" />
73 {canonicalUrl ? <link rel="canonical" href={canonicalUrl} /> : null}
74 {description ? <meta name="description" content={description} /> : null}
75 {ogType ? <meta property="og:type" content={ogType} /> : null}
76 {ogTitle ? <meta property="og:title" content={ogTitle} /> : null}
77 {description ? <meta property="og:description" content={description} /> : null}
78 {ogUrl ? <meta property="og:url" content={ogUrl} /> : null}
79 {ogImage ? (
80 <>
81 <meta property="og:image" content={ogImage.url} />
82 <meta property="og:image:type" content={ogImage.type} />
83 <meta property="og:image:width" content={String(ogImage.width)} />
84 <meta property="og:image:height" content={String(ogImage.height)} />
85 </>
86 ) : null}
87 {includeModulePreloads
88 ? assets.modulePreloadHrefs.map((href) => <link key={href} rel="modulepreload" href={href} />)
89 : null}
90 </head>
91 );
92}
93 
94function SiteDeferredStyles() {
95 const assets = readSitesDocumentAssets();
96 return <link rel="stylesheet" href={assets.fontStylesheetHref} />;
97}
98 
99export function SitePageDocument({ children }: SitePageDocumentProps) {
100 const {
101 page: { title, icon, coverUrl, ogImage, outline, metrics, description, canonicalUrl, site },
102 } = readSitesPageRenderState();
103 const coverStyle = coverUrl
104 ? { backgroundImage: isGradient(coverUrl) ? coverUrl : `url(${JSON.stringify(coverUrl)})` }
105 : null;
106 const hasOutline = outline.length > 0;
107 
108 return (
109 <html lang="en" className="dark scheme-dark">
110 <SiteHead
111 title={title}
112 canonicalUrl={canonicalUrl}
113 ogTitle={title}
114 ogUrl={canonicalUrl}
115 ogType="article"
116 ogImage={ogImage}
117 description={description}
118 />
119 <body className="site-shell flex min-h-screen flex-col bg-canvas font-sans font-[450] text-zinc-100 antialiased">
120 <SiteHeader />
121 <main id="main-content" className={`site-main flex-1 ${PAGE_SHELL_CLASS}`}>
122 <DocumentFrame
123 main={
124 <div className="site-container mx-auto min-w-0 max-w-3xl">
125 {coverStyle ? (
126 <div
127 className="site-cover-wrapper group/cover relative -mx-4 -mt-10 mb-6 sm:-mx-8 lg:mx-0"
128 aria-hidden="true"
129 >
130 <div
131 className="site-cover h-48 w-full overflow-hidden rounded-b-lg bg-cover bg-center bg-no-repeat"
132 style={coverStyle}
133 />
134 </div>
135 ) : null}
136 {icon ? (
137 <div className="site-icon mb-4 flex min-h-9 items-center leading-none">
138 <PageIcon icon={icon} />
139 </div>
140 ) : null}
141 <h1 className="site-title mb-1.5 font-display text-3xl font-bold leading-9 tracking-[-0.03em] text-zinc-100 sm:text-4xl sm:leading-10">
142 {title}
143 </h1>
144 <div className="tiptap-document-lead">
145 {hasOutline ? (
146 <div className={DOC_PAGE_BALANCED_INLINE_OUTLINE_BEFORE_CLASS}>
147 <OutlinePresentation items={outline} mode="link" variant="card" />
148 </div>
149 ) : null}
150 <div className="tiptap tiptap-page-body">{children}</div>
151 </div>
152 {site.currentIsHome ? null : (
153 <div className="mt-4 flex flex-wrap items-center justify-end gap-x-4 gap-y-1">
154 <EditorMetricsPresentation metrics={metrics} />
155 </div>
156 )}
157 </div>
158 }
159 rail={hasOutline ? <SiteOutlineControllerIslandHost items={outline} /> : undefined}
160 railBalance="content"
161 />
162 </main>
163 <SiteFooter />
164 <SiteDeferredStyles />
165 </body>
166 </html>
167 );
168}
169 
170export function NotFoundDocument() {
171 const header = readSitesHeaderRenderState();
172 const site = header
173 ? { workspaceName: header.workspaceName, workspaceIcon: header.workspaceIcon, homeHref: header.homeHref }
174 : null;
175 const heading = site ? "Nothing here to read." : "Nothing here.";
176 const sub = site ? "This page is not published, or never was." : "This URL doesn't point to a bland site.";
177 const ctaLabel = site ? `Back to ${site.workspaceName}` : "Visit bland.tools";
178 const ctaHref = site ? site.homeHref : "https://bland.tools";
179 const docTitle = site ? `Not found - ${site.workspaceName}` : "Not found - bland";
180 
181 return (
182 <SiteStatusDocument
183 mark="404"
184 variantClass="site-not-found"
185 heading={heading}
186 sub={sub}
187 ctaLabel={ctaLabel}
188 ctaHref={ctaHref}
189 docTitle={docTitle}
190 showHeader={Boolean(site)}
191 />
192 );
193}
194 
195export function BuildingDocument() {
196 const header = readSitesHeaderRenderState();
197 const site = header
198 ? { workspaceName: header.workspaceName, workspaceIcon: header.workspaceIcon, homeHref: header.homeHref }
199 : null;
200 const ctaLabel = site ? `Back to ${site.workspaceName}` : "Visit bland.tools";
201 const ctaHref = site ? site.homeHref : "https://bland.tools";
202 const docTitle = site ? `Preparing page - ${site.workspaceName}` : "Preparing page - bland";
203 
204 return (
205 <SiteStatusDocument
206 mark="hold on"
207 variantClass="site-building"
208 heading="Hang tight."
209 sub="This page is being prepared. Come back in a moment."
210 ctaLabel={ctaLabel}
211 ctaHref={ctaHref}
212 docTitle={docTitle}
213 showHeader={Boolean(site)}
214 />
215 );
216}
217 
218function SiteStatusDocument({
219 mark,
220 variantClass,
221 heading,
222 sub,
223 ctaLabel,
224 ctaHref,
225 docTitle,
226 showHeader,
227}: {
228 mark: string;
229 variantClass: string;
230 heading: string;
231 sub: string;
232 ctaLabel: string;
233 ctaHref: string;
234 docTitle: string;
235 showHeader: boolean;
236}) {
237 return (
238 <html lang="en" className="dark scheme-dark">
239 <SiteHead title={docTitle} includeModulePreloads={false} />
240 <body
241 className={`${variantClass} flex min-h-screen flex-col bg-canvas font-sans font-[450] text-zinc-100 antialiased`}
242 >
243 {showHeader ? <SiteHeader /> : null}
244 <main className="flex flex-1 items-center justify-center px-4 py-16">
245 <div className="max-w-md text-center">
246 <p className="font-display text-7xl font-bold tracking-tighter text-zinc-700">{mark}</p>
247 <h1 className="mt-4 font-display text-xl font-semibold text-zinc-200">{heading}</h1>
248 <p className="mt-2 text-sm text-zinc-400">{sub}</p>
249 <a
250 href={ctaHref}
251 className="mt-6 inline-flex items-center gap-1 rounded-md border border-zinc-700 px-4 py-2 text-sm text-zinc-300 transition-colors hover:bg-zinc-800 hover:text-zinc-100 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-accent-500/50 focus-visible:ring-offset-2 focus-visible:ring-offset-canvas"
252 >
253 {ctaLabel}
254 </a>
255 </div>
256 </main>
257 <SiteDeferredStyles />
258 </body>
259 </html>
260 );
261}
262 
263export function ApexDocument() {
264 return (
265 <html lang="en" className="dark scheme-dark">
266 <SiteHead title="bland" includeModulePreloads={false} />
267 <body className="site-apex flex min-h-screen items-center justify-center bg-canvas font-sans text-[1.75rem] tracking-[-0.02em] antialiased">
268 <a
269 href="https://bland.tools"
270 rel="noopener noreferrer"
271 aria-label="bland, visit bland.tools"
272 className="site-apex-mark inline-block bg-[linear-gradient(135deg,oklch(0.72_0.22_25),oklch(0.78_0.18_60),oklch(0.88_0.18_95),oklch(0.78_0.20_145),oklch(0.70_0.20_240),oklch(0.65_0.22_305))] bg-clip-text font-semibold text-transparent transition-[filter,transform] duration-200 ease-out hover:scale-[1.02] hover:saturate-[1.15]"
273 >
274 bland.
275 </a>
276 <SiteDeferredStyles />
277 </body>
278 </html>
279 );
280}
281 
282export function renderSiteNotFoundDocumentHtml(props: NotFoundDocumentProps): string {
283 return runWithSitesReactRenderContext(
284 createSitesNotFoundRenderContext(props),
285 () => `<!doctype html>${renderToStaticMarkup(<NotFoundDocument />)}`,
286 );
287}
288 
289export function renderSiteBuildingDocumentHtml(props: NotFoundDocumentProps): string {
290 return runWithSitesReactRenderContext(
291 createSitesNotFoundRenderContext(props),
292 () => `<!doctype html>${renderToStaticMarkup(<BuildingDocument />)}`,
293 );
294}
295 
296export function renderApexDocumentHtml(props: ApexDocumentProps): string {
297 return runWithSitesReactRenderContext(
298 createSitesApexRenderContext(props),
299 () => `<!doctype html>${renderToStaticMarkup(<ApexDocument />)}`,
300 );
301}
302 
303export function renderRobotsTxt(): string {
304 return "User-agent: *\nAllow: /\n";
305}