├── .changeset
├── README.md
├── config.json
├── wet-carrots-shout.md
└── yellow-dragons-admire.md
├── .editorconfig
├── .eslintignore
├── .eslintrc.cjs
├── .github
└── workflows
│ ├── main.yml
│ └── publish.yml
├── .gitignore
├── .prettierignore
├── .prettierrc
├── CHANGELOG.md
├── LICENSE
├── README.md
├── assets
├── toggle_moon.svg
└── toggle_sun.svg
├── build-body-js-string.sh
├── package.json
├── pnpm-lock.yaml
├── src
├── append-js-pf.ts
├── append-js.ts
├── cli
│ ├── copy-files.ts
│ ├── index.ts
│ ├── init-repo.ts
│ ├── parser-config.ts
│ ├── site-config.ts
│ └── validators.ts
├── config-helpers.ts
├── handlers
│ ├── handle-api.ts
│ ├── handle-app-js.ts
│ ├── handle-favicon.ts
│ ├── handle-js.ts
│ ├── handle-options.ts
│ ├── handle-other.ts
│ ├── handle-sitemap.ts
│ └── index.ts
├── index.ts
├── reverse-proxy-init.ts
├── reverse-proxy.ts
├── rewriters
│ ├── _body-js-string.ts
│ ├── body-rewriter.ts
│ ├── body.js
│ ├── element-handler.ts
│ ├── head-rewriter.ts
│ ├── index.ts
│ └── meta-rewriter.ts
└── types.ts
├── templates
└── default
│ ├── .editorconfig
│ ├── _gitignore
│ ├── package.json
│ ├── src
│ ├── _build-page-script-js-string.js
│ ├── _page-script-js-string.ts
│ ├── index.ts
│ ├── page-script.js
│ └── site-config.ts
│ ├── tsconfig.json
│ └── wrangler.toml
└── tsconfig.json
/.changeset/README.md:
--------------------------------------------------------------------------------
1 | # Changesets
2 |
3 | Hello and welcome! This folder has been automatically generated by `@changesets/cli`, a build tool that works
4 | with multi-package repos, or single-package repos to help you version and publish your code. You can
5 | find the full documentation for it [in our repository](https://github.com/changesets/changesets)
6 |
7 | We have a quick list of common questions to get you started engaging with this project in
8 | [our documentation](https://github.com/changesets/changesets/blob/main/docs/common-questions.md)
9 |
--------------------------------------------------------------------------------
/.changeset/config.json:
--------------------------------------------------------------------------------
1 | {
2 | "$schema": "https://unpkg.com/@changesets/config@3.0.0/schema.json",
3 | "changelog": "@changesets/cli/changelog",
4 | "commit": false,
5 | "fixed": [],
6 | "linked": [],
7 | "access": "restricted",
8 | "baseBranch": "main",
9 | "updateInternalDependencies": "patch",
10 | "ignore": []
11 | }
12 |
--------------------------------------------------------------------------------
/.changeset/wet-carrots-shout.md:
--------------------------------------------------------------------------------
1 | ---
2 | "notehost": patch
3 | ---
4 |
5 | Asks for main Notion page ID, uses chalk for output
6 |
--------------------------------------------------------------------------------
/.changeset/yellow-dragons-admire.md:
--------------------------------------------------------------------------------
1 | ---
2 | "notehost": patch
3 | ---
4 |
5 | Update README
6 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | tab_width = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 | semicolon = false
12 | quote_type = single
13 |
14 | [*.yml]
15 | indent_style = space
16 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | /**/node_modules/*
2 | node_modules/
3 |
4 | dist/
5 | build/
6 | out/
7 | templates/
8 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | // eslint-disable-next-line no-undef
2 | module.exports = {
3 | root: true,
4 | env: {
5 | browser: true,
6 | es2021: true,
7 | },
8 | extends: [
9 | 'eslint:recommended',
10 | 'plugin:@typescript-eslint/recommended',
11 | // "plugin:prettier/recommended",
12 | // "prettier",
13 | ],
14 | parser: '@typescript-eslint/parser',
15 | parserOptions: {
16 | ecmaVersion: 12,
17 | sourceType: 'module',
18 | },
19 | plugins: [
20 | '@typescript-eslint',
21 | '@stylistic',
22 | // "prettier",
23 | ],
24 | rules: {
25 | 'import/prefer-default-export': ['off'],
26 | 'import/no-unresolved': ['off'],
27 | 'import/extensions': ['off'],
28 |
29 | 'no-restricted-syntax': ['off'],
30 | 'no-use-before-define': ['off'],
31 | 'no-case-declarations': ['off'],
32 |
33 | '@typescript-eslint/no-unused-vars': 'error',
34 | '@typescript-eslint/no-explicit-any': 'error',
35 | // '@typescript-eslint/no-floating-promises': 'error',
36 | // '@typescript-eslint/promise-function-async': 'error',
37 | // '@typescript-eslint/no-misused-promises': 'error',
38 | // '@typescript-eslint/require-await': 'error',
39 | // '@typescript-eslint/no-shadow': 'error',
40 |
41 | // '@stylistic/object-curly-spacing': ['error', 'always'],
42 | // '@stylistic/object-curly-newline': [
43 | // 'error',
44 | // {
45 | // ObjectExpression: { multiline: true, minProperties: 4, consistent: true },
46 | // ObjectPattern: { multiline: true, minProperties: 4, consistent: true },
47 | // },
48 | // ],
49 | // '@stylistic/switch-colon-spacing': ['error', { after: false, before: false }],
50 | '@stylistic/padding-line-between-statements': [
51 | 'error',
52 | {
53 | blankLine: 'always',
54 | prev: '*',
55 | next: [
56 | 'block',
57 | 'block-like',
58 | 'cjs-export',
59 | 'class',
60 | 'const',
61 | 'export',
62 | 'import',
63 | 'let',
64 | 'var',
65 | ],
66 | },
67 | {
68 | blankLine: 'always',
69 | prev: [
70 | 'block',
71 | 'block-like',
72 | 'cjs-export',
73 | 'class',
74 | 'const',
75 | 'export',
76 | 'import',
77 | 'let',
78 | 'var',
79 | ],
80 | next: '*',
81 | },
82 | {
83 | blankLine: 'never',
84 | prev: ['const', 'let', 'var'],
85 | next: ['const', 'let', 'var'],
86 | },
87 | {
88 | blankLine: 'any',
89 | prev: ['export', 'import'],
90 | next: ['export', 'import'],
91 | },
92 | { blankLine: 'always', prev: '*', next: 'return' },
93 | ],
94 | },
95 | };
96 |
--------------------------------------------------------------------------------
/.github/workflows/main.yml:
--------------------------------------------------------------------------------
1 | name: CI
2 | on:
3 | push:
4 | branches:
5 | - "**"
6 |
7 | jobs:
8 | build:
9 | runs-on: ubuntu-latest
10 | steps:
11 | - uses: actions/checkout@v3
12 | - uses: pnpm/action-setup@v2
13 | with:
14 | version: 7
15 | - uses: actions/setup-node@v3
16 | with:
17 | node-version: 18.x
18 | cache: "pnpm"
19 |
20 | - run: pnpm install --frozen-lockfile
21 | - run: pnpm run lint && pnpm run build
22 |
--------------------------------------------------------------------------------
/.github/workflows/publish.yml:
--------------------------------------------------------------------------------
1 | name: Publish
2 | on:
3 | workflow_run:
4 | workflows: [CI]
5 | branches: [main]
6 | types: [completed]
7 |
8 | concurrency: ${{ github.workflow }}-${{ github.ref }}
9 |
10 | permissions:
11 | contents: write
12 | pull-requests: write
13 |
14 | jobs:
15 | publish:
16 | if: ${{ github.event.workflow_run.conclusion == 'success' }}
17 | runs-on: ubuntu-latest
18 | steps:
19 | - uses: actions/checkout@v3
20 | - uses: pnpm/action-setup@v2
21 | with:
22 | version: 7
23 | - uses: actions/setup-node@v3
24 | with:
25 | node-version: 18.x
26 | cache: "pnpm"
27 |
28 | - run: pnpm install --frozen-lockfile
29 | - name: Create Release Pull Request or Publish
30 | id: changesets
31 | uses: changesets/action@v1
32 | with:
33 | publish: pnpm run release
34 | env:
35 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
36 | NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
37 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # Logs
2 |
3 | logs
4 | _.log
5 | npm-debug.log_
6 | yarn-debug.log*
7 | yarn-error.log*
8 | lerna-debug.log*
9 | .pnpm-debug.log*
10 |
11 | # Diagnostic reports (https://nodejs.org/api/report.html)
12 |
13 | report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json
14 |
15 | # Runtime data
16 |
17 | pids
18 | _.pid
19 | _.seed
20 | \*.pid.lock
21 |
22 | # Directory for instrumented libs generated by jscoverage/JSCover
23 |
24 | lib-cov
25 |
26 | # Coverage directory used by tools like istanbul
27 |
28 | coverage
29 | \*.lcov
30 |
31 | # nyc test coverage
32 |
33 | .nyc_output
34 |
35 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
36 |
37 | .grunt
38 |
39 | # Bower dependency directory (https://bower.io/)
40 |
41 | bower_components
42 |
43 | # node-waf configuration
44 |
45 | .lock-wscript
46 |
47 | # Compiled binary addons (https://nodejs.org/api/addons.html)
48 |
49 | build/Release
50 |
51 | # Dependency directories
52 |
53 | node_modules/
54 | jspm_packages/
55 |
56 | # Snowpack dependency directory (https://snowpack.dev/)
57 |
58 | web_modules/
59 |
60 | # TypeScript cache
61 |
62 | \*.tsbuildinfo
63 |
64 | # Optional npm cache directory
65 |
66 | .npm
67 |
68 | # Optional eslint cache
69 |
70 | .eslintcache
71 |
72 | # Optional stylelint cache
73 |
74 | .stylelintcache
75 |
76 | # Microbundle cache
77 |
78 | .rpt2_cache/
79 | .rts2_cache_cjs/
80 | .rts2_cache_es/
81 | .rts2_cache_umd/
82 |
83 | # Optional REPL history
84 |
85 | .node_repl_history
86 |
87 | # Output of 'npm pack'
88 |
89 | \*.tgz
90 |
91 | # Yarn Integrity file
92 |
93 | .yarn-integrity
94 |
95 | # dotenv environment variable files
96 |
97 | .env
98 | .env.development.local
99 | .env.test.local
100 | .env.production.local
101 | .env.local
102 |
103 | # parcel-bundler cache (https://parceljs.org/)
104 |
105 | .cache
106 | .parcel-cache
107 |
108 | # Next.js build output
109 |
110 | .next
111 | out
112 |
113 | # Nuxt.js build / generate output
114 |
115 | .nuxt
116 | dist
117 |
118 | # Gatsby files
119 |
120 | .cache/
121 |
122 | # Comment in the public line in if your project uses Gatsby and not Next.js
123 |
124 | # https://nextjs.org/blog/next-9-1#public-directory-support
125 |
126 | # public
127 |
128 | # vuepress build output
129 |
130 | .vuepress/dist
131 |
132 | # vuepress v2.x temp and cache directory
133 |
134 | .temp
135 | .cache
136 |
137 | # Docusaurus cache and generated files
138 |
139 | .docusaurus
140 |
141 | # Serverless directories
142 |
143 | .serverless/
144 |
145 | # FuseBox cache
146 |
147 | .fusebox/
148 |
149 | # DynamoDB Local files
150 |
151 | .dynamodb/
152 |
153 | # TernJS port file
154 |
155 | .tern-port
156 |
157 | # Stores VSCode versions used for testing VSCode extensions
158 |
159 | .vscode-test
160 |
161 | # yarn v2
162 |
163 | .yarn/cache
164 | .yarn/unplugged
165 | .yarn/build-state.yml
166 | .yarn/install-state.gz
167 | .pnp.\*
168 |
169 | # wrangler project
170 |
171 | .dev.vars
172 | .wrangler/
173 | .vscode
174 | .git
175 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | # Ignore artifacts:
2 | build
3 | coverage
4 | dist
5 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "trailingComma": "all",
3 | "tabWidth": 2,
4 | "useTabs": false,
5 | "arrowParens": "always",
6 | "semi": false,
7 | "singleQuote": true,
8 | "bracketSameLine": false,
9 | "printWidth": 120
10 | }
11 |
--------------------------------------------------------------------------------
/CHANGELOG.md:
--------------------------------------------------------------------------------
1 | # notehost
2 |
3 | ## 1.0.6
4 |
5 | ### Patch Changes
6 |
7 | - d064689: fix output permissions for bash script
8 |
9 | ## 1.0.5
10 |
11 | ### Patch Changes
12 |
13 | - 78fc22d: Working cli UI
14 | - 78fc22d: Updated readme, prepared types for cli (which is not implemented yet)
15 |
16 | ## 1.0.2
17 |
18 | ### Patch Changes
19 |
20 | - c4c611e: Testing changeset
21 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2024, Vels (Itzhak Lobak), Inc. All rights reserved.
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy
6 | of this software and associated documentation files (the "Software"), to deal
7 | in the Software without restriction, including without limitation the rights
8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9 | copies of the Software, and to permit persons to whom the Software is
10 | furnished to do so, subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21 | SOFTWARE.
22 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # NoteHost: Free Hosting for Notion Sites!
2 |
3 | ## Using:
4 |
5 | - Cloudflare DNS
6 | - Cloudflare workers
7 | - Reverse proxy implementation
8 | - TypeScript
9 |
10 | ## Supports:
11 |
12 | - Custom meta tags
13 | - Page slugs
14 | - Dark mode toggle
15 | - Custom JS for head and body
16 | - Custom fonts (using [Google Fonts](https://fonts.google.com/))
17 | - Subdomain redirect (e.g. www)
18 |
19 |
20 |
21 | ## How to use:
22 |
23 | ### Setup your Cloudflare account
24 |
25 | ---
26 |
27 | 1. Add your domain to Cloudflare. Make sure that DNS doesn't have `A` records for your domain and no `CNAME` alias for `www`
28 | 2. Create a new worker on Cloudflare and give it a meaningful name, e.g. `yourdomain-com-notion-proxy`
29 | 3. Keep the default example worker code, we will overwrite it anyway during deploy (see below)
30 |
31 | > [!TIP]
32 | > A bit outdated but detailed description on how to add your domain to Cloudflare and create a worker is [here](https://stephenou.notion.site/stephenou/Fruition-Free-Open-Source-Toolkit-for-Building-Websites-with-Notion-771ef38657244c27b9389734a9cbff44).
33 | >
34 | > Search for "Step 1: Set up your Cloudflare account".
35 | >
36 | > If someone wishes to create an up-to-date tutorial for NoteHost, please submit a pull request 😉
37 |
38 |
39 |
40 | ### Generate your NoteHost worker
41 |
42 | ---
43 |
44 | Go into your working directory and run:
45 |
46 | ```sh
47 | npx notehost init
48 | ```
49 |
50 | Follow the prompts to confirm your domain name and enter the requested information. You can change these settings later via the configuration file.
51 |
52 | NoteHost will create a directory with the name of your domain. In this directory you will see the following files:
53 |
54 | ```
55 | .
56 | ├── build-page-script-js-string.sh helper script, details below
57 | ├── package.json test & deploy your website, see realtime logs
58 | ├── tsconfig.json types config
59 | ├── wrangler.toml your Cloudflare worker config
60 | └── src
61 | ├── _page-script-js-string.ts generated by helper script
62 | ├── index.ts runs reverse proxy
63 | ├── page-script.js your custom JS page script
64 | └── site-config.ts your domain and website config
65 | ```
66 |
67 | Go into this directory and run
68 |
69 | ```sh
70 | npm install
71 | ```
72 |
73 |
74 |
75 | ### Configure your domain
76 |
77 | ---
78 |
79 | Make sure that wrangler is authenticated with your Cloudflare account
80 |
81 | ```sh
82 | npx wrangler login
83 | ```
84 |
85 | 1. Edit `wrangler.toml` and make sure that the `name` field matches your worker name in Cloudflare
86 | 2. Edit `site-config.ts` and set all the necessary options: domain, metadata, slugs, subdomain redirects, etc. All settings should be self explanatory, I hope 😊
87 |
88 | ```ts filename="src/site-config.ts"
89 | import { NoteHostSiteConfig, googleTag } from 'notehost'
90 | import { PAGE_SCRIPT_JS_STRING } from './_page-script-js-string'
91 |
92 | // Set this to your Google Tag ID from Google Analytics
93 | const GOOGLE_TAG_ID = ''
94 |
95 | export const SITE_CONFIG: NoteHostSiteConfig = {
96 | domain: 'yourdomain.com',
97 |
98 | // Metatags, optional
99 | // For main page link preview
100 | siteName: 'My Notion Website',
101 | siteDescription: 'Build your own website with Notion. This is a demo site.',
102 | siteImage: 'https://imagehosting.com/images/preview.jpg',
103 |
104 | // URL to custom favicon.ico
105 | siteIcon: 'https://imagehosting.com/images/favicon.ico',
106 |
107 | // Social media links, optional
108 | twitterHandle: '@mytwitter',
109 |
110 | // Additional safety: avoid serving extraneous Notion content from your website
111 | // Use the value from your Notion settings => Workspace => Settings => Domain
112 | notionDomain: 'mydomain',
113 |
114 | // Map slugs (short page names) to Notion page IDs
115 | // Empty slug is your main page
116 | slugToPage: {
117 | '': 'NOTION_PAGE_ID',
118 | about: 'NOTION_PAGE_ID',
119 | contact: 'NOTION_PAGE_ID',
120 | // Hint: you can use '/' in slug name to create subpages
121 | 'about/people': 'NOTION_PAGE_ID',
122 | },
123 |
124 | // Rewrite meta tags for specific pages
125 | // Use the Notion page ID as the key
126 | pageMetadata: {
127 | 'NOTION_PAGE_ID': {
128 | title: 'My Custom Page Title',
129 | description: 'My custom page description',
130 | image: 'https://imagehosting.com/images/page_preview.jpg',
131 | author: 'My Name',
132 | },
133 | },
134 |
135 | // Subdomain redirects are optional
136 | // But it is recommended to have one for www
137 | subDomains: {
138 | www: {
139 | redirect: 'https://yourdomain.com',
140 | },
141 | },
142 |
143 | // The 404 (not found) page is optional
144 | // If you don't have one, the default 404 page will be used
145 | fof: {
146 | page: 'NOTION_PAGE_ID',
147 | slug: '404', // default
148 | },
149 |
150 | // Google Font name, you can choose from https://fonts.google.com
151 | googleFont: 'Roboto',
152 |
153 | // Custom CSS/JS for head and body of a Notion page
154 | customHeadCSS: `
155 | .notion-topbar {
156 | background: lightblue
157 | }`,
158 | customHeadJS: googleTag(GOOGLE_TAG_ID),
159 | customBodyJS: PAGE_SCRIPT_JS_STRING,
160 | }
161 | ```
162 |
163 |
164 |
165 | ### Deploy your website
166 |
167 | ---
168 |
169 | ```sh
170 | npm run deploy
171 | ```
172 |
173 | 🎉 Enjoy your Notion website on your own domain! 🎉
174 |
175 | > [!IMPORTANT]
176 | > You need to run deploy every time you update `page-script.js` or `site-config.ts`.
177 |
178 |
179 |
180 | ### What is build-page-script-js-string.sh?
181 |
182 | ---
183 |
184 | The file `src/page-script.js` contains an example of a page script that you can run on your Notion pages.
185 | This example script removes tooltips from images and hides optional properties in database cards.
186 |
187 | 🔥 This script is run in the web browser! 🔥
188 |
189 | You can use `document`, `window` and all the functionality of a web browser to control the contents and behavior of your Notion pages.
190 | Also, because this is a JS file, you can edit it in your code editor with syntax highlighting and intellisense!
191 |
192 | To incorporate this script into a Notion page, NoteHost must transform the file's contents into a string. Consequently, the `build-page-script-js-string.sh` script is executed whenever you run `npm run deploy`.
193 |
194 | So just add your JS magic to `page-script.js`, run deploy and everything else will happen automagically 😎
195 |
196 |
197 |
198 | ### Logs
199 |
200 | ---
201 |
202 | You can see realtime logs from your website by running
203 |
204 | ```sh
205 | npm run logs
206 | ```
207 |
208 |
209 |
210 | ### Demo
211 |
212 | ---
213 |
214 | https://www.velsa.net
215 |
216 |
217 |
218 | ### Acknowledgments
219 |
220 | ---
221 |
222 | Based on [Fruition](https://fruitionsite.com), which is no longer maintained 😕
223 |
224 | Lots of thanks to [@DudeThatsErin](https://github.com/DudeThatsErin) and her [code snippet](https://github.com/stephenou/fruitionsite/issues/258#issue-1929516345).
225 |
--------------------------------------------------------------------------------
/assets/toggle_moon.svg:
--------------------------------------------------------------------------------
1 |
4 |
--------------------------------------------------------------------------------
/assets/toggle_sun.svg:
--------------------------------------------------------------------------------
1 |
19 |
--------------------------------------------------------------------------------
/build-body-js-string.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | OUT_FILE=./src/rewriters/_body-js-string.ts
3 | echo 'export const BODY_JS_STRING = `' >$OUT_FILE
4 | cat ./src/rewriters/body.js >>$OUT_FILE
5 | echo '`' >>$OUT_FILE
6 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "notehost",
3 | "version": "1.0.33",
4 | "description": "NoteHost is a free and powerful Notion hosting service.",
5 | "repository": "https://github.com/velsa/notehost",
6 | "homepage": "https://github.com/velsa/notehost",
7 | "keywords": [
8 | "Notion",
9 | "Cloudflare",
10 | "Workers",
11 | "Hosting"
12 | ],
13 | "author": {
14 | "name": "Vels (Itzhak Lobak)",
15 | "email": "velshome@gmail.com"
16 | },
17 | "license": "MIT",
18 | "publishConfig": {
19 | "access": "public"
20 | },
21 | "type": "module",
22 | "main": "dist/index.js",
23 | "module": "dist/index.cjs",
24 | "types": "dist/index.d.ts",
25 | "bin": {
26 | "init": "./dist/cli/index.js"
27 | },
28 | "scripts": {
29 | "prebuild": "./build-body-js-string.sh",
30 | "dev:cli": "npx tsx --inspect src/cli/index.ts init test.com",
31 | "build": "npm run prebuild && tsup src/index.ts --format cjs,esm --dts-resolve --clean --sourcemap --out-dir dist && npm run build:cli",
32 | "build:cli": "tsup src/cli/index.ts --format esm --clean --out-dir dist/cli",
33 | "release": "npm run build && changeset publish",
34 | "lint": "echo Fix lint!"
35 | },
36 | "dependencies": {
37 | "@inquirer/prompts": "^3.3.2",
38 | "chalk": "^5.3.0",
39 | "change-case-all": "^2.1.0",
40 | "commander": "^11.1.0",
41 | "ejs": "^3.1.9",
42 | "htmlrewriter": "^0.0.7",
43 | "is-valid-domain": "^0.1.6"
44 | },
45 | "devDependencies": {
46 | "@changesets/cli": "^2.27.1",
47 | "@cloudflare/workers-types": "^4.20231218.0",
48 | "@stylistic/eslint-plugin": "^1.5.4",
49 | "@types/node": "^20.11.6",
50 | "@typescript-eslint/eslint-plugin": "^6.19.0",
51 | "@typescript-eslint/parser": "^6.19.0",
52 | "eslint-plugin-import": "^2.29.1",
53 | "prettier": "3.2.2",
54 | "tsc": "^2.0.4",
55 | "tsup": "^8.0.1",
56 | "typescript": "^5.3.3",
57 | "wrangler": "^3.0.0"
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/append-js-pf.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable @typescript-eslint/no-explicit-any */
2 | import { HTMLRewriter } from 'htmlrewriter'
3 | import { NoteHostSiteConfigFull } from '.'
4 | import { BodyRewriter, HeadRewriter, MetaRewriter } from './rewriters'
5 |
6 | export async function appendJavascriptPolyfill(res: Response, url: URL, config: NoteHostSiteConfigFull) {
7 | // eslint-disable-next-line no-undef
8 | return new HTMLRewriter()
9 | .on('title', new MetaRewriter(config, url) as any)
10 | .on('meta', new MetaRewriter(config, url) as any)
11 | .on('head', new HeadRewriter(config) as any)
12 | .on('body', new BodyRewriter(config) as any)
13 | .transform(res)
14 | }
15 |
--------------------------------------------------------------------------------
/src/append-js.ts:
--------------------------------------------------------------------------------
1 | import { MetaRewriter, HeadRewriter, BodyRewriter } from './rewriters'
2 | import { NoteHostSiteConfigFull } from './types'
3 |
4 | export async function appendJavascript(res: Response, url: URL, config: NoteHostSiteConfigFull) {
5 | // eslint-disable-next-line no-undef
6 | return new HTMLRewriter()
7 | .on('title', new MetaRewriter(config, url))
8 | .on('meta', new MetaRewriter(config, url))
9 | .on('head', new HeadRewriter(config))
10 | .on('body', new BodyRewriter(config))
11 | .transform(res)
12 | }
13 |
--------------------------------------------------------------------------------
/src/cli/copy-files.ts:
--------------------------------------------------------------------------------
1 | import * as ejs from 'ejs'
2 | import fs from 'fs'
3 | import path from 'path'
4 | import { ParserConfig } from './parser-config'
5 |
6 | interface CopyFilesToSDKParams {
7 | parserConfig: ParserConfig
8 | originDir: string
9 | sdkDir: string
10 | }
11 |
12 | export async function copyFilesToSDK({ parserConfig, originDir, sdkDir }: CopyFilesToSDKParams) {
13 | const files = fs.readdirSync(originDir)
14 |
15 | fs.mkdirSync(sdkDir, { recursive: true })
16 |
17 | files.forEach((file) => {
18 | const destFile = file === '_gitignore' ? '.gitignore' : file
19 | const filePath = path.join(originDir, file)
20 |
21 | if (isDirectory(filePath)) {
22 | copyFilesToSDK({
23 | parserConfig,
24 | originDir: path.join(originDir, file),
25 | sdkDir: path.join(sdkDir, file),
26 | })
27 | } else {
28 | try {
29 | const templateFile = fs.readFileSync(filePath, 'utf8')
30 | const renderedFile = ejs.render(templateFile, parserConfig)
31 | const sdkFilePath = path.join(sdkDir, destFile)
32 | const fileExt = path.extname(sdkFilePath)
33 |
34 | fs.writeFileSync(sdkFilePath, renderedFile, {
35 | mode: fileExt === '.sh' ? 0o755 : 0o644,
36 | })
37 | } catch (e) {
38 | console.error('Error generating SDK file: ', filePath)
39 | console.error(e)
40 | process.exit(1)
41 | }
42 | }
43 | })
44 | }
45 |
46 | function isDirectory(path: string) {
47 | try {
48 | return fs.lstatSync(path).isDirectory()
49 | } catch (e) {
50 | return false
51 | }
52 | }
53 |
--------------------------------------------------------------------------------
/src/cli/index.ts:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env node
2 | import { program } from 'commander'
3 | import { version } from '../../package.json'
4 | import { initRepo } from './init-repo'
5 |
6 | program
7 | .name('notehost')
8 | .description('NoteHost CLI: Deploy and manage Notion websites via Cloudflare workers.')
9 | .version(version)
10 |
11 | program
12 | .command('init')
13 | .description('Initialize a new NoteHost worker repo')
14 | .argument('', 'domain name')
15 | .action(initRepo)
16 |
17 | program.parse()
18 |
--------------------------------------------------------------------------------
/src/cli/init-repo.ts:
--------------------------------------------------------------------------------
1 | import { confirm, select } from '@inquirer/prompts'
2 | import chalk from 'chalk'
3 | import fs from 'fs'
4 | import path from 'path'
5 | import { copyFilesToSDK } from './copy-files'
6 | import { getParserConfig } from './parser-config'
7 |
8 | export async function initRepo(domain) {
9 | const sdkDir = path.join(process.cwd(), domain)
10 | const originDir = buildOriginDir(process.argv[1])
11 | const parserConfig = await getParserConfig(domain)
12 | const templates = fs.readdirSync(path.join(originDir, 'templates'))
13 | const template =
14 | templates.length > 1
15 | ? await select({
16 | message: 'Generate from template:',
17 | choices: templates.map((t) => ({ name: t, value: t })),
18 | })
19 | : templates[0]
20 |
21 | console.log(`\n🎬 Ready to generate NoteHost worker in: ${sdkDir}`)
22 | await confirm({ message: 'Continue?', default: true })
23 |
24 | console.log('Generating...')
25 |
26 | copyFilesToSDK({
27 | parserConfig,
28 | originDir: path.join(originDir, 'templates', template),
29 | sdkDir,
30 | })
31 |
32 | console.log(`\n🎉 Done! Your worker is in`, sdkDir)
33 | console.log(`\nGo into this directory and run ${chalk.bold('npm install')}`)
34 | console.log(`Edit ${chalk.bold('src/site-config.ts')} to setup your website.`)
35 | console.log(`Review ${chalk.bold('wrangler.toml')} and make sure your worker name is correct.`)
36 | console.log(`And finally run ${chalk.bold('npm run deploy')} to publish your website.`)
37 |
38 | process.exit(0)
39 | }
40 |
41 | function buildOriginDir(appPath: string) {
42 | const runDir = path.parse(appPath).dir
43 | const parts = runDir.split(path.sep)
44 | const last = parts[parts.length - 1]
45 | const beforeLast = parts[parts.length - 2]
46 |
47 | // console.error('appPath', appPath)
48 | // console.error('runDir', runDir)
49 | // console.error('parts', parts)
50 |
51 | // running locally
52 | if (process.env.NOTEHOST_CLI_DEBUG) {
53 | return path.join(runDir, '..', '..')
54 | }
55 |
56 | if (last === '.bin' && beforeLast === 'node_modules') {
57 | // npx (installed)
58 | return path.join(runDir, '..', 'notehost')
59 | }
60 |
61 | if (last === 'cli' && beforeLast === 'dist') {
62 | // npx (tmp)
63 | return path.join(runDir, '..', '..')
64 | }
65 |
66 | if (beforeLast === 'notehost') {
67 | // pnpx
68 | return path.join(runDir, '..')
69 | }
70 |
71 | // npx
72 | return path.join(runDir, '..', 'notehost')
73 | }
74 |
--------------------------------------------------------------------------------
/src/cli/parser-config.ts:
--------------------------------------------------------------------------------
1 | import * as changeCase from 'change-case-all'
2 | import { version } from '../../package.json'
3 | import { SiteConfig, getSiteConfigFromUser } from './site-config'
4 |
5 | export interface ParserConfig extends SiteConfig {
6 | packageJsonName: string
7 | wranglerWorkerName: string
8 |
9 | notehostVersion: string
10 | }
11 |
12 | export async function getParserConfig(domainName: string | undefined): Promise {
13 | const { mainPageId, siteName, siteDescription, siteImage } = await getSiteConfigFromUser(domainName)
14 | const kebabDomain = changeCase.kebabCase(domainName)
15 |
16 | return {
17 | domainName,
18 | mainPageId,
19 | siteName,
20 | siteDescription,
21 | siteImage,
22 | packageJsonName: kebabDomain,
23 | wranglerWorkerName: `${kebabDomain}-notion-proxy`,
24 | notehostVersion: version,
25 | }
26 | }
27 |
--------------------------------------------------------------------------------
/src/cli/site-config.ts:
--------------------------------------------------------------------------------
1 | import { input } from '@inquirer/prompts'
2 | import { validDomainName } from './validators'
3 |
4 | export interface SiteConfig {
5 | domainName: string
6 | mainPageId: string
7 | siteName: string
8 | siteDescription: string
9 | siteImage: string
10 | }
11 |
12 | export async function getSiteConfigFromUser(domain: string): Promise {
13 | const siteConfig = { domainName: domain } as SiteConfig
14 |
15 | do {
16 | siteConfig.domainName = await input({
17 | message: 'Please confirm the domain name:',
18 | default: siteConfig.domainName,
19 | })
20 |
21 | if (!validDomainName(siteConfig.domainName)) {
22 | console.error('Invalid domain name:', siteConfig.domainName)
23 | }
24 | } while (!validDomainName(siteConfig.domainName))
25 |
26 | console.log(
27 | '\n🏷️ Metadata details.' +
28 | '\nYou can skip those fields now (press Enter) and fill them later via the config file.\n',
29 | )
30 |
31 | siteConfig.mainPageId = await input({
32 | message: 'ID of your Notion Page:',
33 | })
34 | siteConfig.siteName = await input({ message: 'Your site name:' })
35 | siteConfig.siteDescription = await input({
36 | message: 'Your site description:',
37 | })
38 | siteConfig.siteImage = await input({
39 | message: 'Link preview image URL:',
40 | })
41 |
42 | return siteConfig
43 | }
44 |
--------------------------------------------------------------------------------
/src/cli/validators.ts:
--------------------------------------------------------------------------------
1 | import isValidDomain from 'is-valid-domain'
2 |
3 | export function validDomainName(domain: string) {
4 | return (
5 | domain &&
6 | domain.length > 0 &&
7 | isValidDomain(domain, {
8 | wildcard: false,
9 | })
10 | )
11 | }
12 |
--------------------------------------------------------------------------------
/src/config-helpers.ts:
--------------------------------------------------------------------------------
1 | export function googleTag(googleTagId: string) {
2 | return `
3 |
4 |
5 |
12 | `
13 | }
14 |
--------------------------------------------------------------------------------
/src/handlers/handle-api.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | export async function handleApi(url: URL, request: Request) {
3 | // Forward API
4 | const response = await fetch(url.toString(), {
5 | body: url.pathname.startsWith('/api/v3/getPublicPageData') ? null : request.body,
6 | headers: {
7 | 'content-type': 'application/json;charset=UTF-8',
8 | 'user-agent':
9 | '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',
10 | },
11 | method: 'POST',
12 | })
13 | const ret = new Response(response.body as BodyInit, response)
14 |
15 | ret.headers.set('Access-Control-Allow-Origin', '*')
16 |
17 | return ret
18 | }
19 |
--------------------------------------------------------------------------------
/src/handlers/handle-app-js.ts:
--------------------------------------------------------------------------------
1 | import { NoteHostSiteConfigFull } from '../types'
2 |
3 | export async function handleAppJs(url: URL, siteConfig: NoteHostSiteConfigFull) {
4 | const { domain } = siteConfig
5 | const response = await fetch(url.toString())
6 | const body = await response.text()
7 | const siteRegex = new RegExp(
8 | siteConfig.notionDomain ? `${siteConfig.notionDomain}.notion.site` : 'www.notion.so',
9 | 'g',
10 | )
11 | const ret = new Response(body.replace(siteRegex, domain).replace(/notion.so/g, domain), response)
12 |
13 | ret.headers.set('Content-Type', 'application/x-javascript')
14 |
15 | return ret
16 | }
17 |
--------------------------------------------------------------------------------
/src/handlers/handle-favicon.ts:
--------------------------------------------------------------------------------
1 | export async function handleFavicon(url: URL, siteIcon: string) {
2 | const response = await fetch(siteIcon)
3 | const body = await response.arrayBuffer()
4 | const ret = new Response(body, response)
5 |
6 | return ret
7 | }
8 |
--------------------------------------------------------------------------------
/src/handlers/handle-js.ts:
--------------------------------------------------------------------------------
1 | export async function handleJs(url: URL) {
2 | const response = await fetch(url.toString())
3 | const body = await response.text()
4 | const ret = new Response(body, response)
5 |
6 | ret.headers.set('Content-Type', 'application/x-javascript')
7 |
8 | return ret
9 | }
10 |
--------------------------------------------------------------------------------
/src/handlers/handle-options.ts:
--------------------------------------------------------------------------------
1 | const corsHeaders = {
2 | 'Access-Control-Allow-Origin': '*',
3 | 'Access-Control-Allow-Methods': 'GET, HEAD, POST, PUT, OPTIONS',
4 | 'Access-Control-Allow-Headers': 'Content-Type',
5 | }
6 |
7 | export function handleOptions(request: Request) {
8 | if (
9 | request.headers.get('Origin') !== null &&
10 | request.headers.get('Access-Control-Request-Method') !== null &&
11 | request.headers.get('Access-Control-Request-Headers') !== null
12 | ) {
13 | // Handle CORS pre-flight request.
14 | return new Response(null, {
15 | headers: corsHeaders,
16 | })
17 | }
18 |
19 | // Handle standard OPTIONS request.
20 | return new Response(null, {
21 | headers: {
22 | Allow: 'GET, HEAD, POST, PUT, OPTIONS',
23 | },
24 | })
25 | }
26 |
--------------------------------------------------------------------------------
/src/handlers/handle-other.ts:
--------------------------------------------------------------------------------
1 | export async function handleNotionAsset(url: URL) {
2 | const response = await fetch(url.toString())
3 | const body = await response.arrayBuffer()
4 | const ret = new Response(body, response)
5 |
6 | return ret
7 | }
8 |
--------------------------------------------------------------------------------
/src/handlers/handle-sitemap.ts:
--------------------------------------------------------------------------------
1 | import { NoteHostSiteConfigFull } from '../types'
2 |
3 | export function handleSitemap(request: Request, siteConfig: NoteHostSiteConfigFull) {
4 | const { domain, slugs } = siteConfig
5 | let sitemap = ''
6 |
7 | slugs.forEach((slug) => {
8 | sitemap += `https://${domain}/${slug}`
9 | })
10 | sitemap += ''
11 |
12 | const response = new Response(sitemap)
13 |
14 | response.headers.set('content-type', 'application/xml')
15 |
16 | return response
17 | }
18 |
--------------------------------------------------------------------------------
/src/handlers/index.ts:
--------------------------------------------------------------------------------
1 | export * from './handle-api'
2 | export * from './handle-app-js'
3 | export * from './handle-js'
4 | export * from './handle-options'
5 | export * from './handle-sitemap'
6 |
--------------------------------------------------------------------------------
/src/index.ts:
--------------------------------------------------------------------------------
1 | export * from './config-helpers'
2 | export * from './reverse-proxy'
3 | export { initializeReverseProxy } from './reverse-proxy-init'
4 | export * from './types'
5 |
--------------------------------------------------------------------------------
/src/reverse-proxy-init.ts:
--------------------------------------------------------------------------------
1 | import { NoteHostSiteConfig, NoteHostSiteConfigFull } from './types'
2 |
3 | export let siteConfig: NoteHostSiteConfigFull = {} as NoteHostSiteConfigFull
4 |
5 | export function initializeReverseProxy(siteConfigUser: NoteHostSiteConfig) {
6 | siteConfig = {
7 | ...siteConfigUser,
8 | slugs: [],
9 | pages: [],
10 | pageToSlug: {},
11 | }
12 |
13 | siteConfig.pageMetadata = siteConfig.pageMetadata || {}
14 |
15 | siteConfig.fof = {
16 | page: siteConfig.fof?.page,
17 | slug: siteConfig.fof?.slug || '404',
18 | }
19 |
20 | if (siteConfig.fof.page?.length) {
21 | siteConfig.slugToPage[siteConfig.fof.slug] = siteConfig.fof.page
22 | }
23 |
24 | // Build helper indexes for worker and for the client (body.js)
25 | Object.keys(siteConfig.slugToPage).forEach((slug) => {
26 | const pageId = siteConfig.slugToPage[slug]
27 |
28 | siteConfig.slugs.push(slug)
29 | siteConfig.pages.push(pageId)
30 | siteConfig.pageToSlug[pageId] = slug
31 | })
32 | }
33 |
--------------------------------------------------------------------------------
/src/reverse-proxy.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-undef */
2 | import { appendJavascript } from './append-js'
3 | import { appendJavascriptPolyfill } from './append-js-pf'
4 | import { handleFavicon } from './handlers/handle-favicon'
5 | import { handleNotionAsset } from './handlers/handle-other'
6 | import { handleApi, handleAppJs, handleJs, handleOptions, handleSitemap } from './handlers/index'
7 | import { siteConfig } from './reverse-proxy-init'
8 |
9 | export async function reverseProxy(
10 | request: Request,
11 | opts: {
12 | cloudflare: boolean
13 | } = { cloudflare: true },
14 | ) {
15 | const { cloudflare } = opts
16 |
17 | if (!siteConfig) {
18 | throw new Error('Site config is not initialized. Please call initializeReverseProxy() first.')
19 | }
20 |
21 | const { domain, slugToPage, siteIcon } = siteConfig
22 |
23 | if (request.method === 'OPTIONS') {
24 | return handleOptions(request)
25 | }
26 |
27 | const url = new URL(request.url)
28 | const subDomain = url.hostname.split('.')[0]
29 |
30 | if (url.hostname === domain) {
31 | url.hostname = siteConfig.notionDomain ? `${siteConfig.notionDomain}.notion.site` : 'www.notion.so'
32 |
33 | // Handle special Notion routes
34 | if (url.pathname === '/robots.txt') {
35 | return new Response(`Sitemap: https://${domain}/sitemap.xml`)
36 | }
37 |
38 | if (url.pathname === '/sitemap.xml') {
39 | return handleSitemap(request, siteConfig)
40 | }
41 |
42 | if (url.pathname.startsWith('/app') && url.pathname.endsWith('js')) {
43 | return handleAppJs(url, siteConfig)
44 | }
45 |
46 | if (url.pathname.startsWith('/api')) {
47 | return handleApi(url, request)
48 | }
49 |
50 | if (url.pathname.endsWith('.js')) {
51 | return handleJs(url)
52 | }
53 |
54 | if (url.pathname.endsWith('favicon.ico') && siteIcon) {
55 | return handleFavicon(url, siteIcon)
56 | }
57 |
58 | if (
59 | url.pathname.startsWith('/_assets') ||
60 | url.pathname.startsWith('/image') ||
61 | url.pathname.startsWith('/f/refresh') ||
62 | url.pathname.match(/\.[a-zA-Z]{2,4}$/)
63 | ) {
64 | return handleNotionAsset(url)
65 | }
66 |
67 | // Handle slugs, from site-config and from KV
68 | console.log('url.pathname', url.pathname)
69 |
70 | const slug = url.pathname.split('/').pop()
71 | const slugHash = url.pathname.slice(-32)
72 | const page = slugToPage[slug]
73 |
74 | if (page) {
75 | console.log(`Redirecting ${slug} to https://${domain}/${page}`)
76 |
77 | return Response.redirect(`https://${domain}/${page}`, 301)
78 | } else if (slugHash && slugHash !== slug && slugHash.length === 32) {
79 | console.log(`Redirecting ${slug} to https://${domain}/${slugHash}`)
80 |
81 | return Response.redirect(`https://${domain}/${slugHash}`, 301)
82 | } else if (slug && slug.length !== 32) {
83 | if (siteConfig.fof?.page?.length) {
84 | return Response.redirect(`https://${domain}/${siteConfig.fof.page}`, 301)
85 | } else {
86 | console.error('!! Page Not found (404)', url.pathname)
87 |
88 | return new Response('NoteHost: Page Not found (404).', { status: 404 })
89 | }
90 | }
91 | } else if (subDomain && siteConfig.subDomains) {
92 | const sub = siteConfig.subDomains[subDomain]
93 |
94 | if (sub) {
95 | // console.log(`Redirecting ${url.hostname} to ${sub.redirect}`)
96 |
97 | return Response.redirect(sub.redirect, 301)
98 | }
99 | }
100 |
101 | const response = await fetch(url.toString(), {
102 | body: request.body,
103 | headers: request.headers,
104 | method: request.method,
105 | })
106 | const ret = new Response(response.body as BodyInit, response)
107 |
108 | ret.headers.delete('Content-Security-Policy')
109 | ret.headers.delete('X-Content-Security-Policy')
110 |
111 | return cloudflare ? appendJavascript(ret, url, siteConfig) : appendJavascriptPolyfill(ret, url, siteConfig)
112 | }
113 |
--------------------------------------------------------------------------------
/src/rewriters/_body-js-string.ts:
--------------------------------------------------------------------------------
1 | export const BODY_JS_STRING = `
2 | /* eslint-disable no-unused-vars */
3 | /* eslint-disable prefer-rest-params */
4 | /* eslint-disable no-lonely-if */
5 | /* eslint-disable no-restricted-globals */
6 | /* eslint-disable no-underscore-dangle */
7 | /* eslint-disable no-undef */
8 | localStorage.__console = true
9 |
10 | const el = document.createElement('div')
11 | let redirected = false
12 |
13 | function getPage() {
14 | return location.pathname.slice(-32)
15 | }
16 |
17 | function getSlug() {
18 | return location.pathname.slice(1)
19 | }
20 |
21 | function updateSlug() {
22 | const slug = PAGE_TO_SLUG[getPage()]
23 |
24 | if (slug != null) {
25 | history.replaceState(history.state, '', ['/', slug].join(''))
26 | }
27 | }
28 |
29 | function enableConsoleEffectAndSetMode(mode) {
30 | if (__console && !__console.isEnabled) {
31 | __console.enable()
32 | window.location.reload()
33 | } else {
34 | __console?.environment?.ThemeStore?.setState({ mode })
35 | localStorage.setItem('newTheme', JSON.stringify({ mode }))
36 | }
37 | }
38 |
39 | function onDark() {
40 | el.innerHTML =
41 | ''
42 | document.body.classList.add('dark')
43 | enableConsoleEffectAndSetMode('dark')
44 | }
45 |
46 | function onLight() {
47 | el.innerHTML =
48 | ''
49 | document.body.classList.remove('dark')
50 | enableConsoleEffectAndSetMode('light')
51 | }
52 |
53 | function toggle() {
54 | if (document.body.classList.contains('dark')) {
55 | onLight()
56 | } else {
57 | onDark()
58 | }
59 | }
60 |
61 | function addDarkModeButton(device) {
62 | const nav =
63 | device === 'web'
64 | ? document.querySelector('.notion-topbar').firstChild
65 | : document.querySelector('.notion-topbar-mobile')
66 |
67 | el.className = 'toggle-mode'
68 | el.addEventListener('click', toggle)
69 |
70 | const timeout = device === 'web' ? 0 : 500
71 |
72 | setTimeout(() => {
73 | nav.appendChild(el)
74 | }, timeout)
75 |
76 | // get the current theme and add the toggle to represent that theme
77 | const currentTheme = JSON.parse(localStorage.getItem('newTheme'))?.mode
78 |
79 | if (currentTheme) {
80 | if (currentTheme === 'dark') {
81 | onDark()
82 | } else {
83 | onLight()
84 | }
85 | } else {
86 | // enable smart dark mode based on user-preference
87 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
88 | onDark()
89 | } else {
90 | onLight()
91 | }
92 | }
93 |
94 | // try to detect if user-preference change
95 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
96 | toggle()
97 | })
98 | }
99 |
100 | const observer = new MutationObserver(() => {
101 | if (redirected) return
102 |
103 | const nav = document.querySelector('.notion-topbar')
104 | const mobileNav = document.querySelector('.notion-topbar-mobile')
105 |
106 | if ((nav && nav.firstChild && nav.firstChild.firstChild) || (mobileNav && mobileNav.firstChild)) {
107 | // console.log('redirected', getSlug())
108 | updateSlug()
109 | redirected = true
110 |
111 | addDarkModeButton(nav ? 'web' : 'mobile')
112 |
113 | const { onpopstate } = window
114 |
115 | window.onpopstate = function () {
116 | // console.log('onpopstate');
117 | if (slugs.includes(getSlug())) {
118 | const page = SLUG_TO_PAGE[getSlug()]
119 |
120 | if (page) {
121 | // console.log('slug:', getSlug())
122 | // console.log('redirecting to:', page)
123 | history.replaceState(history.state, 'bypass', ['/', page].join(''))
124 | }
125 | }
126 |
127 | onpopstate.apply(this, [].slice.call(arguments))
128 | updateSlug()
129 | }
130 | }
131 | })
132 |
133 | observer.observe(document.querySelector('#notion-app'), {
134 | childList: true,
135 | subtree: true,
136 | })
137 |
138 | const { replaceState, back, forward } = window.history
139 |
140 | window.history.back = function () {
141 | back.apply(window.history, arguments)
142 | }
143 |
144 | window.history.forward = function () {
145 | forward.apply(window.history, arguments)
146 | }
147 |
148 | window.history.replaceState = function () {
149 | if (arguments[1] === 'bypass') {
150 | return
151 | }
152 |
153 | const slug = getSlug()
154 | const isKnownSlug = slugs.includes(slug)
155 |
156 | console.log('replaceState:', { slug, isKnownSlug, arguments })
157 |
158 | // console.log('replaceState arguments:', arguments)
159 | // console.log('replaceState state:', state)
160 |
161 | if (arguments[2] === '/login') {
162 | const page = SLUG_TO_PAGE[slug]
163 |
164 | if (page) {
165 | // console.log('slug:', slug)
166 | // console.log('redirecting to:', page)
167 | arguments[2] = ['/', page].join('')
168 | replaceState.apply(window.history, arguments)
169 | window.location.reload()
170 |
171 | return
172 | }
173 | } else {
174 | if (isKnownSlug && arguments[2] !== ['/', slug].join('')) {
175 | return
176 | }
177 | }
178 |
179 | replaceState.apply(window.history, arguments)
180 | }
181 |
182 | const { pushState } = window.history
183 |
184 | window.history.pushState = function () {
185 | const dest = new URL(location.protocol + location.host + arguments[2])
186 | const id = dest.pathname.slice(-32)
187 |
188 | // console.log('pushState state:', state)
189 | // console.log('pushState id:', id)
190 | if (pages.includes(id)) {
191 | arguments[2] = ['/', PAGE_TO_SLUG[id]].join('')
192 | }
193 |
194 | return pushState.apply(window.history, arguments)
195 | }
196 |
197 | const { open } = window.XMLHttpRequest.prototype
198 |
199 | window.XMLHttpRequest.prototype.open = function () {
200 | arguments[1] = arguments[1].replace(domain, notionDomain)
201 |
202 | if (arguments[1].indexOf('msgstore.' + notionDomain) > -1) {
203 | return
204 | }
205 |
206 | // console.log('XMLHttpRequest.open arguments:', arguments)
207 | open.apply(this, [].slice.call(arguments))
208 | }
209 | `
210 |
--------------------------------------------------------------------------------
/src/rewriters/body-rewriter.ts:
--------------------------------------------------------------------------------
1 | import { NoteHostSiteConfigFull } from '../types'
2 | import { BODY_JS_STRING } from './_body-js-string'
3 | /* eslint-disable class-methods-use-this */
4 | export class BodyRewriter {
5 | siteConfig: NoteHostSiteConfigFull
6 |
7 | constructor(siteConfig: NoteHostSiteConfigFull) {
8 | this.siteConfig = siteConfig
9 | }
10 |
11 | element(element: Element) {
12 | const { domain, slugToPage, pageToSlug, slugs, pages, customBodyJS } = this.siteConfig
13 |
14 | element.append(
15 | `
16 |
26 | ${customBodyJS ?? ''}
27 | `,
28 | {
29 | html: true,
30 | },
31 | )
32 | }
33 | }
34 |
--------------------------------------------------------------------------------
/src/rewriters/body.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable no-unused-vars */
2 | /* eslint-disable prefer-rest-params */
3 | /* eslint-disable no-lonely-if */
4 | /* eslint-disable no-restricted-globals */
5 | /* eslint-disable no-underscore-dangle */
6 | /* eslint-disable no-undef */
7 | localStorage.__console = true
8 |
9 | const el = document.createElement('div')
10 | let redirected = false
11 |
12 | function getPage() {
13 | return location.pathname.slice(-32)
14 | }
15 |
16 | function getSlug() {
17 | return location.pathname.slice(1)
18 | }
19 |
20 | function updateSlug() {
21 | const slug = PAGE_TO_SLUG[getPage()]
22 |
23 | if (slug != null) {
24 | history.replaceState(history.state, '', ['/', slug].join(''))
25 | }
26 | }
27 |
28 | function enableConsoleEffectAndSetMode(mode) {
29 | if (__console && !__console.isEnabled) {
30 | __console.enable()
31 | window.location.reload()
32 | } else {
33 | __console?.environment?.ThemeStore?.setState({ mode })
34 | localStorage.setItem('newTheme', JSON.stringify({ mode }))
35 | }
36 | }
37 |
38 | function onDark() {
39 | el.innerHTML =
40 | ''
41 | document.body.classList.add('dark')
42 | enableConsoleEffectAndSetMode('dark')
43 | }
44 |
45 | function onLight() {
46 | el.innerHTML =
47 | ''
48 | document.body.classList.remove('dark')
49 | enableConsoleEffectAndSetMode('light')
50 | }
51 |
52 | function toggle() {
53 | if (document.body.classList.contains('dark')) {
54 | onLight()
55 | } else {
56 | onDark()
57 | }
58 | }
59 |
60 | function addDarkModeButton(device) {
61 | const nav =
62 | device === 'web'
63 | ? document.querySelector('.notion-topbar').firstChild
64 | : document.querySelector('.notion-topbar-mobile')
65 |
66 | el.className = 'toggle-mode'
67 | el.addEventListener('click', toggle)
68 |
69 | const timeout = device === 'web' ? 0 : 500
70 |
71 | setTimeout(() => {
72 | nav.appendChild(el)
73 | }, timeout)
74 |
75 | // get the current theme and add the toggle to represent that theme
76 | const currentTheme = JSON.parse(localStorage.getItem('newTheme'))?.mode
77 |
78 | if (currentTheme) {
79 | if (currentTheme === 'dark') {
80 | onDark()
81 | } else {
82 | onLight()
83 | }
84 | } else {
85 | // enable smart dark mode based on user-preference
86 | if (window.matchMedia && window.matchMedia('(prefers-color-scheme: dark)').matches) {
87 | onDark()
88 | } else {
89 | onLight()
90 | }
91 | }
92 |
93 | // try to detect if user-preference change
94 | window.matchMedia('(prefers-color-scheme: dark)').addEventListener('change', () => {
95 | toggle()
96 | })
97 | }
98 |
99 | const observer = new MutationObserver(() => {
100 | if (redirected) return
101 |
102 | const nav = document.querySelector('.notion-topbar')
103 | const mobileNav = document.querySelector('.notion-topbar-mobile')
104 |
105 | if ((nav && nav.firstChild && nav.firstChild.firstChild) || (mobileNav && mobileNav.firstChild)) {
106 | // console.log('redirected', getSlug())
107 | updateSlug()
108 | redirected = true
109 |
110 | addDarkModeButton(nav ? 'web' : 'mobile')
111 |
112 | const { onpopstate } = window
113 |
114 | window.onpopstate = function () {
115 | // console.log('onpopstate');
116 | if (slugs.includes(getSlug())) {
117 | const page = SLUG_TO_PAGE[getSlug()]
118 |
119 | if (page) {
120 | // console.log('slug:', getSlug())
121 | // console.log('redirecting to:', page)
122 | history.replaceState(history.state, 'bypass', ['/', page].join(''))
123 | }
124 | }
125 |
126 | onpopstate.apply(this, [].slice.call(arguments))
127 | updateSlug()
128 | }
129 | }
130 | })
131 |
132 | observer.observe(document.querySelector('#notion-app'), {
133 | childList: true,
134 | subtree: true,
135 | })
136 |
137 | const { replaceState, back, forward } = window.history
138 |
139 | window.history.back = function () {
140 | back.apply(window.history, arguments)
141 | }
142 |
143 | window.history.forward = function () {
144 | forward.apply(window.history, arguments)
145 | }
146 |
147 | window.history.replaceState = function () {
148 | if (arguments[1] === 'bypass') {
149 | return
150 | }
151 |
152 | const slug = getSlug()
153 | const isKnownSlug = slugs.includes(slug)
154 |
155 | console.log('replaceState:', { slug, isKnownSlug, arguments })
156 |
157 | // console.log('replaceState arguments:', arguments)
158 | // console.log('replaceState state:', state)
159 |
160 | if (arguments[2] === '/login') {
161 | const page = SLUG_TO_PAGE[slug]
162 |
163 | if (page) {
164 | // console.log('slug:', slug)
165 | // console.log('redirecting to:', page)
166 | arguments[2] = ['/', page].join('')
167 | replaceState.apply(window.history, arguments)
168 | window.location.reload()
169 |
170 | return
171 | }
172 | } else {
173 | if (isKnownSlug && arguments[2] !== ['/', slug].join('')) {
174 | return
175 | }
176 | }
177 |
178 | replaceState.apply(window.history, arguments)
179 | }
180 |
181 | const { pushState } = window.history
182 |
183 | window.history.pushState = function () {
184 | const dest = new URL(location.protocol + location.host + arguments[2])
185 | const id = dest.pathname.slice(-32)
186 |
187 | // console.log('pushState state:', state)
188 | // console.log('pushState id:', id)
189 | if (pages.includes(id)) {
190 | arguments[2] = ['/', PAGE_TO_SLUG[id]].join('')
191 | }
192 |
193 | return pushState.apply(window.history, arguments)
194 | }
195 |
196 | const { open } = window.XMLHttpRequest.prototype
197 |
198 | window.XMLHttpRequest.prototype.open = function () {
199 | arguments[1] = arguments[1].replace(domain, notionDomain)
200 |
201 | if (arguments[1].indexOf('msgstore.' + notionDomain) > -1) {
202 | return
203 | }
204 |
205 | // console.log('XMLHttpRequest.open arguments:', arguments)
206 | open.apply(this, [].slice.call(arguments))
207 | }
208 |
--------------------------------------------------------------------------------
/src/rewriters/element-handler.ts:
--------------------------------------------------------------------------------
1 | /* eslint-disable class-methods-use-this */
2 | import { NoteHostSiteConfigFull } from '../types'
3 |
4 | export interface HandleRule {
5 | attribute: string
6 | match: string
7 | action: {
8 | type: 'remove' | 'replace'
9 | value?: string
10 | }
11 | }
12 |
13 | export class ElementHandler {
14 | siteConfig: NoteHostSiteConfigFull
15 |
16 | handleRules: HandleRule[]
17 |
18 | constructor(siteConfig: NoteHostSiteConfigFull, handleRules: HandleRule[] = []) {
19 | this.siteConfig = siteConfig
20 | this.handleRules = handleRules
21 | }
22 |
23 | element(element: Element) {
24 | // console.log(`Incoming element: <${element.tagName}>${element.getAttribute('content')}${element.tagName}>`)
25 |
26 | for (const rule of this.handleRules) {
27 | const attribute = element.getAttribute(rule.attribute) ?? ''
28 |
29 | if (attribute.match(rule.match)) {
30 | switch (rule.action.type) {
31 | case 'remove':
32 | element.remove()
33 | break
34 | case 'replace':
35 | element.setAttribute(rule.attribute, rule.action.value ?? '')
36 | break
37 | default:
38 | console.error(`Unknown action type in rule: ${JSON.stringify(rule, null, 2)}`)
39 | break
40 | }
41 | }
42 | }
43 | }
44 | }
45 |
--------------------------------------------------------------------------------
/src/rewriters/head-rewriter.ts:
--------------------------------------------------------------------------------
1 | import { NoteHostSiteConfigFull } from '../types'
2 |
3 | /* eslint-disable class-methods-use-this */
4 | export class HeadRewriter {
5 | siteConfig: NoteHostSiteConfigFull
6 |
7 | constructor(siteConfig: NoteHostSiteConfigFull) {
8 | this.siteConfig = siteConfig
9 | }
10 |
11 | element(element: Element) {
12 | const { googleFont, customHeadJS, customHeadCSS } = this.siteConfig
13 |
14 | if (googleFont) {
15 | element.append(
16 | `
20 | `,
21 | {
22 | html: true,
23 | },
24 | )
25 | }
26 |
27 | element.append(
28 | `
42 | ${customHeadJS ?? ''}`,
43 | {
44 | html: true,
45 | },
46 | )
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/src/rewriters/index.ts:
--------------------------------------------------------------------------------
1 | export * from './body-rewriter'
2 | export * from './head-rewriter'
3 | export * from './meta-rewriter'
4 |
--------------------------------------------------------------------------------
/src/rewriters/meta-rewriter.ts:
--------------------------------------------------------------------------------
1 | import { NoteHostSiteConfigFull } from '../types'
2 |
3 | /* eslint-disable class-methods-use-this */
4 | export class MetaRewriter {
5 | siteConfig: NoteHostSiteConfigFull
6 |
7 | url: URL
8 |
9 | isRootPage: boolean
10 |
11 | constructor(siteConfig: NoteHostSiteConfigFull, url: URL) {
12 | this.siteConfig = siteConfig
13 | this.url = url
14 | this.isRootPage = this.siteConfig.pageToSlug[this.url.pathname.slice(1)] === ''
15 | }
16 |
17 | element(element: Element) {
18 | const { siteName, siteDescription, twitterHandle, siteImage, domain, pageToSlug, pageMetadata } = this.siteConfig
19 | const page = this.url.pathname.slice(-32)
20 | const property = element.getAttribute('property') ?? ''
21 | const name = element.getAttribute('name') ?? ''
22 | const content = element.getAttribute('content') ?? ''
23 |
24 | // console.log(
25 | // `${this.url}: <${element.tagName} name="${name}" property="${property}">${content}${element.tagName}>`,
26 | // )
27 |
28 | if (element.tagName === 'title') {
29 | const pageTitle = pageMetadata[page]?.title ?? removeNotionAds(content)
30 |
31 | element.setInnerContent(pageTitle)
32 | }
33 |
34 | if (property === 'og:title' || name === 'twitter:title') {
35 | const pageTitle = pageMetadata[page]?.title ?? removeNotionAds(content)
36 |
37 | element.setAttribute('content', pageTitle)
38 | }
39 |
40 | if (property === 'og:site_name') {
41 | element.setAttribute('content', siteName)
42 | }
43 |
44 | if (name === 'article:author') {
45 | const pageAuthor = pageMetadata[page]?.author ?? content
46 |
47 | element.setAttribute('content', pageAuthor)
48 | }
49 |
50 | if (name === 'description' || property === 'og:description' || name === 'twitter:description') {
51 | if (this.isRootPage) {
52 | element.setAttribute('content', siteDescription)
53 | } else {
54 | const pageDescription = pageMetadata[page]?.description ?? removeNotionAds(content)
55 |
56 | element.setAttribute('content', pageDescription)
57 | }
58 | }
59 |
60 | if (property === 'og:url' || name === 'twitter:url') {
61 | if (this.isRootPage) {
62 | element.setAttribute('content', `https://${domain}/`)
63 | } else if (pageToSlug[page]) {
64 | element.setAttribute('content', `https://${domain}/${pageToSlug[page]}`)
65 | } else {
66 | element.setAttribute('content', `https://${domain}/${page}`)
67 | }
68 | }
69 |
70 | if (name === 'twitter:site') {
71 | if (twitterHandle) {
72 | element.setAttribute('content', `${twitterHandle}`)
73 | } else {
74 | element.remove()
75 | }
76 | }
77 |
78 | if (property === 'og:image' || name === 'twitter:image') {
79 | if (this.isRootPage && siteImage) {
80 | // console.log(`----- Image for url '${this.url.pathname}: ${siteImage}'`)
81 | element.setAttribute('content', siteImage)
82 | } else {
83 | const pageImage = pageMetadata[page]?.image ?? content
84 | // console.log(`----- Image for url '${this.url.pathname}: ${pageImage}'`)
85 |
86 | element.setAttribute('content', pageImage)
87 | }
88 | }
89 |
90 | if (name === 'apple-itunes-app') {
91 | element.remove()
92 | }
93 | }
94 | }
95 |
96 | function removeNotionAds(text: string) {
97 | return text
98 | .replace(' | Built with Notion', '')
99 | .replace(' | Notion', '')
100 | .replace('Built with Notion, the all-in-one connected workspace with publishing capabilities.', '')
101 | }
102 |
--------------------------------------------------------------------------------
/src/types.ts:
--------------------------------------------------------------------------------
1 | export interface Env {
2 | slugs: KVNamespace
3 | }
4 |
5 | export type NoteHostSiteConfig = Omit
6 |
7 | export interface NoteHostSiteConfigFull {
8 | // Site domain, example.com
9 | domain: string
10 |
11 | // Mapping from slug to page ID
12 | slugToPage: Record
13 | notionSlugToPage?: NoteHostNotionSlugConfig
14 | pageMetadata?: Record
15 |
16 | // SEO metadata
17 | // title, og:site_name, article:author
18 | siteName: string
19 | // description, og:description, twitter:description
20 | siteDescription: string
21 | // twitter:site, twitter:creator
22 | twitterHandle?: string
23 | // og:image, twitter:image
24 | siteImage?: string
25 |
26 | // URL to custom favicon.ico
27 | siteIcon?: string
28 |
29 | // Additional safety: avoid serving extraneous Notion content from your website
30 | // Use the value from your Notion settings => Workspace => Settings => Domain
31 | notionDomain?: string
32 |
33 | // 404 Notion page to display to visitors, the default slug is '404'
34 | fof?: {
35 | page: string | undefined
36 | slug: string | undefined
37 | }
38 |
39 | subDomains?: Record
40 |
41 | // Google Font name, you can choose from https://fonts.google.com
42 | googleFont?: string
43 |
44 | // Custom CSS/JS to be injected in and
45 | customHeadCSS?: string
46 | customHeadJS?: string
47 | customBodyJS?: string
48 |
49 | // Calculated fields
50 | pageToSlug: Record
51 | slugs: Array
52 | pages: Array
53 | }
54 |
55 | export interface NoteHostSiteConfigSubDomainRedirect {
56 | redirect: string
57 | }
58 |
59 | export interface NoteHostNotionSlugConfig {
60 | // Notion database with mapping from slug to page ID
61 | // Columns:
62 | // | ------------ | ------------- |
63 | // | slug | page link |
64 | // | ------------ | ------------- |
65 | // ↑ ↑
66 | // text Link to Notion page
67 | //
68 | databaseId: string
69 |
70 | // TODO: sync this DB to firebase via notesync
71 | // and save values to slugs KV on webhook from notesync
72 | }
73 |
74 | // Page SEO metadata
75 | // Overrides site-level metadata
76 | export interface NoteHostSiteConfigPageMetadata {
77 | // , og:title and twitter:title
78 | title?: string
79 | // description, og:description and twitter:description
80 | description?: string
81 | // og:image and twitter:image
82 | image?: string
83 | // article:author
84 | author?: string
85 | }
--------------------------------------------------------------------------------
/templates/default/.editorconfig:
--------------------------------------------------------------------------------
1 | # http://editorconfig.org
2 | root = true
3 |
4 | [*]
5 | indent_style = space
6 | tab_width = 2
7 | end_of_line = lf
8 | charset = utf-8
9 | trim_trailing_whitespace = true
10 | insert_final_newline = true
11 |
12 | [*.yml]
13 | indent_style = space
14 |
--------------------------------------------------------------------------------
/templates/default/_gitignore:
--------------------------------------------------------------------------------
1 | # common
2 | .DS_Store
3 | /node_modules
4 | *-lock.*
5 | *.lock
6 | *.log
7 |
8 | /dist
9 |
10 | # wrangler project
11 |
12 | .dev.vars
13 | .wrangler/
14 | .vscode
15 | .git
16 |
--------------------------------------------------------------------------------
/templates/default/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "<%= packageJsonName %>",
3 | "version": "1.0.0",
4 | "main": "src/index.ts",
5 | "private": true,
6 | "scripts": {
7 | "prebuild": "node ./src/_build-page-script-js-string.js",
8 | "start": "npm run prebuild && npx --yes wrangler@latest dev",
9 | "deploy": "npm run prebuild && npx --yes wrangler@latest deploy",
10 | "logs": "npx --yes wrangler@latest tail"
11 | },
12 | "dependencies": {
13 | "notehost": "^<%= notehostVersion %>"
14 | },
15 | "devDependencies": {
16 | "@cloudflare/workers-types": "^4.20231218.0"
17 | }
18 | }
19 |
--------------------------------------------------------------------------------
/templates/default/src/_build-page-script-js-string.js:
--------------------------------------------------------------------------------
1 | const fs = require('fs')
2 | const path = require('path')
3 |
4 | const outFilePath = path.join(__dirname, '_page-script-js-string.ts')
5 | const pageScriptPath = path.join(__dirname, 'page-script.js')
6 |
7 | try {
8 | const pageScriptContent = fs.readFileSync(pageScriptPath, 'utf8')
9 | const escapedContent = pageScriptContent.replace(/\\/g, '\\')
10 | const finalContent = `export const PAGE_SCRIPT_JS_STRING = \`\``
11 |
12 | fs.writeFileSync(outFilePath, finalContent)
13 | console.log('Page script was built successfully!')
14 | } catch (error) {
15 | console.error('Failed to build page script:', error)
16 | }
17 |
--------------------------------------------------------------------------------
/templates/default/src/_page-script-js-string.ts:
--------------------------------------------------------------------------------
1 | export const PAGE_SCRIPT_JS_STRING = ``
--------------------------------------------------------------------------------
/templates/default/src/index.ts:
--------------------------------------------------------------------------------
1 | import { initializeReverseProxy, reverseProxy } from 'notehost'
2 | import { SITE_CONFIG } from './site-config'
3 |
4 | initializeReverseProxy(SITE_CONFIG)
5 |
6 | export default {
7 | async fetch(request: Request): Promise {
8 | return await reverseProxy(request)
9 | },
10 | }
11 |
--------------------------------------------------------------------------------
/templates/default/src/page-script.js:
--------------------------------------------------------------------------------
1 | /* eslint-disable func-names */
2 | /* eslint-disable no-return-assign */
3 | /* eslint-disable no-param-reassign */
4 |
5 | // This script is injected into the Notion page and runs on every page load.
6 | window.onload = function () {
7 | setInterval(() => {
8 | // Remove all Notion tooltips on images
9 | document
10 | .querySelectorAll('div[style*="position: absolute; top: 4px;"]')
11 | ?.forEach((el) => (el.style.display = 'none'))
12 |
13 | // Remove hidden properties dropdown
14 | const propertiesDropdown = document.querySelector('div[aria-label="Page properties"]')?.nextElementSibling
15 |
16 | if (propertiesDropdown) {
17 | propertiesDropdown.style.display = 'none'
18 | }
19 | }, 1000)
20 | }
21 |
--------------------------------------------------------------------------------
/templates/default/src/site-config.ts:
--------------------------------------------------------------------------------
1 | import { NoteHostSiteConfig, googleTag } from 'notehost'
2 | import { PAGE_SCRIPT_JS_STRING } from './_page-script-js-string'
3 |
4 | // Set this to your Google Tag ID from Google Analytics
5 | const GOOGLE_TAG_ID = ''
6 |
7 | export const SITE_CONFIG: NoteHostSiteConfig = {
8 | domain: '<%= domainName %>',
9 |
10 | // Metatags, optional
11 | // For main page link preview
12 | siteName: '<%= siteName %>',
13 | siteDescription: '<%= siteDescription %>',
14 | siteImage: '<%= siteImage %>',
15 |
16 | // Twitter handle, optional
17 | // twitterHandle: '',
18 |
19 | // URL to custom favicon.ico
20 | // siteIcon: '',
21 |
22 | // Additional safety: avoid serving extraneous Notion content from your website
23 | // Use the value from your Notion settings => Workspace => Settings => Domain
24 | // notionDomain: '',
25 |
26 | // Map slugs (short page names) to Notion page IDs
27 | // Empty slug is your main page
28 | slugToPage: {
29 | '': '<%= mainPageId %>',
30 | contact: 'NOTION_PAGE_ID',
31 | about: 'NOTION_PAGE_ID',
32 | // Hint: you can use '/' in slug name to create subpages
33 | 'about/people': 'NOTION_PAGE_ID',
34 | },
35 |
36 | // Rewrite meta tags for specific pages
37 | // Use the Notion page ID as the key
38 | // pageMetadata: {
39 | // 'NOTION_PAGE_ID': {
40 | // title: 'My Custom Page Title',
41 | // description: 'My custom page description',
42 | // image: 'https://imagehosting.com/images/page_preview.jpg',
43 | // author: 'My Name',
44 | // },
45 | // },
46 |
47 | // Subdomain redirects are optional
48 | // But it is recommended to have one for www
49 | subDomains: {
50 | www: {
51 | redirect: 'https://<%= domainName %>',
52 | },
53 | },
54 |
55 | // The 404 (not found) page is optional
56 | // If you don't have one, the default 404 page will be used
57 | // fof: {
58 | // page: "NOTION_PAGE_ID",
59 | // slug: "404", // default
60 | // },
61 |
62 | // Google Font name, you can choose from https://fonts.google.com
63 | googleFont: 'Roboto',
64 |
65 | // Custom JS for head and body of a Notion page
66 | customHeadCSS: ``,
67 | customHeadJS: googleTag(GOOGLE_TAG_ID),
68 | customBodyJS: PAGE_SCRIPT_JS_STRING,
69 | }
70 |
--------------------------------------------------------------------------------
/templates/default/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "moduleResolution": "node",
4 | "target": "esnext",
5 | "module": "esnext",
6 | "lib": [
7 | "esnext"
8 | ],
9 | "types": [
10 | "@cloudflare/workers-types"
11 | ]
12 | },
13 | "include": [
14 | "src/**/*.ts",
15 | "src/page-script.js",
16 | ],
17 | }
18 |
--------------------------------------------------------------------------------
/templates/default/wrangler.toml:
--------------------------------------------------------------------------------
1 | name = "<%= wranglerWorkerName %>"
2 | main = "src/index.ts"
3 | compatibility_date = "2024-01-08"
4 | workers_dev = true
5 |
6 | routes = [
7 | { pattern = "<%= domainName %>", custom_domain = true },
8 | { pattern = "www.<%= domainName %>", custom_domain = true }
9 | ]
10 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | /* Visit https://aka.ms/tsconfig to read more about this file */
4 | /* Projects */
5 | // "incremental": true, /* Save .tsbuildinfo files to allow for incremental compilation of projects. */
6 | // "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
7 | // "tsBuildInfoFile": "./.tsbuildinfo", /* Specify the path to .tsbuildinfo incremental compilation file. */
8 | // "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects. */
9 | // "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
10 | // "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
11 | /* Language and Environment */
12 | "target": "ESNext", /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */
13 | // "lib": [], /* Specify a set of bundled library declaration files that describe the target runtime environment. */
14 | // "jsx": "preserve", /* Specify what JSX code is generated. */
15 | // "experimentalDecorators": true, /* Enable experimental support for legacy experimental decorators. */
16 | // "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
17 | // "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h'. */
18 | // "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
19 | // "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using 'jsx: react-jsx*'. */
20 | // "reactNamespace": "", /* Specify the object invoked for 'createElement'. This only applies when targeting 'react' JSX emit. */
21 | // "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
22 | // "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
23 | // "moduleDetection": "auto", /* Control what method is used to detect module-format JS files. */
24 | /* Modules */
25 | "module": "commonjs", /* Specify what module code is generated. */
26 | // "rootDir": "./", /* Specify the root folder within your source files. */
27 | // "moduleResolution": "node10", /* Specify how TypeScript looks up a file from a given module specifier. */
28 | // "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
29 | // "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
30 | // "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
31 | // "typeRoots": [], /* Specify multiple folders that act like './node_modules/@types'. */
32 | // "types": [], /* Specify type package names to be included without being referenced in a source file. */
33 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */
34 | // "moduleSuffixes": [], /* List of file name suffixes to search when resolving a module. */
35 | // "allowImportingTsExtensions": true, /* Allow imports to include TypeScript file extensions. Requires '--moduleResolution bundler' and either '--noEmit' or '--emitDeclarationOnly' to be set. */
36 | // "resolvePackageJsonExports": true, /* Use the package.json 'exports' field when resolving package imports. */
37 | // "resolvePackageJsonImports": true, /* Use the package.json 'imports' field when resolving imports. */
38 | // "customConditions": [], /* Conditions to set in addition to the resolver-specific defaults when resolving imports. */
39 | "resolveJsonModule": true, /* Enable importing .json files. */
40 | // "allowArbitraryExtensions": true, /* Enable importing files with any extension, provided a declaration file is present. */
41 | // "noResolve": true, /* Disallow 'import's, 'require's or ''s from expanding the number of files TypeScript should add to a project. */
42 | /* JavaScript Support */
43 | // "allowJs": true, /* Allow JavaScript files to be a part of your program. Use the 'checkJS' option to get errors from these files. */
44 | // "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
45 | // "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from 'node_modules'. Only applicable with 'allowJs'. */
46 | /* Emit */
47 | // "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
48 | // "declarationMap": true, /* Create sourcemaps for d.ts files. */
49 | // "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
50 | // "sourceMap": true, /* Create source map files for emitted JavaScript files. */
51 | // "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
52 | // "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If 'declaration' is true, also designates a file that bundles all .d.ts output. */
53 | // "outDir": "./", /* Specify an output folder for all emitted files. */
54 | // "removeComments": true, /* Disable emitting comments. */
55 | // "noEmit": true, /* Disable emitting files from a compilation. */
56 | // "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
57 | // "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types. */
58 | // "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
59 | // "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
60 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
61 | // "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
62 | // "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
63 | // "newLine": "crlf", /* Set the newline character for emitting files. */
64 | // "stripInternal": true, /* Disable emitting declarations that have '@internal' in their JSDoc comments. */
65 | // "noEmitHelpers": true, /* Disable generating custom helper functions like '__extends' in compiled output. */
66 | // "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
67 | // "preserveConstEnums": true, /* Disable erasing 'const enum' declarations in generated code. */
68 | // "declarationDir": "./", /* Specify the output directory for generated declaration files. */
69 | // "preserveValueImports": true, /* Preserve unused imported values in the JavaScript output that would otherwise be removed. */
70 | /* Interop Constraints */
71 | // "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
72 | // "verbatimModuleSyntax": true, /* Do not transform or elide any imports or exports not marked as type-only, ensuring they are written in the output file's format based on the 'module' setting. */
73 | // "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
74 | "esModuleInterop": true, /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables 'allowSyntheticDefaultImports' for type compatibility. */
75 | // "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
76 | "forceConsistentCasingInFileNames": true, /* Ensure that casing is correct in imports. */
77 | /* Type Checking */
78 | // "strict": true, /* Enable all strict type-checking options. */
79 | // "noImplicitAny": true, /* Enable error reporting for expressions and declarations with an implied 'any' type. */
80 | // "strictNullChecks": true, /* When type checking, take into account 'null' and 'undefined'. */
81 | // "strictFunctionTypes": true, /* When assigning functions, check to ensure parameters and the return values are subtype-compatible. */
82 | // "strictBindCallApply": true, /* Check that the arguments for 'bind', 'call', and 'apply' methods match the original function. */
83 | // "strictPropertyInitialization": true, /* Check for class properties that are declared but not set in the constructor. */
84 | // "noImplicitThis": true, /* Enable error reporting when 'this' is given the type 'any'. */
85 | // "useUnknownInCatchVariables": true, /* Default catch clause variables as 'unknown' instead of 'any'. */
86 | // "alwaysStrict": true, /* Ensure 'use strict' is always emitted. */
87 | // "noUnusedLocals": true, /* Enable error reporting when local variables aren't read. */
88 | // "noUnusedParameters": true, /* Raise an error when a function parameter isn't read. */
89 | // "exactOptionalPropertyTypes": true, /* Interpret optional property types as written, rather than adding 'undefined'. */
90 | // "noImplicitReturns": true, /* Enable error reporting for codepaths that do not explicitly return in a function. */
91 | // "noFallthroughCasesInSwitch": true, /* Enable error reporting for fallthrough cases in switch statements. */
92 | // "noUncheckedIndexedAccess": true, /* Add 'undefined' to a type when accessed using an index. */
93 | // "noImplicitOverride": true, /* Ensure overriding members in derived classes are marked with an override modifier. */
94 | // "noPropertyAccessFromIndexSignature": true, /* Enforces using indexed accessors for keys declared using an indexed type. */
95 | // "allowUnusedLabels": true, /* Disable error reporting for unused labels. */
96 | // "allowUnreachableCode": true, /* Disable error reporting for unreachable code. */
97 | /* Completeness */
98 | // "skipDefaultLibCheck": true, /* Skip type checking .d.ts files that are included with TypeScript. */
99 | "skipLibCheck": true, /* Skip type checking all .d.ts files. */
100 | "noUncheckedIndexedAccess": true,
101 | "noEmit": true,
102 | "types": [
103 | "@cloudflare/workers-types",
104 | "@types/node"
105 | ],
106 | },
107 | "include": [
108 | "**/*.ts"
109 | ],
110 | "exclude": [
111 | "node_modules",
112 | "dist",
113 | "templates",
114 | ],
115 | }
116 |
--------------------------------------------------------------------------------