Skip to content

Instantly share code, notes, and snippets.

@izuolan
Last active July 6, 2023 15:15
Show Gist options
  • Save izuolan/b8fc562894fabd127590b8a0f0a615c4 to your computer and use it in GitHub Desktop.
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
// 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
})
}
}
@gustavojellav
Copy link

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!

@izuolan
Copy link
Author

izuolan commented Aug 15, 2022

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