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);
+ }
+};