ogthing 0.0.0-alpha.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/tabs.tsx ADDED
@@ -0,0 +1,374 @@
1
+ import { useState } from "react";
2
+ import type { MetaData } from "../server/types";
3
+ import { CheckIcon, CopyIcon, ExternalIcon } from "./icons";
4
+
5
+ export interface TabItem {
6
+ id: string;
7
+ label: string;
8
+ }
9
+
10
+ export function Tabs({
11
+ tabs,
12
+ active,
13
+ onChange,
14
+ }: {
15
+ tabs: TabItem[];
16
+ active: string;
17
+ onChange: (id: string) => void;
18
+ }) {
19
+ return (
20
+ <div className="tabs">
21
+ {tabs.map((tab) => (
22
+ <button
23
+ type="button"
24
+ key={tab.id}
25
+ className={`tab ${active === tab.id ? "active" : ""}`}
26
+ onMouseDown={() => onChange(tab.id)}
27
+ >
28
+ {tab.label}
29
+ </button>
30
+ ))}
31
+ </div>
32
+ );
33
+ }
34
+
35
+ const Card = ({ title, children }: { title: string; children: React.ReactNode }) => (
36
+ <section className="card">
37
+ <div className="card-head">{title}</div>
38
+ <div className="card-body">{children}</div>
39
+ </section>
40
+ );
41
+
42
+ const lengthStatus = (length: number, limit?: number): "ok" | "warn" | "error" => {
43
+ if (!limit || length <= limit) return "ok";
44
+ if (length <= limit * 1.2) return "warn";
45
+ return "error";
46
+ };
47
+
48
+ const Field = ({
49
+ label,
50
+ value,
51
+ isImage,
52
+ limit,
53
+ characterCount,
54
+ missing,
55
+ }: {
56
+ label: string;
57
+ value?: string;
58
+ isImage?: boolean;
59
+ limit?: number;
60
+ characterCount?: boolean;
61
+ missing?: boolean;
62
+ }) => {
63
+ const status = value ? lengthStatus(value.length, limit) : "ok";
64
+ return (
65
+ <div className="field-row">
66
+ <div className="field-label">{label}</div>
67
+ <div className="field-value">
68
+ {isImage && value ? (
69
+ <a href={value} target="_blank" rel="noopener noreferrer" className="image-link">
70
+ <img
71
+ src={value}
72
+ alt={label}
73
+ loading="lazy"
74
+ onError={(event) => {
75
+ event.currentTarget.style.display = "none";
76
+ event.currentTarget.parentElement?.append("unavailable");
77
+ }}
78
+ />
79
+ </a>
80
+ ) : (
81
+ <>
82
+ {value ? <span className="field-text">{value}</span> : <span className="dim">—</span>}
83
+ {characterCount && value && (
84
+ <div className={`char-count ${status}`}>
85
+ {value.length}
86
+ {limit ? ` / ${limit}` : ""} chars
87
+ </div>
88
+ )}
89
+ {missing && !value && <span className="missing">missing</span>}
90
+ </>
91
+ )}
92
+ </div>
93
+ </div>
94
+ );
95
+ };
96
+
97
+ const LinkPresence = ({ label, url }: { label: string; url?: string }) => (
98
+ <div className="field-row">
99
+ <div className="field-label">{label}</div>
100
+ <div className="field-value">
101
+ {url ? (
102
+ <a href={url} target="_blank" rel="noopener noreferrer" className="found">
103
+ found <ExternalIcon size={10} />
104
+ </a>
105
+ ) : (
106
+ <span className="missing">not found</span>
107
+ )}
108
+ </div>
109
+ </div>
110
+ );
111
+
112
+ const scoreOf = (metadata: MetaData) => {
113
+ const checks = [
114
+ { label: "Title", ok: !!metadata.title },
115
+ { label: "Title Length", ok: !!metadata.title && metadata.title.length <= 60 },
116
+ { label: "Description", ok: !!metadata.description },
117
+ { label: "Desc Length", ok: !!metadata.description && metadata.description.length <= 160 },
118
+ { label: "OG Title", ok: !!metadata.ogTitle },
119
+ { label: "OG Description", ok: !!metadata.ogDescription },
120
+ { label: "OG Image", ok: !!metadata.ogImage },
121
+ { label: "Twitter Card", ok: !!metadata.twitterCard },
122
+ { label: "Twitter Image", ok: !!metadata.twitterImage || !!metadata.ogImage },
123
+ { label: "Canonical", ok: !!metadata.canonical },
124
+ { label: "Favicon", ok: !!metadata.favicon },
125
+ { label: "Viewport", ok: !!metadata.viewport },
126
+ { label: "robots.txt", ok: !!metadata.robotsFile },
127
+ { label: "sitemap.xml", ok: !!metadata.sitemap },
128
+ ];
129
+ return {
130
+ checks,
131
+ score: Math.round((checks.filter((check) => check.ok).length / checks.length) * 100),
132
+ };
133
+ };
134
+
135
+ export function ScoreTab({ metadata }: { metadata: MetaData }) {
136
+ const { score, checks } = scoreOf(metadata);
137
+ const tone = score >= 80 ? "good" : score >= 50 ? "mid" : "bad";
138
+ const circumference = 2 * Math.PI * 36;
139
+ const offset = circumference - (score / 100) * circumference;
140
+
141
+ return (
142
+ <div className="score-wrap">
143
+ <div className={`score-ring ${tone}`}>
144
+ <svg viewBox="0 0 80 80" aria-hidden="true">
145
+ <circle cx="40" cy="40" r="36" className="ring-bg" strokeWidth="4" fill="none" />
146
+ <circle
147
+ cx="40"
148
+ cy="40"
149
+ r="36"
150
+ className="ring-fg"
151
+ strokeWidth="4"
152
+ strokeLinecap="round"
153
+ fill="none"
154
+ strokeDasharray={circumference}
155
+ strokeDashoffset={offset}
156
+ transform="rotate(-90 40 40)"
157
+ />
158
+ </svg>
159
+ <span className="score-number">{score}</span>
160
+ </div>
161
+ <div className="checks-grid">
162
+ {checks.map((check) => (
163
+ <div key={check.label} className="check-item">
164
+ <span className={`dot ${check.ok ? "ok" : "bad"}`} />
165
+ {check.label}
166
+ </div>
167
+ ))}
168
+ </div>
169
+ </div>
170
+ );
171
+ }
172
+
173
+ export function BasicTab({ metadata, url }: { metadata: MetaData; url: string }) {
174
+ const fallbackFavicon =
175
+ metadata.favicon ||
176
+ (() => {
177
+ try {
178
+ return new URL("/favicon.ico", url).toString();
179
+ } catch {
180
+ return "";
181
+ }
182
+ })();
183
+
184
+ return (
185
+ <div className="stack">
186
+ <Card title="Essential">
187
+ <Field label="Favicon" value={fallbackFavicon} isImage />
188
+ <Field label="Title" value={metadata.title} characterCount limit={60} missing />
189
+ <Field
190
+ label="Description"
191
+ value={metadata.description}
192
+ characterCount
193
+ limit={160}
194
+ missing
195
+ />
196
+ <Field label="Canonical" value={metadata.canonical} />
197
+ <LinkPresence label="robots.txt" url={metadata.robotsFile} />
198
+ <LinkPresence label="sitemap.xml" url={metadata.sitemap} />
199
+ </Card>
200
+
201
+ <Card title="Technical">
202
+ <Field label="Language" value={metadata.language} />
203
+ <Field label="Charset" value={metadata.charset} />
204
+ <Field label="Viewport" value={metadata.viewport} />
205
+ <Field label="Robots" value={metadata.robots} />
206
+ <Field label="Generator" value={metadata.generator} />
207
+ <Field label="Theme Color" value={metadata.themeColor} />
208
+ {metadata.themeColor && (
209
+ <span
210
+ className="swatch"
211
+ style={{ backgroundColor: metadata.themeColor }}
212
+ aria-hidden="true"
213
+ />
214
+ )}
215
+ <Field
216
+ label="Status"
217
+ value={metadata.statusCode ? String(metadata.statusCode) : undefined}
218
+ />
219
+ </Card>
220
+ </div>
221
+ );
222
+ }
223
+
224
+ export function OpenGraphTab({ metadata }: { metadata: MetaData }) {
225
+ return (
226
+ <Card title="Open Graph">
227
+ <Field label="Title" value={metadata.ogTitle} characterCount limit={60} missing />
228
+ <Field
229
+ label="Description"
230
+ value={metadata.ogDescription}
231
+ characterCount
232
+ limit={160}
233
+ missing
234
+ />
235
+ <Field label="Image" value={metadata.ogImage} isImage missing />
236
+ {(metadata.ogImageAll?.length ?? 0) > 1 && (
237
+ <Field label="All Images" value={metadata.ogImageAll?.join("\n")} />
238
+ )}
239
+ <Field label="Type" value={metadata.ogType} />
240
+ <Field label="URL" value={metadata.ogUrl} />
241
+ <Field label="Site Name" value={metadata.ogSiteName} />
242
+ <Field label="Locale" value={metadata.ogLocale} />
243
+ </Card>
244
+ );
245
+ }
246
+
247
+ export function TwitterTab({ metadata }: { metadata: MetaData }) {
248
+ return (
249
+ <Card title="X Card">
250
+ <Field label="Card Type" value={metadata.twitterCard} missing />
251
+ <Field
252
+ label="Title"
253
+ value={metadata.twitterTitle || metadata.ogTitle}
254
+ characterCount
255
+ limit={70}
256
+ />
257
+ <Field
258
+ label="Description"
259
+ value={metadata.twitterDescription || metadata.ogDescription}
260
+ characterCount
261
+ limit={200}
262
+ />
263
+ <Field label="Image" value={metadata.twitterImage} isImage missing />
264
+ {(metadata.twitterImageAll?.length ?? 0) > 1 && (
265
+ <Field label="All Images" value={metadata.twitterImageAll?.join("\n")} />
266
+ )}
267
+ <Field label="Site" value={metadata.twitterSite} />
268
+ <Field label="Creator" value={metadata.twitterCreator} />
269
+ </Card>
270
+ );
271
+ }
272
+
273
+ const recommended = {
274
+ og: { width: 1200, height: 630 },
275
+ twitter: { width: 1200, height: 600 },
276
+ };
277
+
278
+ function PreviewImage({ title, url }: { title: string; url: string }) {
279
+ const [dimensions, setDimensions] = useState<{ width: number; height: number } | null>(null);
280
+ const [failed, setFailed] = useState(false);
281
+ const isFavicon = /favicon/i.test(title);
282
+ const isTwitter = /^x$/i.test(title);
283
+
284
+ return (
285
+ <div className="preview-image-card">
286
+ <div className="section-label">{title}</div>
287
+ {failed ? (
288
+ <span className="dim">unavailable</span>
289
+ ) : (
290
+ <>
291
+ <a href={url} target="_blank" rel="noopener noreferrer">
292
+ <img
293
+ src={url}
294
+ alt={title}
295
+ className={isFavicon ? "favicon-preview" : ""}
296
+ loading="lazy"
297
+ onLoad={(event) =>
298
+ setDimensions({
299
+ width: event.currentTarget.naturalWidth,
300
+ height: event.currentTarget.naturalHeight,
301
+ })
302
+ }
303
+ onError={() => setFailed(true)}
304
+ />
305
+ </a>
306
+ {dimensions && !isFavicon && (
307
+ <div className="dimension-line">
308
+ <span
309
+ className={`dot ${
310
+ dimensions.width >= recommended.og.width &&
311
+ dimensions.height >=
312
+ (isTwitter ? recommended.twitter.height : recommended.og.height)
313
+ ? "ok"
314
+ : "warn"
315
+ }`}
316
+ />
317
+ {dimensions.width} × {dimensions.height}
318
+ {!isFavicon &&
319
+ dimensions.width < recommended.og.width &&
320
+ ` (recommended ${recommended.og.width}×${recommended.og.height})`}
321
+ </div>
322
+ )}
323
+ </>
324
+ )}
325
+ </div>
326
+ );
327
+ }
328
+
329
+ export function ImagesTab({ metadata }: { metadata: MetaData }) {
330
+ const hasImages = metadata.ogImage || metadata.twitterImage || metadata.favicon;
331
+ return (
332
+ <div className="stack">
333
+ {metadata.ogImage && <PreviewImage title="Open Graph" url={metadata.ogImage} />}
334
+ {metadata.twitterImage && metadata.twitterImage !== metadata.ogImage && (
335
+ <PreviewImage title="X" url={metadata.twitterImage} />
336
+ )}
337
+ {metadata.favicon && <PreviewImage title="Favicon" url={metadata.favicon} />}
338
+ {!hasImages && <div className="empty-note">no images found</div>}
339
+ </div>
340
+ );
341
+ }
342
+
343
+ export function RawTab({ metadata }: { metadata: MetaData }) {
344
+ const [format, setFormat] = useState<"json" | "html">("json");
345
+ const [copied, setCopied] = useState(false);
346
+ const json = JSON.stringify({ ...metadata, htmlTags: undefined }, null, 2);
347
+ const html = metadata.htmlTags ?? "";
348
+
349
+ const copy = () => {
350
+ navigator.clipboard.writeText(format === "json" ? json : html);
351
+ setCopied(true);
352
+ setTimeout(() => setCopied(false), 1500);
353
+ };
354
+
355
+ return (
356
+ <div>
357
+ <div className="raw-toolbar">
358
+ <Tabs
359
+ tabs={[
360
+ { id: "json", label: "JSON" },
361
+ { id: "html", label: "HTML" },
362
+ ]}
363
+ active={format}
364
+ onChange={(id) => setFormat(id as "json" | "html")}
365
+ />
366
+ <button type="button" className="copy-button" onClick={copy}>
367
+ {copied ? <CheckIcon /> : <CopyIcon />}
368
+ {copied ? "Copied" : "Copy"}
369
+ </button>
370
+ </div>
371
+ <pre className="code-block">{format === "json" ? json : html}</pre>
372
+ </div>
373
+ );
374
+ }
@@ -0,0 +1 @@
1
+ /// <reference types="vite/client" />
package/tsconfig.json ADDED
@@ -0,0 +1,21 @@
1
+ {
2
+ "compilerOptions": {
3
+ "target": "ES2022",
4
+ "useDefineForClassFields": true,
5
+ "lib": ["ES2022", "DOM", "DOM.Iterable"],
6
+ "allowJs": false,
7
+ "skipLibCheck": true,
8
+ "esModuleInterop": true,
9
+ "allowSyntheticDefaultImports": true,
10
+ "strict": true,
11
+ "forceConsistentCasingInFileNames": true,
12
+ "module": "ESNext",
13
+ "moduleResolution": "Bundler",
14
+ "resolveJsonModule": true,
15
+ "isolatedModules": true,
16
+ "noEmit": true,
17
+ "jsx": "react-jsx",
18
+ "types": ["node", "bun-types"]
19
+ },
20
+ "include": ["src", "server", "vite.config.ts", "tests"]
21
+ }
package/vite.config.ts ADDED
@@ -0,0 +1,16 @@
1
+ import react from "@vitejs/plugin-react";
2
+ import { defineConfig } from "vite";
3
+ import { ogthingApi } from "./server/ogthing-api";
4
+
5
+ export default defineConfig({
6
+ plugins: [react(), ogthingApi()],
7
+ server: {
8
+ host: "127.0.0.1",
9
+ allowedHosts: true,
10
+ strictPort: true,
11
+ watch:
12
+ process.env.CODEX_SANDBOX === "seatbelt"
13
+ ? { useFsEvents: false, usePolling: true }
14
+ : undefined,
15
+ },
16
+ });