diff --git a/README.md b/README.md index cb6cb85..d00ecf4 100644 --- a/README.md +++ b/README.md @@ -30,6 +30,26 @@ Deploy anywhere that serves static files. For local development: npx live-server ``` +### Cloudflare Pages + +1. Create a new repository on GitHub. +2. Push your code to the repository. +3. Go to [Cloudflare Pages](https://pages.cloudflare.com/) and connect your GitHub repository. +4. Configure the build settings: + * **Production branch:** `main` (or your main branch name) + * **Build command:** Leave empty + * **Build output directory:** `/` (root) +5. Save and deploy. + +### Optional: Cloudflare Worker for OG/Twitter Tags + +For improved SEO and social sharing, you can use a Cloudflare Worker to dynamically generate OG/Twitter tags. + +1. Create a new Cloudflare Worker using the code in `cloudflare-worker.js`. +2. Set the `SITE_URL` environment variable to where your site will be located, eg. `https://lit.ruv.wtf/docs/` +3. Set the `DOCS_URL` environment variable to the URL where your documentation files are hosted (usually your Cloudflare Pages URL). +4. Configure a route in your Cloudflare account to route all requests to your Cloudflare Pages site through the worker. + ## Project Structure ``` diff --git a/cloudflare-worker.js b/cloudflare-worker.js new file mode 100644 index 0000000..3a28cf2 --- /dev/null +++ b/cloudflare-worker.js @@ -0,0 +1,127 @@ +async function generateOGTags(url, SITE_URL, DOCS_URL) { + const params = new URLSearchParams(url.search); + let slug = ''; + if (params.has('')) { + slug = params.get(''); + } else { + for (const [key] of params) { + slug = key; + break; + } + } + + const indexResponse = await fetch(`${DOCS_URL}/index.json`); + const indexData = await indexResponse.json(); + const defaultPage = indexData.defaultPage || 'welcome'; + slug = slug || defaultPage; + + function findDocumentBySlug(documents, targetSlug) { + for (const doc of documents) { + if (doc.slug === targetSlug) return doc; + if (doc.type === 'folder' && doc.items) { + const found = findDocumentBySlug(doc.items, targetSlug); + if (found) return found; + } + } + return null; + } + + const currentDoc = findDocumentBySlug(indexData.documents, slug); + let description = indexData.metadata?.description || 'Documentation reader'; + let thumbnail = indexData.metadata?.thumbnail || 'img/og-image.png'; + if (currentDoc) { + try { + const docResponse = await fetch(`${DOCS_URL}/${currentDoc.path}`); + const content = await docResponse.text(); + + const metadataMatch = content.match(/---\n([\s\S]*?)\n---/); + if (metadataMatch) { + const metadataStr = metadataMatch[1]; + console.log(metadataMatch); + const thumbnailMatch = metadataStr.match(/thumbnail:\s*(.*)/); + if (thumbnailMatch) { + thumbnail = thumbnailMatch[1].trim(); + } + const descriptionMatch = metadataStr.match(/description:\s*(.*)/); + if (descriptionMatch) { + description = descriptionMatch[1].trim(); + } + if (!description) { + // Get content after metadata + const afterMetadata = content.substring( + metadataMatch.index + metadataMatch[0].length + ); + const paragraphMatch = afterMetadata + .split('\n\n') + .find(p => p.trim() && !p.startsWith('#')); + if (paragraphMatch) { + description = paragraphMatch.trim().replace(/\[|\]|\(|\)/g, '').substring(0, 160); + } + } + } + } catch (e) { + console.error('Error fetching document:', e); + } + } + const site = new URL(SITE_URL); + site.pathname += (url.pathname === '/' ? '' : url.pathname); + site.search = url.search; + site.hash = url.hash; + + const title = currentDoc?.title || 'Documentation'; + const ogImage = new URL(thumbnail, SITE_URL).href; + const siteName = indexData.metadata?.site_name || 'Documentation'; + + return ` + + + + + + + + + + `; +} + +async function handleRequest(request, SITE_URL, DOCS_URL) { + const url = new URL(request.url); + const siteUrlObj = new URL(SITE_URL); + const pathPrefix = siteUrlObj.pathname; + + const strippedPath = url.pathname.startsWith(pathPrefix) + ? url.pathname.slice(pathPrefix.length - 1) + : url.pathname; + + const docUrl = new URL(DOCS_URL); + docUrl.pathname = strippedPath; + docUrl.search = url.search; + docUrl.hash = url.hash; + + const response = await fetch(docUrl.toString(), request); + + if (url.pathname === pathPrefix || url.pathname === '/') { + const contentType = response.headers.get('content-type') || ''; + if (contentType.includes('text/html')) { + let html = await response.text(); + const ogTags = await generateOGTags(url, SITE_URL, DOCS_URL); + html = html.replace('', ogTags); + return new Response(html, { + headers: { + 'content-type': 'text/html;charset=UTF-8', + ...Object.fromEntries(response.headers) + } + }); + } + } + return new Response(response.body, response); +} + +export default { + async fetch(request, env, ctx) { + const SITE_URL = env.SITE_URL; + const DOCS_URL = env.DOCS_URL; + return handleRequest(request, SITE_URL, DOCS_URL); + } +};