-
-
Save izuolan/b8fc562894fabd127590b8a0f0a615c4 to your computer and use it in GitHub Desktop.
// 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 | |
}) | |
} | |
} |
Thanks a lot !
I'm ok with points 1 and 2.
Point 3 has direct relation with the craft config page ?
Will investigate with access, i'm new with cloudflare, but seems i begin to understand.
One more time thanks for sharing your time and knowledge.
Hi there. Is it possible to use this custom domain setup with Craft pages that have password-protection enabled? At present, I am unable to use the password to gain access to the page.
@moondigital This version is adapted to the password access, but this worker.js has not yet.
You need insert this somewhere to support webfonts
// Proxy fonts
else if (pathname.startsWith('/share/') && pathname.endsWith('.woff2')) {
response = await fetch(url)
// The readable side will become our new response body.
let { readable, writable } = new TransformStream();
// Start pumping the body. NOTE: No await!
response.body.pipeTo(writable);
// ... and deliver our Response while that’s running.
return new Response(readable, response);
}
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!
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.
Hi, @datagitateur