Last active
July 6, 2023 15:15
-
-
Save izuolan/b8fc562894fabd127590b8a0f0a615c4 to your computer and use it in GitHub Desktop.
Custom domain for your Craft.do pages. Demo: https://next-craft.vercel.app and Tutorial: https://zuolan.me/en/next_craft_en
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
// Your domain name | |
const MY_DOMAIN = 'note.example.com' | |
// Website language | |
const LANG = 'en' | |
// Favicon url | |
const FAVICON_URL = 'https://example.com/favicon.ico' | |
// Your config page link | |
const CONFIG_URL = 'https://www.craft.do/s/XXXXXXXXX' | |
// Your Telegram Token and ID | |
const TG_TOKEN = "" | |
const TG_CHAT_ID = "" | |
// END | |
// Default function | |
addEventListener('fetch', event => { | |
event.respondWith(fetchAndApply(event.request)) | |
}) | |
// Fetch url | |
async function fetchAndApply(request) { | |
let url = new URL(request.url) | |
// Set upstream domain | |
url.host = 'www.craft.do' | |
let pathname = url.pathname | |
let response = null | |
const config_obj = await configParser() | |
// Automatically generate robots.txt and sitemap.xml | |
if (pathname === '/robots.txt') { | |
return new Response('Sitemap: https://' + MY_DOMAIN + '/sitemap.xml') | |
} | |
if (pathname === '/sitemap.xml') { | |
let sitemap = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">' | |
for(var path in config_obj){ | |
sitemap += '<url><loc>https://' + MY_DOMAIN + '/' + path + '</loc></url>' | |
} | |
sitemap += '</urlset>' | |
response = new Response(sitemap) | |
response.headers.set('content-type', 'application/xml') | |
return response | |
} | |
if (pathname === '/favicon.svg') { | |
response = new Response('<svg xmlns="http://www.w3.org/2000/svg" width="100pt" height="100pt" viewBox="0 0 100 100"><g fill="blue" transform="translate(0.000000,100) scale(0.080000,-0.080000)"><path d="M762 1203 c-6 -15 -13 -46 -17 -68 -4 -22 -13 -49 -20 -61 -15 -23 -122 -69 -257 -109 -49 -14 -88 -28 -88 -29 0 -2 33 -20 73 -40 49 -24 87 -36 115 -36 28 0 42 -4 42 -13 0 -34 -295 -517 -390 -639 -40 -52 -4 -28 86 56 49 46 105 109 124 141 19 31 64 98 100 148 77 108 125 186 173 283 20 39 46 78 59 86 13 8 69 34 126 58 107 45 118 57 110 111 -3 21 -10 25 -78 34 l-75 10 -5 45 c-5 42 -7 45 -36 48 -26 3 -33 -1 -42 -25z"/><path d="M754 616 c-40 -19 -88 -39 -108 -46 -43 -14 -45 -30 -7 -72 25 -28 33 -31 80 -30 39 1 54 -3 58 -15 7 -18 -30 -140 -58 -192 -36 -67 6 -93 135 -84 l86 6 0 -26 c0 -14 -4 -37 -10 -51 -5 -14 -8 -26 -6 -26 7 0 110 68 129 85 11 10 17 30 17 60 0 62 -22 70 -150 57 -52 -5 -98 -6 -103 -2 -4 3 3 31 16 61 13 30 32 78 42 108 10 30 28 70 41 89 26 38 30 63 14 93 -17 31 -91 25 -176 -15z"/></g></svg>') | |
response.headers.set('content-type', 'image/svg+xml') | |
return response | |
} | |
// Default path | |
if (pathname === '/') { | |
url.pathname = '/s/' + config_obj['index'].slice(23) | |
} | |
// Prohibit other Craft.do share pages | |
else if (pathname.startsWith('/s/')) { | |
url.pathname = "/404" | |
} | |
// Proxy index blocks pages | |
else if (pathname.startsWith('/b/')) { | |
url.pathname = '/s/' + config_obj['index'].slice(23) + pathname | |
} | |
// TODO: There is an unresolved issue here | |
// External pages url is troublesome to deal with. | |
else if (pathname.includes('/x/')) { | |
// url.pathname = '/s/' + config_obj['index'].slice(23) + pathname | |
return Response.redirect('https://' + MY_DOMAIN, 301) | |
} | |
// Proxy js | |
else if (pathname.startsWith('/share/') && pathname.endsWith('.js')) { | |
response = await fetch(url) | |
let body = await response.text() | |
// replace all js files domain | |
response = new Response(body.replace(/www.craft.do/g, MY_DOMAIN), response) | |
response.headers.set('Content-Type', 'application/x-javascript') | |
} | |
// Proxy images | |
else if (pathname.startsWith('/img/')) { | |
// Proxy api.craft.do, pages preview api | |
// This code can proxy "MY_DOMAIN/img/<pathname>" --> "api.craft.do/render/preview/<slug>" | |
url.host = 'api.craft.do' | |
let path_name = pathname.slice(5) | |
let img_slug = config_obj[path_name].slice(23) | |
url.pathname = pathname.replace(pathname, '/render/preview/' + img_slug) | |
// Cache images | |
const cacheImage = `https://${url.host}${url.pathname}` | |
let response = await fetch(url.href, { | |
cf: { | |
// Always cache this fetch regardless of content type | |
// for a max of 86400 seconds before revalidating the resource | |
cacheTtl: 86400, | |
cacheEverything: true, | |
//Enterprise only feature, see Cache API for other plans | |
cacheKey: cacheImage, | |
}, | |
}) | |
// Reconstruct the Response object to make its headers mutable. | |
response = new Response(response.body, response) | |
// Set cache control headers to cache on browser for 25 minutes | |
response.headers.set("Cache-Control", "max-age=1500") | |
return response | |
} | |
// Disable Craft log. | |
else if (pathname.startsWith('/api/log/')) { | |
return new Response('Disable loging.') | |
} | |
// Proxy comment API. | |
else if (pathname.startsWith('/api/') && (pathname.includes('submitAnon'))) { | |
const init = { | |
body: request.body, | |
method: 'PUT', | |
headers: { | |
"content-type": "application/json;charset=UTF-8", | |
}, | |
} | |
const response = await fetch(url.href, init) | |
const resp_json = await response.json() | |
const resp_str = JSON.stringify(resp_json) | |
// Telegram notify | |
const comment_message = resp_json.comments[0].content | |
const craft_slug = pathname.split("/")[5] | |
const craft_url = 'https://www.craft.do/s/' + craft_slug | |
const comment_slug = findJsonKey(config_obj, craft_url) | |
const tg_message = 'Comment URL:\n' | |
+ 'https://' + MY_DOMAIN + '/' + comment_slug | |
+ '\n\n' | |
+ 'Comment Message:\n' + comment_message | |
await sendToTelegram(tg_message) | |
return new Response(resp_str) | |
} | |
else { | |
try { | |
let urlIndexSlug = null | |
if (pathname.startsWith('/en/') || pathname.startsWith('/p/') || pathname.startsWith('/page/')) { | |
urlIndexSlug = pathname.split("/")[1] + '/' + pathname.split("/")[2] | |
} else { | |
urlIndexSlug = pathname.split("/")[1] | |
} | |
let configPath = config_obj[urlIndexSlug].slice(23) | |
url.pathname = '/s/' + configPath | |
console.log(url.pathname) | |
if (typeof(configPath) == "undefined") { throw new Error('404 not found: ' + configPath) } | |
} catch (error) { | |
if (pathname.startsWith('/api/') || pathname.endsWith('.css') || pathname.endsWith('.webmanifest') || pathname.endsWith('.svg')) { | |
// nothing | |
} else { | |
url.pathname = '/404' | |
// return new Response(error.message) | |
} | |
} | |
} | |
class AttributeRewriter { | |
element(element) { | |
if (element.getAttribute('property') === 'og:url') { | |
element.setAttribute('content', 'https://' + MY_DOMAIN + pathname) | |
} | |
if (element.getAttribute('property') === 'og:image') { | |
if (pathname === '/') { pathname = '/index' } | |
element.setAttribute('content', 'https://' + MY_DOMAIN + '/img' + pathname) | |
} | |
if (element.getAttribute('name') === 'luki:api-endpoint') { | |
element.setAttribute('content', 'https://' + MY_DOMAIN + '/api/') | |
} | |
if (element.getAttribute('lang') === 'en') { | |
element.setAttribute('lang', LANG) | |
} | |
if (element.getAttribute('rel') === 'icon') { | |
element.setAttribute('href', FAVICON_URL) | |
} | |
if (element.getAttribute('rel') === 'apple-touch-icon') { | |
element.setAttribute('href', FAVICON_URL) | |
} | |
} | |
} | |
class RemoveElement { | |
element(element) { | |
element.remove() | |
} | |
} | |
async function rewriteHTML(res) { | |
res.headers.delete("Content-Security-Policy") | |
return new HTMLRewriter() | |
.on('body', new BodyRewriter()) | |
.on('head', new HeadRewriter()) | |
.on('html', new AttributeRewriter()) | |
.on('meta', new AttributeRewriter()) | |
.on('link', new AttributeRewriter()) | |
.on('meta[name="robots"]', new RemoveElement()) // SEO | |
.on('head>style', new RemoveElement()) // Remove fonts | |
.on('script[src="https://www.craft.do/assets/js/analytics2.js"]', new RemoveElement()) // Delete analytics js | |
.transform(res) | |
} | |
let method = request.method | |
let request_headers = request.headers | |
let new_request_headers = new Headers(request_headers) | |
new_request_headers.set('Host', url.hostname) | |
new_request_headers.set('Referer', url.hostname) | |
let original_response = await fetch(url.href, { | |
method: method, | |
headers: new_request_headers | |
}) | |
let response_headers = original_response.headers | |
let new_response_headers = new Headers(response_headers) | |
let status = original_response.status | |
response = new Response(original_response.body, { | |
status, | |
headers: new_response_headers | |
}) | |
// If you want change anything in response. | |
let text = await response.text() | |
// Return modified response. | |
let modified_response = new Response(text, { | |
status: response.status, | |
statusText: response.statusText, | |
headers: response.headers | |
}) | |
if (pathname.startsWith('/share/static/js/') && (pathname.includes('codehighlight'))) { | |
return modified_response | |
} else { | |
return rewriteHTML(modified_response) | |
} | |
} | |
async function configParser() { | |
// Delete string "https://www.craft.do/s/" | |
let config_slug = CONFIG_URL.slice(23) | |
const api_url = 'https://www.craft.do/api/share/' + config_slug | |
const init = { | |
headers: { | |
"content-type": "application/json;charset=UTF-8", | |
}, | |
} | |
const config_response = await fetch(api_url, init) // Get www.craft.do/api/share/<slug> content. | |
const response_json = await config_response.json() // Convert the content to json format (string). | |
const content_json = response_json.blocks[1].content // Get the json data of the first block in the body. | |
const content_str = JSON.stringify(content_json) // Convert json to string. | |
// Handle escape characters. | |
const config_json = content_str.replace(/\\t/g, '').replace(/\\n/g, '').replace(/\\/g, '').replace('"{', '{').replace('}"', '}') | |
let config_obj = JSON.parse(config_json) | |
return config_obj | |
} | |
async function sendToTelegram(message) { | |
const tgUrl = "https://api.telegram.org/bot" + TG_TOKEN + "/sendMessage" | |
const init = { | |
method: 'POST', | |
headers: { | |
"content-type": "application/json;charset=UTF-8", | |
}, | |
body: JSON.stringify({ | |
"chat_id": TG_CHAT_ID, | |
"text": message | |
}) | |
} | |
await fetch(tgUrl, init) | |
// const response = await fetch(tgUrl, init) | |
// const resp_text = await response.text() | |
// return new Response(resp_text) | |
} | |
function findJsonKey(obj, value, compare = (a, b) => a === b) { | |
return Object.keys(obj).find(k => compare(obj[k], value)) | |
} | |
class BodyRewriter { | |
element(element) { | |
// Append your html | |
element.append(` | |
`, { | |
html: true | |
}) | |
} | |
} | |
class HeadRewriter { | |
element(element) { | |
element.append(` | |
<style> | |
/* Hide the Craft "Login in" button in comment board. */ | |
.sc-CtfFt { | |
visibility: hidden; | |
} | |
.hGGlzy { | |
visibility: hidden; | |
} | |
</style> | |
`, { | |
html: true | |
}) | |
} | |
} |
Hi! Thank you for sharing this. It has helped me a lot. Is it possible that it works with the Craft analytics? Right now it does not track any visitors. Thank you!
@gustavojellav Oh, I removed the Craft analysis code by default. You can delete this script line 210, then Craft's analysis will be enabled.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Hi! Thank you for sharing this. It has helped me a lot. Is it possible that it works with the Craft analytics? Right now it does not track any visitors. Thank you!