Notion + CloudFlare + 域名搭建网站

Notion + CloudFlare + 域名搭建网站

Created: March 21, 2022 11:42 AM

0你需要有cloudflare,一个域名和notion账号,并且notion有一个开放的网页

Untitled

已进入cloudflare,如果你没有使用cloudflare他会让你在你购买域名的机构做修改。按照cloudflare官方指引来就可以。

1.CloudFlare 配置 CNAME 记录 ( ajiang.online → notion.so )

Untitled

Untitled

前方添加你的域名或子域名(这里以ajiang.online为例)目标填写为notion.so

3配置worker

Untitled

Untitled

Untitled

  • 直接复制代码

    /* CONFIGURATION STARTS HERE */
      
      /* Step 1: enter your domain name like fruitionsite.com */
      const MY_DOMAIN = 'fruitionsite.com';#改成你的域名
      
      /*
       * Step 2: enter your URL slug to page ID mapping
       * The key on the left is the slug (without the slash)
       * The value on the right is the Notion page ID
       */
      const SLUG_TO_PAGE = {
        '': '771ef38657244c27b9389734a9cbff44',#改成你的page
      };
      
      /* Step 3: enter your page title and description for SEO purposes */
      const PAGE_TITLE = '';
      const PAGE_DESCRIPTION = '';
      
      /* Step 4: enter a Google Font name, you can choose from https://fonts.google.com */
      const GOOGLE_FONT = '';
      
      /* Step 5: enter any custom scripts you'd like */
      const CUSTOM_SCRIPT = ``;
      
      /* CONFIGURATION ENDS HERE */
      
      const PAGE_TO_SLUG = {};
      const slugs = [];
      const pages = [];
      Object.keys(SLUG_TO_PAGE).forEach(slug => {
        const page = SLUG_TO_PAGE[slug];
        slugs.push(slug);
        pages.push(page);
        PAGE_TO_SLUG[page] = slug;
      });
      
      addEventListener('fetch', event => {
        event.respondWith(fetchAndApply(event.request));
      });
    
      function generateSitemap() {
        let sitemap = '<urlset xmlns="http://www.sitemaps.org/schemas/sitemap/0.9">';
        slugs.forEach(
          (slug) =>
            (sitemap +=
              '<url><loc>https://' + MY_DOMAIN + '/' + slug + '</loc></url>')
        );
        sitemap += '</urlset>';
        return sitemap;
      }
      
      const corsHeaders = {
        'Access-Control-Allow-Origin': '*',
        'Access-Control-Allow-Methods': 'GET, HEAD, POST, PUT, OPTIONS',
        'Access-Control-Allow-Headers': 'Content-Type',
      };
      
      function handleOptions(request) {
        if (request.headers.get('Origin') !== null &&
          request.headers.get('Access-Control-Request-Method') !== null &&
          request.headers.get('Access-Control-Request-Headers') !== null) {
          // Handle CORS pre-flight request.
          return new Response(null, {
            headers: corsHeaders
          });
        } else {
          // Handle standard OPTIONS request.
          return new Response(null, {
            headers: {
              'Allow': 'GET, HEAD, POST, PUT, OPTIONS',
            }
          });
        }
      }
      
      async function fetchAndApply(request) {
        if (request.method === 'OPTIONS') {
          return handleOptions(request);
        }
        let url = new URL(request.url);
        url.hostname = 'www.notion.so';
        if (url.pathname === '/robots.txt') {
          return new Response('Sitemap: https://' + MY_DOMAIN + '/sitemap.xml');
        }
        if (url.pathname === '/sitemap.xml') {
          let response = new Response(generateSitemap());
          response.headers.set('content-type', 'application/xml');
          return response;
        }
        let response;
        if (url.pathname.startsWith('/app') && url.pathname.endsWith('js')) {
          response = await fetch(url.toString());
          let body = await response.text();
          response = new Response(body.replace(/www.notion.so/g, MY_DOMAIN).replace(/notion.so/g, MY_DOMAIN), response);
          response.headers.set('Content-Type', 'application/x-javascript');
          return response;
        } else if ((url.pathname.startsWith('/api'))) {
          // Forward API
          response = await fetch(url.toString(), {
            body: url.pathname.startsWith('/api/v3/getPublicPageData') ? null : request.body,
            headers: {
              'content-type': 'application/json;charset=UTF-8',
              'user-agent': 'Mozilla/5.0 (Macintosh; Intel Mac OS X 10_12_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/80.0.3987.163 Safari/537.36'
            },
            method: 'POST',
          });
          response = new Response(response.body, response);
          response.headers.set('Access-Control-Allow-Origin', '*');
          return response;
        } else if (slugs.indexOf(url.pathname.slice(1)) > -1) {
          const pageId = SLUG_TO_PAGE[url.pathname.slice(1)];
          return Response.redirect('https://' + MY_DOMAIN + '/' + pageId, 301);
        } else {
          response = await fetch(url.toString(), {
            body: request.body,
            headers: request.headers,
            method: request.method,
          });
          response = new Response(response.body, response);
          response.headers.delete('Content-Security-Policy');
          response.headers.delete('X-Content-Security-Policy');
        }
      
        return appendJavascript(response, SLUG_TO_PAGE);
      }
      
      class MetaRewriter {
        element(element) {
          if (PAGE_TITLE !== '') {
            if (element.getAttribute('property') === 'og:title'
              || element.getAttribute('name') === 'twitter:title') {
              element.setAttribute('content', PAGE_TITLE);
            }
            if (element.tagName === 'title') {
              element.setInnerContent(PAGE_TITLE);
            }
          }
          if (PAGE_DESCRIPTION !== '') {
            if (element.getAttribute('name') === 'description'
              || element.getAttribute('property') === 'og:description'
              || element.getAttribute('name') === 'twitter:description') {
              element.setAttribute('content', PAGE_DESCRIPTION);
            }
          }
          if (element.getAttribute('property') === 'og:url'
            || element.getAttribute('name') === 'twitter:url') {
            element.setAttribute('content', MY_DOMAIN);
          }
          if (element.getAttribute('name') === 'apple-itunes-app') {
            element.remove();
          }
        }
      }
      
      class HeadRewriter {
        element(element) {
          if (GOOGLE_FONT !== '') {
            element.append(`<link href="https://fonts.googleapis.com/css?family=${GOOGLE_FONT.replace(' ', '+')}:Regular,Bold,Italic&display=swap" rel="stylesheet">
            <style>* { font-family: "${GOOGLE_FONT}" !important; }</style>`, {
              html: true
            });
          }
          element.append(`<style>
          div.notion-topbar > div > div:nth-child(3) { display: none !important; }
          div.notion-topbar > div > div:nth-child(4) { display: none !important; }
          div.notion-topbar > div > div:nth-child(5) { display: none !important; }
          div.notion-topbar > div > div:nth-child(6) { display: none !important; }
          div.notion-topbar-mobile > div:nth-child(3) { display: none !important; }
          div.notion-topbar-mobile > div:nth-child(4) { display: none !important; }
          div.notion-topbar > div > div:nth-child(1n).toggle-mode { display: block !important; }
          div.notion-topbar-mobile > div:nth-child(1n).toggle-mode { display: block !important; }
          </style>`, {
            html: true
          })
        }
      }
      
      class BodyRewriter {
        constructor(SLUG_TO_PAGE) {
          this.SLUG_TO_PAGE = SLUG_TO_PAGE;
        }
        element(element) {
          element.append(`<div style="display:none">Powered by <a href="http://fruitionsite.com">Fruition</a></div>
          <script>
          window.CONFIG.domainBaseUrl = 'https://${MY_DOMAIN}';
          const SLUG_TO_PAGE = ${JSON.stringify(this.SLUG_TO_PAGE)};
          const PAGE_TO_SLUG = {};
          const slugs = [];
          const pages = [];
          const el = document.createElement('div');
          let redirected = false;
          Object.keys(SLUG_TO_PAGE).forEach(slug => {
            const page = SLUG_TO_PAGE[slug];
            slugs.push(slug);
            pages.push(page);
            PAGE_TO_SLUG[page] = slug;
          });
          function getPage() {
            return location.pathname.slice(-32);
          }
          function getSlug() {
            return location.pathname.slice(1);
          }
          function updateSlug() {
            const slug = PAGE_TO_SLUG[getPage()];
            if (slug != null) {
              history.replaceState(history.state, '', '/' + slug);
            }
          }
          function onDark() {
            el.innerHTML = '<div title="Change to Light Mode" style="margin-left: auto; margin-right: 14px; min-width: 0px;"><div role="button" tabindex="0" style="user-select: none; transition: background 120ms ease-in 0s; cursor: pointer; border-radius: 44px;"><div style="display: flex; flex-shrink: 0; height: 14px; width: 26px; border-radius: 44px; padding: 2px; box-sizing: content-box; background: rgb(46, 170, 220); transition: background 200ms ease 0s, box-shadow 200ms ease 0s;"><div style="width: 14px; height: 14px; border-radius: 44px; background: white; transition: transform 200ms ease-out 0s, background 200ms ease-out 0s; transform: translateX(12px) translateY(0px);"></div></div></div></div>';
            document.body.classList.add('dark');
            __console.environment.ThemeStore.setState({ mode: 'dark' });
          };
          function onLight() {
            el.innerHTML = '<div title="Change to Dark Mode" style="margin-left: auto; margin-right: 14px; min-width: 0px;"><div role="button" tabindex="0" style="user-select: none; transition: background 120ms ease-in 0s; cursor: pointer; border-radius: 44px;"><div style="display: flex; flex-shrink: 0; height: 14px; width: 26px; border-radius: 44px; padding: 2px; box-sizing: content-box; background: rgba(135, 131, 120, 0.3); transition: background 200ms ease 0s, box-shadow 200ms ease 0s;"><div style="width: 14px; height: 14px; border-radius: 44px; background: white; transition: transform 200ms ease-out 0s, background 200ms ease-out 0s; transform: translateX(0px) translateY(0px);"></div></div></div></div>';
            document.body.classList.remove('dark');
            __console.environment.ThemeStore.setState({ mode: 'light' });
          }
          function toggle() {
            if (document.body.classList.contains('dark')) {
              onLight();
            } else {
              onDark();
            }
          }
          function addDarkModeButton(device) {
            const nav = device === 'web' ? document.querySelector('.notion-topbar').firstChild : document.querySelector('.notion-topbar-mobile');
            el.className = 'toggle-mode';
            el.addEventListener('click', toggle);
            nav.appendChild(el);
            onLight();
          }
          const observer = new MutationObserver(function() {
            if (redirected) return;
            const nav = document.querySelector('.notion-topbar');
            const mobileNav = document.querySelector('.notion-topbar-mobile');
            if (nav && nav.firstChild && nav.firstChild.firstChild
              || mobileNav && mobileNav.firstChild) {
              redirected = true;
              updateSlug();
              addDarkModeButton(nav ? 'web' : 'mobile');
              const onpopstate = window.onpopstate;
              window.onpopstate = function() {
                if (slugs.includes(getSlug())) {
                  const page = SLUG_TO_PAGE[getSlug()];
                  if (page) {
                    history.replaceState(history.state, 'bypass', '/' + page);
                  }
                }
                onpopstate.apply(this, [].slice.call(arguments));
                updateSlug();
              };
            }
          });
          observer.observe(document.querySelector('#notion-app'), {
            childList: true,
            subtree: true,
          });
          const replaceState = window.history.replaceState;
          window.history.replaceState = function(state) {
            if (arguments[1] !== 'bypass' && slugs.includes(getSlug())) return;
            return replaceState.apply(window.history, arguments);
          };
          const pushState = window.history.pushState;
          window.history.pushState = function(state) {
            const dest = new URL(location.protocol + location.host + arguments[2]);
            const id = dest.pathname.slice(-32);
            if (pages.includes(id)) {
              arguments[2] = '/' + PAGE_TO_SLUG[id];
            }
            return pushState.apply(window.history, arguments);
          };
          const open = window.XMLHttpRequest.prototype.open;
          window.XMLHttpRequest.prototype.open = function() {
            arguments[1] = arguments[1].replace('${MY_DOMAIN}', 'www.notion.so');
            return open.apply(this, [].slice.call(arguments));
          };
        </script>${CUSTOM_SCRIPT}`, {
            html: true
          });
        }
      }
      
      async function appendJavascript(res, SLUG_TO_PAGE) {
        return new HTMLRewriter()
          .on('title', new MetaRewriter())
          .on('meta', new MetaRewriter())
          .on('head', new HeadRewriter())
          .on('body', new BodyRewriter(SLUG_TO_PAGE))
          .transform(res);
      }
    
  • 或者直接在下方输入你的域名和notionurl再点击“COPY THE CODE”,配置文件会自动生成。

    https://fruition.stephenou.vercel.app

4配置路由

在worker右边选择“添加路由”

Untitled

添加你的域名之后选择刚才复制的worker

Untitled

保存后就可以正常查看了。

示例网站

Fruition: Free, Open Source Toolkit for Building Websites with Notion (fruitionsite.com)

使用 **TipTap + Vue 2** 搭建一个 **Notion 风格的编辑器项目** 是完全可行的。虽然 TipTap 官方示例多基于 Vue 3 和 React,但它仍然支持 Vue 2(通过 `@tiptap/vue-2` 插件),你可以构建出类似 Notion 的块级、美观、交互丰富的文档编辑器。 --- ## ✅ 项目目标 搭建一个具有以下功能的 Notion 风格编辑器: - 支持标题、段落、待办列表、引用、代码块等 - 美观的 UI(仿 Notion 的简洁风格) - 实时保存内容 - 可扩展插件系统(如 `/` 命令菜单) - 使用 Vue 2 技术栈 --- ## 🛠 技术选型 | 功能 | 工具 | |------|------| | 编辑器核心 | [TipTap v2](https://v2.tiptap.dev/) + `@tiptap/vue-2` | | 前端框架 | Vue 2.x | | 构建工具 | Vue CLI | | 样式库 | Tailwind CSS 或原生 CSS(推荐 Tailwind 模仿 Notion) | | 状态管理 | Vuex / Options API | | 存储 | LocalStorage / IndexedDB / 后端 API | > ⚠️ 注意:TipTap v2 是为 Vue 2 设计的最后一个兼容版本,新项目建议升级到 Vue 3,但如果你必须使用 Vue 2,本方案完全可用。 --- ## 🔧 步骤一:初始化 Vue 2 项目 ```bash # 安装 Vue CLI(如果没有) npm install -g @vue/cli # 创建 Vue 2 项目 vue create tiptap-notion-vue2 # 在提示中选择 "Manually select features" # 选择: Babel, CSS Pre-processors, Linter # Vue version => Use config in package.json ``` 进入项目并安装依赖: ```bash cd tiptap-notion-vue2 ``` --- ## ✅ 安装 TipTap(Vue 2 兼容版) ```bash npm install @tiptap/vue-2 @tiptap/core@2 @tiptap/starter-kit@2 ``` > TipTap v2 的包名是 `@tiptap/core@2` 和 `@tiptap/starter-kit@2`,确保版本匹配。 --- ## ✅ 配置 Tailwind CSS(可选但推荐) ```bash npm install -D tailwindcss@^2.0 postcss@^8 autoprefixer@^9 npx tailwindcss init ``` ### 创建 `tailwind.config.js` ```js // tailwind.config.js module.exports = { purge: ['./public/index.html', './src/**/*.{vue,js,ts,jsx,tsx}'], darkMode: false, theme: { extend: {}, }, variants: { extend: {}, }, plugins: [], } ``` ### 添加到 `src/assets/styles/tailwind.css` ```css /* src/assets/styles/tailwind.css */ @tailwind base; @tailwind components; @tailwind utilities; ``` ### 引入到 `main.js` ```js // src/main.js import Vue from 'vue' import App from './App.vue' import './assets/styles/tailwind.css' Vue.config.productionTip = false new Vue({ render: h => h(App), }).$mount('#app') ``` --- ## ✅ 核心代码:创建 Notion 风格编辑器组件 ```vue <!-- src/components/NotionEditor.vue --> <template> <div class="notion-editor bg-gray-50 min-h-screen"> <div class="max-w-3xl mx-auto"> <editor-content :editor="editor" /> </div> </div> </template> <script> import { Editor, EditorContent } from '@tiptap/vue-2' import StarterKit from '@tiptap/starter-kit' import Placeholder from '@tiptap/extension-placeholder' import TaskList from '@tiptap/extension-task-list' import TaskItem from '@tiptap/extension-task-item' export default { name: 'NotionEditor', components: { EditorContent, }, data() { return { editor: null, } }, mounted() { this.editor = new Editor({ extensions: [ StarterKit.configure({ bulletList: { keepMarks: true, HTMLAttributes: { class: 'list-disc list-outside leading-6 ml-4 my-1', }, }, orderedList: { keepMarks: true, HTMLAttributes: { class: 'list-decimal list-outside leading-6 ml-4 my-1', }, }, listItem: { HTMLAttributes: { class: 'leading-normal', }, }, }), TaskList, TaskItem.configure({ nested: true, }), Placeholder.configure({ placeholder: ({ node }) => { if (node.type.name === 'heading') return '输入标题...' if (node.type.name === 'paragraph') return '开始输入…' if (node.type.name === 'taskItem') return '待办事项…' return '' }, }), ], content: ` <h1>我的 Notion 笔记</h1> <p>欢迎使用 Vue 2 + TipTap 构建的类 Notion 编辑器。</p> <ul data-type="taskList"> <li data-type="taskItem" data-checked="false">✅ 学习 TipTap</li> <li data-type="taskItem" data-checked="true">🎉 完成项目</li> </ul> `, editorProps: { attributes: { class: 'prose prose-lg focus:outline-none p-6 mx-auto min-h-screen max-w-3xl', }, }, }) // 自动保存到 localStorage this.autoSave() }, methods: { autoSave() { setInterval(() => { if (this.editor) { const content = this.editor.getHTML() localStorage.setItem('notion-vue2-content', content) } }, 2000) }, }, beforeDestroy() { this.editor.destroy() }, } </script> <style scoped> /* 可以添加一些自定义样式 */ .notion-editor { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu; } </style> ``` --- ## ✅ 修改 App.vue ```vue <!-- src/App.vue --> <template> <div id="app"> <NotionEditor /> </div> </template> <script> import NotionEditor from './components/NotionEditor.vue' export default { name: 'App', components: { NotionEditor, }, } </script> ``` --- ## 💡 进阶功能实现建议 ### 1. **恢复上次编辑内容** ```js // 在 mounted 中读取 localStorage const saved = localStorage.getItem('notion-vue2-content') this.editor.commands.setContent(saved || '<p></p>') ``` ### 2. **添加 `/` 命令菜单(Command Menu)** 需要使用 `@tiptap/suggestion` 包(支持 Vue 2): ```bash npm install @tiptap/suggestion@2 ``` 然后配合 `suggestion()` 扩展监听 `/` 输入,弹出命令面板。 ### 3. **块级操作模拟** TipTap 是流式编辑器,但可以通过自定义节点模拟“块”概念: ```js // 示例:自定义 Block 节点(简化) const CustomBlock = Node.create({ name: 'customBlock', group: 'block', content: 'inline*', parseHTML() { return [{ tag: 'div.block' }] }, renderHTML() { return ['div', { class: 'block border-l-4 pl-4 my-2' }, 0] }, }) ``` 再结合拖拽或装饰器实现 Notion 式交互。 ### 4. **美化 UI:模仿 Notion 图标和间距** ```html <!-- 在 Placeholder 前加图标 --> <li data-type="taskItem">▪️ <span>普通文本</span></li> <li data-type="taskItem" data-checked="false">☐ 待办事项</li> <li data-type="taskItem" data-checked="true">✔️ 已完成</li> ``` 或者用 CSS 伪元素添加。 --- ## 📦 项目结构建议 ``` src/ ├── components/ │ └── NotionEditor.vue ├── assets/ │ └── styles/ │ └── tailwind.css ├── plugins/ │ └── tiptap.js (可抽离配置) └── App.vue ``` --- ## ✅ 启动项目 ```bash npm run serve ``` 访问 `http://localhost:8080` 即可看到你的 Notion 风格编辑器! --- ## ✅ 总结 你已经成功在 Vue 2 中集成 TipTap,并实现了: - 富文本编辑能力 - 多种内容类型(标题、列表、任务项) - 自动保存 - 清爽的 Notion 风格 UI - 可扩展架构 ---
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

阿江要努力鸭

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值