diff --git a/src/App.tsx b/src/App.tsx index f3699df..cd89bbb 100644 --- a/src/App.tsx +++ b/src/App.tsx @@ -42,6 +42,9 @@ import code, { PageMetadata, StructuredDataOptions, BrandingOptions, + AnalyticsOptions, + CustomHtmlOptions, + Custom404Options, } from "./code"; import "./styles.css"; @@ -146,10 +149,20 @@ export default function App() { siteName: "", brandReplacement: "", twitterHandle: "", + faviconUrl: "", }); const [slugMetadataExpanded, setSlugMetadataExpanded] = useState< Record >({}); + const [analytics, setAnalytics] = useState({ + googleTagId: "", + }); + const [customHtml, setCustomHtml] = useState({ + headerHtml: "", + }); + const [custom404, setCustom404] = useState({ + notionUrl: "", + }); function createInputHandler( setter: React.Dispatch>, @@ -248,6 +261,39 @@ export default function App() { setCopied(false); } + function handleAnalyticsChange( + field: keyof AnalyticsOptions, + value: string, + ): void { + setAnalytics({ + ...analytics, + [field]: value, + }); + setCopied(false); + } + + function handleCustomHtmlChange( + field: keyof CustomHtmlOptions, + value: string, + ): void { + setCustomHtml({ + ...customHtml, + [field]: value, + }); + setCopied(false); + } + + function handleCustom404Change( + field: keyof Custom404Options, + value: string, + ): void { + setCustom404({ + ...custom404, + [field]: value, + }); + setCopied(false); + } + function toggleSlugMetadata(index: number): void { setSlugMetadataExpanded({ ...slugMetadataExpanded, @@ -310,6 +356,9 @@ export default function App() { pageMetadata, structuredData, branding, + analytics, + customHtml, + custom404, }; const script = noError ? code(codeData) : undefined; @@ -776,6 +825,93 @@ export default function App() { variant="outlined" size="small" /> + + handleBrandingChange("faviconUrl", e.target.value) + } + value={branding.faviconUrl} + variant="outlined" + size="small" + /> + + + + + Analytics + + + handleAnalyticsChange("googleTagId", e.target.value) + } + value={analytics.googleTagId} + 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" + /> + + + + + Custom 404 Page + + + handleCustom404Change("notionUrl", e.target.value) + } + value={custom404.notionUrl} + variant="outlined" + size="small" + /> diff --git a/src/code.ts b/src/code.ts index a51688a..9f17527 100644 --- a/src/code.ts +++ b/src/code.ts @@ -27,6 +27,19 @@ export interface BrandingOptions { siteName?: string; brandReplacement?: string; twitterHandle?: string; + faviconUrl?: string; +} + +export interface AnalyticsOptions { + googleTagId?: string; +} + +export interface CustomHtmlOptions { + headerHtml?: string; +} + +export interface Custom404Options { + notionUrl?: string; } export interface CodeData { @@ -42,6 +55,9 @@ export interface CodeData { pageMetadata: Record; structuredData: StructuredDataOptions; branding: BrandingOptions; + analytics: AnalyticsOptions; + customHtml: CustomHtmlOptions; + custom404: Custom404Options; } function getId(url: string): string { @@ -68,6 +84,9 @@ export default function code(data: CodeData): string { pageMetadata, structuredData, branding, + analytics, + customHtml, + custom404, } = data; let url = myDomain.replace("https://", "").replace("http://", ""); if (url.slice(-1) === "/") url = url.slice(0, url.length - 1); @@ -118,6 +137,25 @@ ${slugs const SITE_NAME = '${branding?.siteName || ""}'; const BRAND_REPLACEMENT = '${branding?.brandReplacement || ""}'; const TWITTER_HANDLE = '${branding?.twitterHandle || ""}'; + const FAVICON_URL = '${branding?.faviconUrl || ""}'; + + /* + * Step 3.4: analytics configuration (optional) + * Add your Google Analytics 4 Measurement ID for built-in tracking + */ + const GOOGLE_TAG_ID = '${analytics?.googleTagId || ""}'; + + /* + * Step 3.5: custom HTML header injection (optional) + * Add custom HTML to the top of the page body (e.g., navigation, announcements) + */ + const CUSTOM_HEADER = \`${customHtml?.headerHtml || ""}\`; + + /* + * Step 3.6: custom 404 page configuration (optional) + * Specify a Notion page ID to display when a page is not found + */ + const CUSTOM_404_PAGE_ID = '${custom404?.notionUrl ? getId(custom404.notionUrl) : ""}'; /* Step 4: enter a Google Font name, you can choose from https://fonts.google.com */ const GOOGLE_FONT = '${googleFont || ""}'; @@ -345,6 +383,25 @@ ${slugs response.headers.delete('X-Content-Security-Policy'); } + // Handle 404 with custom page if configured (Issue #12) + if (response.status === 404 && CUSTOM_404_PAGE_ID !== '') { + const notFoundUrl = new URL(url); + notFoundUrl.pathname = '/' + CUSTOM_404_PAGE_ID; + const notFoundResponse = await fetch(notFoundUrl.toString(), { + headers: request.headers, + method: 'GET', + }); + // Return custom 404 page content with 404 status + response = new Response(notFoundResponse.body, { + status: 404, + statusText: 'Not Found', + headers: notFoundResponse.headers, + }); + response.headers.delete('Content-Security-Policy'); + response.headers.delete('X-Content-Security-Policy'); + return appendJavascript(response, SLUG_TO_PAGE, '404'); + } + // Get current slug from page ID for canonical URL const pageId = url.pathname.slice(-32); const currentSlug = PAGE_TO_SLUG[pageId] || ''; @@ -419,6 +476,18 @@ ${slugs } } + class LinkRewriter { + element(element) { + // Remove Notion's default favicon links when custom favicon is set (Issue #16) + if (FAVICON_URL !== '') { + const rel = element.getAttribute('rel'); + if (rel && (rel.includes('icon') || rel === 'apple-touch-icon')) { + element.remove(); + } + } + } + } + class HeadRewriter { constructor(slug) { this.slug = slug; @@ -431,6 +500,24 @@ ${slugs element.append(\`\`, { html: true }); element.append(\`\`, { html: true }); + // Add custom favicon if configured (Issue #16) + if (FAVICON_URL !== '') { + element.append(\`\`, { html: true }); + element.append(\`\`, { html: true }); + element.append(\`\`, { html: true }); + } + + // Add Google Analytics 4 if configured (Issue #14) + if (GOOGLE_TAG_ID !== '') { + element.append(\` + \`, { html: true }); + } + // Add Twitter/X meta tags for social cards (Issue #19) if (TWITTER_HANDLE !== '') { element.append(\`\`, { html: true }); @@ -498,6 +585,10 @@ ${slugs this.SLUG_TO_PAGE = SLUG_TO_PAGE; } element(element) { + // Add custom header HTML at the top of body if configured (Issue #20) + if (CUSTOM_HEADER !== '') { + element.prepend(CUSTOM_HEADER, { html: true }); + } element.append(\`
Powered by Worknot