-
-
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 | |
}) | |
} | |
} |
@jwasinski After some time, I found that paths like /b/xxxx or /x/xxxx are not SEO friendly and Google will hardly index such pages. I want to make the url paths look better. Maybe a static page generation tool based on the Craft.do API would be interesting. I haven't tried it yet.
Your solution is what I am looking for. But I can’t seem to implement it. When I get to the last step: deploying the worker, I get this error: Error 1101: Worker threw exception. Just to clarify, do I need to buy the domain in Cloudflare? Or could I use a domain I bought from an external registrar (like Google Domains)? BTW, I'm not a developer.
@vinceimbat I recorded a video tutorial, starting with Cloudflare registration. You can register a domain name anywhere, but you need to let Cloudflare take over DNS resolution.
Apologies if this was mentioned somewhere already, but how do I enable comments? I've tried enabling them through Craft and I've setup the Telegram stuff, but the comment button doesn't show on my website.
nvm, I had to enable comments for the parent page. Would it be possible to only enable comments on certain pages?
@izuolan Thank you for the video tutorial. But I still get this error even after following the video:
@akumeilol Comments are managed by Craft, and you can disable comments on certain pages by setting in Craft.
@vinceimbat It looks like your CONFIG_URL (www.craft.do/s/9Q1QI0QrvZNnNf) does not exist, or no "index" in the json?
@izuolan thanks! Turns out, the config URL is indeed the problem. Ok, it now works in the Cloudflare address: https://knu-salin.vinceimbat.workers.dev/. Thanks!
I proceeded to create a route as you instructed in the tutorial. It should point to: https://salin.kaliskisnaulap.com/
At first, there's the Craft rotating loading screen... But then the screen says: Something went wrong :(
Is there one last mistake I am making?
@vinceimbat Change MY_DOMAIN = 'salin.kaliskisnaulap.com'
:)
Thank you @izuolan ! Everything seems to be working now (https://salin.kaliskisnaulap.com/). I will be using your solution on my personal site, which I'll update in the following days. Craft said it will introduce custom domains this year, but who knows when and how much. Your solution is the best I found for now.
The only problem I notice is that the page doesn't render the images or texts when I use Cards, showing just the outline and background of a box. That's why I converted my Card to a Page just so people see what the "blank box" is all about. Other than that, I think everything looks the same as the original craft page (and better! Because of the custom domain).
More power to your work!
@vinceimbat The card thumbnails take a few seconds (< 5s) to generate for the first time, and then it will be faster (< 2s).
@izuolan I used your script for my other site, the one that houses my zettelkasten. It seems that your solution only works for page links but not for wikilinks. When I click wikilinks in the paragraphs, I get this error:
@vinceimbat Yes, there is an unresolved issue here.
You can remove the 73 lines of code, and it will support external documentation (wikilinks).
https://gist.github.com/izuolan/b8fc562894fabd127590b8a0f0a615c4#file-worker-js-L73
Is it possible to configure the website to apex? Cloudflare gives me the following warning:
Because CNAME records are not allowed at the zone apex (RFC 1034), CNAME flattening will be applied to this record.
Also, do you have any plans on creating a static website generator with Craft API?
@anaclumos Of course, once the worker triggers are set correctly, DNS resolution is an irrelevant setting.
I have not tried to generate a static website using the Craft API. But I remember that someone in Slack had posted a paid project to publish Craft pages as static pages.
Hi everyone,
In order to share documents online with my own domain I am successfully testing this worker.
The content will not be indexed, no seo, no sitemap : I simply removed the code
// 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>'
}
when I look at the code of the generated page I see robots: noindex and do not find a sitemap
Do you think this is enough?
And second question in the craft.do custom domain site in "done" section there is
Support page whitelist
Password access support (public but requires password access)
What is page Whitelist ?
How can we protect a page with password ?
Thanks in advance
Hi, @datagitateur
- I see that the noindex meta tag has been removed in demo page. The code just removed "robots" meta tag, check here.
- robots.txt and sitemap.xml is generated by parsing the CONFIG_URL page first line json. Delete this part of the code and it will not be generated.
- Whitelist means: the worker will only forward the links in the CONFIG_URL JSON, not other Craft.do pages. The advantage of this is that others will not steal your domain name.
- Protect a page with password: Cloudflare Worker can work with Cloudflare Access, by creating specific access policies, to protect some paths pages. For example, the code here. The "/en/", "/page/", "/p/" are my personalized path definitions. You can take other names. For example, change "/p/" to "/password/", and then create a new policy in Access, access to the path requires a password. "Password protection" provided by Cloudflare Access, but I have provided the convenience of customizing the "path" in the code.
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.
Any idea on the solution to the external pages issue (line 70?)
If it helps I've set it up on notes.josef.wasin.ski
I also setup one based on iframes at josef.wasin.ski/notes – but it doesn't have the benefit of "pretty" urls