├── .eslintignore
├── .eslintrc.cjs
├── .github
└── workflows
│ └── build.yml
├── .gitignore
├── .npmrc
├── .prettierignore
├── .prettierrc
├── 2023-04-13-20-09-36.png
├── README.md
├── package.json
├── scripts
└── sitemap.js
├── src
├── app.d.ts
├── app.html
├── hooks.server.ts
├── lib
│ ├── config.ts
│ ├── get-canonical-page-id.ts
│ ├── get-config-value.ts
│ ├── get-site-map.ts
│ ├── icons
│ │ ├── check.svelte
│ │ ├── check.svg
│ │ ├── chevron-down-icon.svelte
│ │ ├── clear-icon.svelte
│ │ ├── collection-view-board.svelte
│ │ ├── collection-view-board.svg
│ │ ├── collection-view-calendar.svelte
│ │ ├── collection-view-calendar.svg
│ │ ├── collection-view-gallery.svelte
│ │ ├── collection-view-gallery.svg
│ │ ├── collection-view-icon.svelte
│ │ ├── collection-view-list.svelte
│ │ ├── collection-view-list.svg
│ │ ├── collection-view-table.svelte
│ │ ├── collection-view-table.svg
│ │ ├── copy.svelte
│ │ ├── copy.svg
│ │ ├── default-page-icon.svelte
│ │ ├── empty-icon.svelte
│ │ ├── file-icon.svelte
│ │ ├── link-icon.svelte
│ │ ├── loading-icon.svelte
│ │ ├── property-icon.svelte
│ │ ├── search-icon.tsx
│ │ ├── type-checkbox.svelte
│ │ ├── type-checkbox.svg
│ │ ├── type-date.svelte
│ │ ├── type-date.svg
│ │ ├── type-email.svelte
│ │ ├── type-email.svg
│ │ ├── type-file.svelte
│ │ ├── type-file.svg
│ │ ├── type-formula.svelte
│ │ ├── type-formula.svg
│ │ ├── type-github.svelte
│ │ ├── type-multi-select.svelte
│ │ ├── type-multi-select.svg
│ │ ├── type-number.svelte
│ │ ├── type-number.svg
│ │ ├── type-person-2.svelte
│ │ ├── type-person-2.svg
│ │ ├── type-person.svelte
│ │ ├── type-person.svg
│ │ ├── type-phone-number.svelte
│ │ ├── type-phone-number.svg
│ │ ├── type-relation.svelte
│ │ ├── type-relation.svg
│ │ ├── type-select.svelte
│ │ ├── type-select.svg
│ │ ├── type-text.svelte
│ │ ├── type-text.svg
│ │ ├── type-timestamp.svelte
│ │ ├── type-timestamp.svg
│ │ ├── type-title.svelte
│ │ ├── type-title.svg
│ │ ├── type-url.svelte
│ │ └── type-url.svg
│ ├── images
│ │ ├── default-page-icon.svg
│ │ ├── github.svg
│ │ ├── svelte-logo.svg
│ │ ├── svelte-welcome.png
│ │ └── svelte-welcome.webp
│ ├── map-image-url.ts
│ ├── notion-api.ts
│ ├── notion.ts
│ ├── site-config.ts
│ ├── style-object.ts
│ └── types.ts
├── routes
│ ├── +layout.svelte
│ ├── +layout.ts
│ ├── +page.server.ts
│ ├── +page.svelte
│ ├── +page.ts
│ ├── Header.svelte
│ ├── [pageId]
│ │ ├── +page.server.ts
│ │ ├── +page.svelte
│ │ └── +page.ts
│ ├── components
│ │ ├── Asset.svelte
│ │ ├── AssetWrapper.svelte
│ │ ├── Block.svelte
│ │ ├── BlockItem
│ │ │ ├── Audio.svelte
│ │ │ ├── Bookmark.svelte
│ │ │ ├── Callout.svelte
│ │ │ ├── Code.svelte
│ │ │ ├── Column.svelte
│ │ │ ├── ColumnList.svelte
│ │ │ ├── Divider.svelte
│ │ │ ├── EOI.svelte
│ │ │ ├── File.svelte
│ │ │ ├── Header.svelte
│ │ │ ├── List.svelte
│ │ │ ├── Quote.svelte
│ │ │ ├── SyncPointerBlock.svelte
│ │ │ ├── Text.svelte
│ │ │ ├── ViewPage.svelte
│ │ │ └── Waiting.svelte
│ │ ├── BlockRender.svelte
│ │ ├── Checkbox.svelte
│ │ ├── Collection
│ │ │ ├── Collection.svelte
│ │ │ ├── CollectionCard.svelte
│ │ │ ├── CollectionColumnTitle.svelte
│ │ │ ├── CollectionProperty.svelte
│ │ │ ├── CollectionPropertyCheckbox.svelte
│ │ │ ├── CollectionPropertyFile.svelte
│ │ │ ├── CollectionPropertyNumber.svelte
│ │ │ ├── CollectionPropertyTime.svelte
│ │ │ ├── CollectionPropteryFormula.svelte
│ │ │ ├── CollectionRow.svelte
│ │ │ ├── CollectionViewGallery.svelte
│ │ │ ├── CollectionViewTabs.svelte
│ │ │ ├── ColletionPropertySelect.svelte
│ │ │ └── eval-formula.ts
│ │ ├── Equation.svelte
│ │ ├── GracefulImage.svelte
│ │ ├── Header.svelte
│ │ ├── LiteYoutubeEmbed.svelte
│ │ ├── PageIcon.svelte
│ │ ├── PageTitle.svelte
│ │ ├── Pdf.svelte
│ │ ├── RecursiveBlock.svelte
│ │ ├── Render.svelte
│ │ ├── Text.svelte
│ │ ├── TextExternalLink.svelte
│ │ ├── TextLink.svelte
│ │ ├── TextPage.svelte
│ │ └── TextPlain.svelte
│ ├── notion.css
│ ├── prism-theme.css
│ └── store.ts
└── site.config.ts
├── static
├── .nojekyll
├── 404.html
├── favicon.png
└── robots.txt
├── svelte.config.js
├── tsconfig.json
└── vite.config.ts
/.eslintignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/.eslintrc.cjs:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | root: true,
3 | parser: '@typescript-eslint/parser',
4 | extends: ['eslint:recommended', 'plugin:@typescript-eslint/recommended', 'prettier'],
5 | plugins: ['svelte3', '@typescript-eslint'],
6 | ignorePatterns: ['*.cjs'],
7 | overrides: [{ files: ['*.svelte'], processor: 'svelte3/svelte3' }],
8 | settings: {
9 | 'svelte3/typescript': () => require('typescript')
10 | },
11 | parserOptions: {
12 | sourceType: 'module',
13 | ecmaVersion: 2020
14 | },
15 | env: {
16 | browser: true,
17 | es2017: true,
18 | node: true
19 | }
20 | };
21 |
--------------------------------------------------------------------------------
/.github/workflows/build.yml:
--------------------------------------------------------------------------------
1 | name: Github Pages
2 |
3 | # Controls when the workflow will run
4 | on:
5 | # Allows you to run this workflow manually from the Actions tab
6 | workflow_dispatch:
7 |
8 |
9 | # A workflow run is made up of one or more jobs that can run sequentially or in parallel
10 | jobs:
11 | # This workflow contains a single job called "build"
12 | build:
13 | # The type of runner that the job will run on
14 | runs-on: ubuntu-latest
15 |
16 | # Steps represent a sequence of tasks that will be executed as part of the job
17 | steps:
18 | - name: Checkout
19 | uses: actions/checkout@v3
20 | - name: Install Dependencies
21 | run: yarn
22 | - name: Build
23 | run: yarn build
24 | - name: Deploy
25 | uses: JamesIves/github-pages-deploy-action@v4
26 | with:
27 | token: ${{ secrets.ACCESS_TOKEN }}
28 | repository-name: tiodot/svelte-notion
29 | branch: doc
30 | folder: build
31 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 | .vercel
10 | .output
11 | vite.config.js.timestamp-*
12 | vite.config.ts.timestamp-*
13 | yarn.lock
14 | .idea
--------------------------------------------------------------------------------
/.npmrc:
--------------------------------------------------------------------------------
1 | engine-strict=true
2 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | .DS_Store
2 | node_modules
3 | /build
4 | /.svelte-kit
5 | /package
6 | .env
7 | .env.*
8 | !.env.example
9 |
10 | # Ignore files for PNPM, NPM and YARN
11 | pnpm-lock.yaml
12 | package-lock.json
13 | yarn.lock
14 |
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "useTabs": true,
3 | "singleQuote": true,
4 | "trailingComma": "none",
5 | "printWidth": 100,
6 | "plugins": ["prettier-plugin-svelte"],
7 | "pluginSearchDirs": ["."],
8 | "overrides": [{ "files": "*.svelte", "options": { "parser": "svelte" } }]
9 | }
10 |
--------------------------------------------------------------------------------
/2023-04-13-20-09-36.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiodot/svelte-notion/c17d3c2ed6017c6143a46eb802dbd007f890df56/2023-04-13-20-09-36.png
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Svelte Notion Kit
2 |
3 | This repo is using svelte to generate static blog site. Inspired by [react-notion-x](https://github.com/NotionX/react-notion-x)
4 |
5 | It uses Notion as a CMS, can be deployed to GitHub pages or Vercel;
6 |
7 | ## Features
8 | - Setup only takes a few minutes(single config file)
9 | - Build using Svelte without CSR that means only html files and css files
10 | - Excellent page speeds, no hydrate, no Javascript.
11 | - Responsive for different devices
12 |
13 | ## Demos
14 | https://xchb.work/svelte-notion/
15 |
16 | 
17 | ## Setup
18 | All config is defined in [site.config.ts](https://github.com/tiodot/svelte-notion/blob/main/src/site.config.ts).
19 |
20 | 1. Fork / clone this repo
21 | 2. Change a few values in [site.config.ts](https://github.com/tiodot/svelte-notion/blob/main/src/site.config.ts)
22 | 3. npm install
23 | 4. npm run dev to test locally
24 | 5. npm run deploy to deploy to vercel 💪
25 |
26 | I tried to make configuration as easy as possible — All you really need to do to get started is edit **rootNotionPageId**.
27 |
28 | We recommend duplicating the [default page](https://tiodot.notion.site/5e19b09eec9e43c5b4c23031d41fea81) as a starting point, but you can use any public notion page you want.
29 |
30 | Make sure your root Notion page is public and then copy the link to your clipboard. Extract the last part of the URL that looks like 7875426197cf461698809def95960ebf, which is your page's Notion ID.
31 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "svelte-notion",
3 | "version": "0.0.1",
4 | "scripts": {
5 | "dev": "vite dev",
6 | "build": "vite build",
7 | "preview": "vite preview",
8 | "check": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json",
9 | "check:watch": "svelte-kit sync && svelte-check --tsconfig ./tsconfig.json --watch",
10 | "lint": "prettier --plugin-search-dir . --check . && eslint .",
11 | "format": "prettier --plugin-search-dir . --write .",
12 | "sitemap": "HOST=https://example.com node scripts/sitemap.js"
13 | },
14 | "devDependencies": {
15 | "@fontsource/fira-mono": "^4.5.10",
16 | "@neoconfetti/svelte": "^1.0.0",
17 | "@sveltejs/adapter-auto": "^2.0.0",
18 | "@sveltejs/adapter-static": "^2.0.1",
19 | "@sveltejs/kit": "^1.5.0",
20 | "@types/cookie": "^0.5.1",
21 | "@typescript-eslint/eslint-plugin": "^5.45.0",
22 | "@typescript-eslint/parser": "^5.45.0",
23 | "classnames": "^2.3.2",
24 | "date-fns": "^2.29.3",
25 | "eslint": "^8.28.0",
26 | "eslint-config-prettier": "^8.5.0",
27 | "eslint-plugin-svelte3": "^4.0.0",
28 | "format-number": "^3.0.0",
29 | "html-minifier": "^4.0.0",
30 | "notion-client": "^6.15.8",
31 | "notion-types": "^6.15.6",
32 | "notion-utils": "^6.15.8",
33 | "p-memoize": "^7.1.1",
34 | "prettier": "^2.8.0",
35 | "prettier-plugin-svelte": "^2.8.1",
36 | "prismjs": "^1.29.0",
37 | "querystring": "^0.2.1",
38 | "svelte": "^3.54.0",
39 | "svelte-check": "^3.0.1",
40 | "svelte-katex": "^0.1.2",
41 | "svelte-pdf": "^1.0.17",
42 | "tslib": "^2.4.1",
43 | "typescript": "^4.9.3",
44 | "vite": "^4.0.0"
45 | },
46 | "type": "module"
47 | }
48 |
--------------------------------------------------------------------------------
/scripts/sitemap.js:
--------------------------------------------------------------------------------
1 | /**
2 | * build sitemap from output
3 | */
4 | import fs from 'fs';
5 | import path from 'path';
6 | import * as devalue from 'devalue'
7 | import { fileURLToPath } from 'url';
8 | import {idToUuid} from 'notion-utils';
9 |
10 | const host = process.env.HOST;
11 |
12 |
13 | const __filename = fileURLToPath(import.meta.url);
14 | const __dirname = path.dirname(__filename);
15 |
16 | const data = fs.readFileSync(path.resolve(__dirname, '../build/__data.json'), {encoding: 'utf-8'});
17 | const blockMap = devalue.unflatten(JSON.parse(data).nodes[1].data)?.recordMap?.block ?? {};
18 |
19 |
20 | function createSitemapXml(urls) {
21 | let urlsXml = ''
22 | urls.forEach(u => {
23 | urlsXml += `
24 | ${u.loc}
25 | ${u.lastmod}
26 | ${u.changefreq}
27 | 0.7
28 |
29 | `
30 | })
31 |
32 | return `
38 | ${urlsXml}
39 | `
40 | }
41 |
42 |
43 | fs.readdir(path.resolve(__dirname, '../build'), {withFileTypes: true}, (err, files) => {
44 | // console.log(files)
45 | const filesNames = files.filter(file => file.isDirectory() && file.name !== '_app').map(file => file.name)
46 | // console.log(filesNames)
47 | const urls = [{
48 | loc: host,
49 | lastmod: new Date().toISOString().split('T')[0],
50 | changefreq: 'daily'
51 | }]
52 |
53 | filesNames.forEach(name => {
54 | const matched = name.match(/-(\w{32})$/)
55 | if (matched && matched[1]) {
56 | const id = idToUuid(matched[1])
57 | const block = blockMap[id]
58 | // console.log(block, '<----')
59 | urls.push({
60 | loc: `${host}/${name}`,
61 | lastmod: new Date(block.value.last_edited_time || block.value.created_time).toISOString().split('T')[0],
62 | changefreq: 'daily'
63 | })
64 | }
65 | })
66 | // console.log(urls)
67 | const content = createSitemapXml(urls)
68 | fs.writeFile(path.resolve(__dirname, '../build/sitemap.xml'), content, console.log)
69 | })
70 |
--------------------------------------------------------------------------------
/src/app.d.ts:
--------------------------------------------------------------------------------
1 | // See https://kit.svelte.dev/docs/types#app
2 | // for information about these interfaces
3 | declare global {
4 | namespace App {
5 | // interface Error {}
6 | // interface Locals {}
7 | // interface PageData {}
8 | // interface Platform {}
9 | }
10 | }
11 |
12 | export {};
13 |
--------------------------------------------------------------------------------
/src/app.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 | %sveltekit.head%
8 |
9 |
10 | %sveltekit.body%
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/hooks.server.ts:
--------------------------------------------------------------------------------
1 | import type { Handle } from '@sveltejs/kit';
2 |
3 | import { minify } from 'html-minifier';
4 | import { building } from '$app/environment';
5 |
6 | const minification_options = {
7 | collapseBooleanAttributes: true,
8 | collapseWhitespace: true,
9 | html5: true,
10 | ignoreCustomComments: [/^#/],
11 | minifyCSS: true,
12 | minifyJS: false,
13 | removeComments: true, // some hydration code needs comments, so leave them in
14 | };
15 |
16 | export const handle = (async ({ event, resolve }) => {
17 | let page = '';
18 |
19 | return resolve(event, {
20 | transformPageChunk: ({ html, done }) => {
21 | page += html;
22 | if (done) {
23 | return building ? minify(page, minification_options) : page;
24 | }
25 | }
26 | });
27 | }) satisfies Handle;
28 |
--------------------------------------------------------------------------------
/src/lib/config.ts:
--------------------------------------------------------------------------------
1 | /**
2 | * Site-wide app configuration.
3 | *
4 | * This file pulls from the root "site.config.ts" as well as environment variables
5 | * for optional depenencies.
6 | */
7 | import { parsePageId } from 'notion-utils';
8 |
9 | import { getEnv, getSiteConfig } from './get-config-value';
10 | import type { NavigationLink } from './site-config';
11 | import type {
12 | NavigationStyle,
13 | PageUrlOverridesInverseMap,
14 | PageUrlOverridesMap,
15 | Site
16 | } from './types';
17 |
18 | export const rootNotionPageId: string = parsePageId(getSiteConfig('rootNotionPageId'), {
19 | uuid: false
20 | });
21 |
22 | if (!rootNotionPageId) {
23 | throw new Error('Config error invalid "rootNotionPageId"');
24 | }
25 |
26 | // if you want to restrict pages to a single notion workspace (optional)
27 | export const rootNotionSpaceId: string | null = parsePageId(
28 | getSiteConfig('rootNotionSpaceId', null),
29 | { uuid: true }
30 | );
31 |
32 | export const pageUrlOverrides = cleanPageUrlMap(getSiteConfig('pageUrlOverrides', {}) || {}, {
33 | label: 'pageUrlOverrides'
34 | });
35 |
36 | export const pageUrlAdditions = cleanPageUrlMap(getSiteConfig('pageUrlAdditions', {}) || {}, {
37 | label: 'pageUrlAdditions'
38 | });
39 |
40 | export const inversePageUrlOverrides = invertPageUrlOverrides(pageUrlOverrides);
41 |
42 | export const environment = process.env.NODE_ENV || 'development';
43 | export const isDev = environment === 'development';
44 |
45 | // general site config
46 | export const name: string = getSiteConfig('name');
47 | export const author: string = getSiteConfig('author');
48 | export const domain: string = getSiteConfig('domain');
49 | export const description: string = getSiteConfig('description', 'Notion Blog');
50 | export const language: string = getSiteConfig('language', 'en');
51 |
52 | // default notion values for site-wide consistency (optional; may be overridden on a per-page basis)
53 | export const defaultPageIcon: string | null = getSiteConfig('defaultPageIcon', null);
54 | export const defaultPageCover: string | null = getSiteConfig('defaultPageCover', null);
55 | export const defaultPageCoverPosition: number = getSiteConfig('defaultPageCoverPosition', 0.5);
56 |
57 | // Optional whether or not to include the Notion ID in page URLs or just use slugs
58 | export const includeNotionIdInUrls: boolean = getSiteConfig('includeNotionIdInUrls', !!isDev);
59 |
60 | export const navigationStyle: NavigationStyle = getSiteConfig('navigationStyle', 'default');
61 |
62 | export const navigationLinks: Array | null = getSiteConfig(
63 | 'navigationLinks',
64 | null
65 | );
66 |
67 | // Optional site search
68 | export const isSearchEnabled: boolean = getSiteConfig('isSearchEnabled', false);
69 |
70 | // ----------------------------------------------------------------------------
71 |
72 | // Optional redis instance for persisting preview images
73 | export const isRedisEnabled: boolean =
74 | getSiteConfig('isRedisEnabled', false) || !!getEnv('REDIS_ENABLED', '');
75 |
76 | // (if you want to enable redis, only REDIS_HOST and REDIS_PASSWORD are required)
77 | // we recommend that you store these in a local `.env` file
78 | export const redisHost: string | null = getEnv('REDIS_HOST', '');
79 | export const redisPassword: string | null = getEnv('REDIS_PASSWORD', '');
80 | export const redisUser: string = getEnv('REDIS_USER', 'default');
81 | export const redisUrl = getEnv('REDIS_URL', `redis://${redisUser}:${redisPassword}@${redisHost}`);
82 | export const redisNamespace: string | null = getEnv('REDIS_NAMESPACE', 'preview-images');
83 |
84 | // ----------------------------------------------------------------------------
85 |
86 | export const isServer = typeof window === 'undefined';
87 |
88 | export const port = getEnv('PORT', '3000');
89 | export const host = isDev ? `http://localhost:${port}` : `https://${domain}`;
90 | export const apiHost = isDev ? host : `https://${process.env.VERCEL_URL || domain}`;
91 |
92 | export const apiBaseUrl = `/api`;
93 |
94 | export const api = {
95 | searchNotion: `${apiBaseUrl}/search-notion`,
96 | getNotionPageInfo: `${apiBaseUrl}/notion-page-info`,
97 | getSocialImage: `${apiBaseUrl}/social-image`
98 | };
99 |
100 | // ----------------------------------------------------------------------------
101 |
102 | export const site: Site = {
103 | domain,
104 | name,
105 | rootNotionPageId,
106 | rootNotionSpaceId,
107 | description
108 | };
109 |
110 | function cleanPageUrlMap(
111 | pageUrlMap: PageUrlOverridesMap,
112 | {
113 | label
114 | }: {
115 | label: string;
116 | }
117 | ): PageUrlOverridesMap {
118 | return Object.keys(pageUrlMap).reduce((acc, uri) => {
119 | const pageId = pageUrlMap[uri];
120 | const uuid = parsePageId(pageId, { uuid: false });
121 |
122 | if (!uuid) {
123 | throw new Error(`Invalid ${label} page id "${pageId}"`);
124 | }
125 |
126 | if (!uri) {
127 | throw new Error(`Missing ${label} value for page "${pageId}"`);
128 | }
129 |
130 | if (!uri.startsWith('/')) {
131 | throw new Error(
132 | `Invalid ${label} value for page "${pageId}": value "${uri}" should be a relative URI that starts with "/"`
133 | );
134 | }
135 |
136 | const path = uri.slice(1);
137 |
138 | return {
139 | ...acc,
140 | [path]: uuid
141 | };
142 | }, {});
143 | }
144 |
145 | function invertPageUrlOverrides(pageUrlOverrides: PageUrlOverridesMap): PageUrlOverridesInverseMap {
146 | return Object.keys(pageUrlOverrides).reduce((acc, uri) => {
147 | const pageId = pageUrlOverrides[uri];
148 |
149 | return {
150 | ...acc,
151 | [pageId]: uri
152 | };
153 | }, {});
154 | }
155 |
--------------------------------------------------------------------------------
/src/lib/get-canonical-page-id.ts:
--------------------------------------------------------------------------------
1 | import type { ExtendedRecordMap } from 'notion-types'
2 | import {
3 | getCanonicalPageId as getCanonicalPageIdImpl,
4 | parsePageId
5 | } from 'notion-utils'
6 |
7 | import { inversePageUrlOverrides } from './config'
8 |
9 | export function getCanonicalPageId(
10 | pageId: string,
11 | recordMap: ExtendedRecordMap,
12 | { uuid = true }: { uuid?: boolean } = {}
13 | ): string | null {
14 | const cleanPageId = parsePageId(pageId, { uuid: false })
15 | if (!cleanPageId) {
16 | return null
17 | }
18 |
19 | const override = inversePageUrlOverrides[cleanPageId]
20 | if (override) {
21 | return override
22 | } else {
23 | return getCanonicalPageIdImpl(pageId, recordMap, {
24 | uuid
25 | })
26 | }
27 | }
28 |
--------------------------------------------------------------------------------
/src/lib/get-config-value.ts:
--------------------------------------------------------------------------------
1 | import rawSiteConfig from '../site.config';
2 | import type { SiteConfig } from './site-config';
3 |
4 | if (!rawSiteConfig) {
5 | throw new Error(`Config error: invalid site.config.ts`);
6 | }
7 |
8 | // allow environment variables to override site.config.ts
9 | let siteConfigOverrides: SiteConfig;
10 |
11 | try {
12 | if (process.env.NEXT_PUBLIC_SITE_CONFIG) {
13 | siteConfigOverrides = JSON.parse(process.env.NEXT_PUBLIC_SITE_CONFIG);
14 | }
15 | } catch (err) {
16 | console.error('Invalid config "NEXT_PUBLIC_SITE_CONFIG" failed to parse');
17 | throw err;
18 | }
19 |
20 | const siteConfig: SiteConfig = {
21 | ...rawSiteConfig,
22 | ...(siteConfigOverrides ?? {})
23 | };
24 |
25 | export function getSiteConfig(key: string, defaultValue?: T): T {
26 | const value = siteConfig[key as keyof SiteConfig];
27 |
28 | if (value !== undefined) {
29 | return value as T;
30 | }
31 |
32 | if (defaultValue !== undefined) {
33 | return defaultValue;
34 | }
35 |
36 | throw new Error(`Config error: missing required site config value "${key}"`);
37 | }
38 |
39 | export function getEnv(key: string, defaultValue?: string, env = process.env): string {
40 | const value = env[key];
41 |
42 | if (value !== undefined) {
43 | return value;
44 | }
45 |
46 | if (defaultValue !== undefined) {
47 | return defaultValue;
48 | }
49 |
50 | throw new Error(`Config error: missing required env variable "${key}"`);
51 | }
52 |
--------------------------------------------------------------------------------
/src/lib/get-site-map.ts:
--------------------------------------------------------------------------------
1 | import { getAllPagesInSpace, uuidToId } from 'notion-utils';
2 | import pMemoize from 'p-memoize';
3 |
4 | import * as config from './config';
5 | import type * as types from './types';
6 | import { includeNotionIdInUrls } from './config';
7 | import { getCanonicalPageId } from './get-canonical-page-id';
8 | import { notion } from './notion-api';
9 |
10 | const uuid = !!includeNotionIdInUrls;
11 |
12 | export async function getSiteMap(): Promise {
13 | const partialSiteMap = await getAllPages(config.rootNotionPageId, config.rootNotionSpaceId);
14 |
15 | return {
16 | site: config.site,
17 | ...partialSiteMap
18 | } as types.SiteMap;
19 | }
20 |
21 | const getAllPages = pMemoize(getAllPagesImpl, {
22 | cacheKey: (...args) => JSON.stringify(args)
23 | });
24 |
25 | async function getAllPagesImpl(
26 | rootNotionPageId: string,
27 | rootNotionSpaceId: string
28 | ): Promise> {
29 | const getPage = async (pageId: string, ...args) => {
30 | // console.log('\nnotion getPage', uuidToId(pageId));
31 | return notion.getPage(pageId, ...args);
32 | };
33 |
34 | const pageMap = await getAllPagesInSpace(rootNotionPageId, rootNotionSpaceId, getPage);
35 |
36 | const canonicalPageMap = Object.keys(pageMap).reduce((map, pageId: string) => {
37 | const recordMap = pageMap[pageId];
38 | if (!recordMap) {
39 | throw new Error(`Error loading page "${pageId}"`);
40 | }
41 |
42 | const canonicalPageId = getCanonicalPageId(pageId, recordMap, {
43 | uuid
44 | });
45 |
46 | if (map[canonicalPageId]) {
47 | // you can have multiple pages in different collections that have the same id
48 | // TODO: we may want to error if neither entry is a collection page
49 | console.warn('error duplicate canonical page id', {
50 | canonicalPageId,
51 | pageId,
52 | existingPageId: map[canonicalPageId]
53 | });
54 |
55 | return map;
56 | } else {
57 | return {
58 | ...map,
59 | [canonicalPageId]: pageId
60 | };
61 | }
62 | }, {});
63 |
64 | return {
65 | pageMap,
66 | canonicalPageMap
67 | };
68 | }
69 |
--------------------------------------------------------------------------------
/src/lib/icons/check.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/check.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/chevron-down-icon.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/clear-icon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/collection-view-board.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/collection-view-board.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/collection-view-calendar.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/collection-view-calendar.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/collection-view-gallery.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/icons/collection-view-gallery.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/collection-view-icon.svelte:
--------------------------------------------------------------------------------
1 |
22 |
--------------------------------------------------------------------------------
/src/lib/icons/collection-view-list.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/collection-view-list.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/collection-view-table.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/collection-view-table.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/copy.svelte:
--------------------------------------------------------------------------------
1 |
2 |
17 |
18 |
--------------------------------------------------------------------------------
/src/lib/icons/copy.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/icons/default-page-icon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/empty-icon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/file-icon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/link-icon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
13 |
--------------------------------------------------------------------------------
/src/lib/icons/loading-icon.svelte:
--------------------------------------------------------------------------------
1 |
2 |
50 |
--------------------------------------------------------------------------------
/src/lib/icons/property-icon.svelte:
--------------------------------------------------------------------------------
1 |
46 |
47 |
--------------------------------------------------------------------------------
/src/lib/icons/search-icon.tsx:
--------------------------------------------------------------------------------
1 | import * as React from 'react'
2 |
3 | import { cs } from '../utils'
4 |
5 | export const SearchIcon = (props) => {
6 | const { className, ...rest } = props
7 | return (
8 |
11 | )
12 | }
13 |
--------------------------------------------------------------------------------
/src/lib/icons/type-checkbox.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/type-checkbox.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-date.svelte:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/icons/type-date.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-email.svelte:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-email.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-file.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/type-file.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-formula.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/type-formula.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-github.svelte:
--------------------------------------------------------------------------------
1 |
2 |
10 |
--------------------------------------------------------------------------------
/src/lib/icons/type-multi-select.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/type-multi-select.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-number.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/icons/type-number.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-person-2.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/icons/type-person-2.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-person.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/icons/type-person.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-phone-number.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/icons/type-phone-number.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-relation.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/icons/type-relation.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-select.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
--------------------------------------------------------------------------------
/src/lib/icons/type-select.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-text.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/icons/type-text.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-timestamp.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/icons/type-timestamp.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-title.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/icons/type-title.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/icons/type-url.svelte:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 |
--------------------------------------------------------------------------------
/src/lib/icons/type-url.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/lib/images/default-page-icon.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/images/github.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/images/svelte-logo.svg:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/src/lib/images/svelte-welcome.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiodot/svelte-notion/c17d3c2ed6017c6143a46eb802dbd007f890df56/src/lib/images/svelte-welcome.png
--------------------------------------------------------------------------------
/src/lib/images/svelte-welcome.webp:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiodot/svelte-notion/c17d3c2ed6017c6143a46eb802dbd007f890df56/src/lib/images/svelte-welcome.webp
--------------------------------------------------------------------------------
/src/lib/map-image-url.ts:
--------------------------------------------------------------------------------
1 | import type { Block } from 'notion-types';
2 |
3 | import { defaultPageCover, defaultPageIcon } from './config';
4 |
5 | const defaultMapImageUrl = (url: string, block: Block) => {
6 | if (!url) {
7 | return null;
8 | }
9 | if (url.startsWith('data:')) {
10 | return url;
11 | }
12 | if (url.startsWith('https://images.unsplash.com')) {
13 | return url;
14 | }
15 |
16 | if (url.startsWith('/images')) {
17 | url = `https://www.notion.so${url}`;
18 | }
19 | url = `https://www.notion.so${
20 | url.startsWith('/image') ? url : `/image/${encodeURIComponent(url)}`
21 | }`;
22 |
23 | const notionImageUrlV2 = new URL(url);
24 | let table = block.parent_table === 'space' ? 'block' : block.parent_table;
25 | if (table === 'collection' || table === 'team') {
26 | table = 'block';
27 | }
28 | notionImageUrlV2.searchParams.set('table', table);
29 | notionImageUrlV2.searchParams.set('id', block.id);
30 | notionImageUrlV2.searchParams.set('cache', 'v2');
31 | url = notionImageUrlV2.toString();
32 | return url;
33 | };
34 |
35 | export const mapImageUrl = (url: string, block: Block) => {
36 | if (url === defaultPageCover || url === defaultPageIcon) {
37 | return url;
38 | }
39 |
40 | return defaultMapImageUrl(url, block);
41 | };
42 |
--------------------------------------------------------------------------------
/src/lib/notion-api.ts:
--------------------------------------------------------------------------------
1 | import { NotionAPI } from 'notion-client'
2 |
3 | export const notion = new NotionAPI({
4 | apiBaseUrl: process.env.NOTION_API_BASE_URL
5 | })
6 |
--------------------------------------------------------------------------------
/src/lib/notion.ts:
--------------------------------------------------------------------------------
1 | import { NotionAPI } from 'notion-client';
2 | import type { ExtendedRecordMap, SearchParams, SearchResults } from 'notion-types';
3 | import { mergeRecordMaps, parsePageId } from 'notion-utils';
4 | import pMap from 'p-map';
5 | import pMemoize from 'p-memoize';
6 | import { notion } from './notion-api';
7 |
8 | import {
9 | navigationLinks,
10 | navigationStyle,
11 | pageUrlAdditions,
12 | pageUrlOverrides,
13 | site
14 | } from './config';
15 | import { getSiteMap } from './get-site-map';
16 | import type { SiteMap } from './types';
17 |
18 | let globalSiteMap: SiteMap;
19 |
20 | const getNavigationLinkPages = pMemoize(async (): Promise => {
21 | const navigationLinkPageIds = (navigationLinks || [])
22 | .map((link) => link && link.pageId)
23 | .filter(Boolean);
24 |
25 | if (navigationStyle !== 'default' && navigationLinkPageIds.length) {
26 | return pMap(
27 | navigationLinkPageIds,
28 | async (navigationLinkPageId) =>
29 | notion.getPage(navigationLinkPageId || '', {
30 | chunkLimit: 1,
31 | fetchMissingBlocks: false,
32 | fetchCollections: false,
33 | signFileUrls: false
34 | }),
35 | {
36 | concurrency: 4
37 | }
38 | );
39 | }
40 |
41 | return [];
42 | });
43 |
44 | export async function getPage(pageId: string): Promise {
45 | let recordMap = await notion.getPage(pageId);
46 |
47 | if (navigationStyle !== 'default') {
48 | // ensure that any pages linked to in the custom navigation header have
49 | // their block info fully resolved in the page record map so we know
50 | // the page title, slug, etc.
51 | const navigationLinkRecordMaps = await getNavigationLinkPages();
52 |
53 | if (navigationLinkRecordMaps?.length) {
54 | recordMap = navigationLinkRecordMaps.reduce(
55 | (map, navigationLinkRecordMap) => mergeRecordMaps(map, navigationLinkRecordMap),
56 | recordMap
57 | );
58 | }
59 | }
60 |
61 | return recordMap;
62 | }
63 |
64 | export async function resolveNotionPage(domain?: string, rawPageId?: string) {
65 | let pageId;
66 | let recordMap: ExtendedRecordMap;
67 | if (rawPageId && rawPageId !== 'index') {
68 | pageId = parsePageId(pageId);
69 |
70 | if (!pageId) {
71 | const override = pageUrlOverrides[rawPageId] || pageUrlAdditions[rawPageId];
72 |
73 | if (override) {
74 | pageId = parsePageId(override);
75 | }
76 | }
77 | if (pageId) {
78 | recordMap = await getPage(pageId);
79 | } else {
80 | if (!globalSiteMap) {
81 | globalSiteMap = await getSiteMap();
82 | }
83 | pageId = globalSiteMap?.canonicalPageMap[rawPageId];
84 | if (pageId) {
85 | recordMap = await getPage(pageId);
86 | }else {
87 | throw Error('not ---> found')
88 | }
89 | }
90 | } else {
91 | recordMap = await getPage(site.rootNotionPageId);
92 | }
93 | return recordMap;
94 | }
95 |
--------------------------------------------------------------------------------
/src/lib/site-config.ts:
--------------------------------------------------------------------------------
1 | import type * as types from './types';
2 |
3 | export interface SiteConfig {
4 | rootNotionPageId: string;
5 | rootNotionSpaceId?: string;
6 |
7 | name: string;
8 | domain: string;
9 | author: string;
10 | description?: string;
11 |
12 | defaultPageIcon?: string | null;
13 | defaultPageCover?: string | null;
14 | defaultPageCoverPosition?: number | null;
15 |
16 | isSearchEnabled?: boolean;
17 |
18 | includeNotionIdInUrls?: boolean;
19 | pageUrlOverrides?: types.PageUrlOverridesMap;
20 | pageUrlAdditions?: types.PageUrlOverridesMap;
21 |
22 | navigationStyle?: types.NavigationStyle;
23 | }
24 |
25 | export interface NavigationLink {
26 | title: string;
27 | pageId?: string;
28 | url?: string;
29 | }
30 |
31 | export const siteConfig = (config: SiteConfig): SiteConfig => {
32 | return config;
33 | };
34 |
--------------------------------------------------------------------------------
/src/lib/style-object.ts:
--------------------------------------------------------------------------------
1 | export const camelCaseToDash = (obj: Record) => {
2 | return Object.keys(obj).reduce((pre, cur) => {
3 | if (obj[cur] === undefined) {
4 | return pre;
5 | }
6 | const dashKey = cur.replaceAll(/[A-W]/g, ($0) => `-${$0.toLocaleLowerCase()}`);
7 | return pre + `;${dashKey}: ${obj[cur]}`;
8 | }, '');
9 | };
10 |
--------------------------------------------------------------------------------
/src/lib/types.ts:
--------------------------------------------------------------------------------
1 | import type { ExtendedRecordMap, PageMap } from 'notion-types';
2 | import type { ParsedUrlQuery } from 'querystring';
3 |
4 | export * from 'notion-types';
5 |
6 | export type NavigationStyle = 'default' | 'custom';
7 |
8 | export interface PageError {
9 | message?: string;
10 | statusCode: number;
11 | }
12 |
13 | export interface PageProps {
14 | site?: Site;
15 | recordMap?: ExtendedRecordMap;
16 | pageId?: string;
17 | error?: PageError;
18 | }
19 |
20 | export interface Params extends ParsedUrlQuery {
21 | pageId: string;
22 | }
23 |
24 | export interface Site {
25 | name: string;
26 | domain: string;
27 |
28 | rootNotionPageId: string;
29 | rootNotionSpaceId: string;
30 |
31 | // settings
32 | html?: string;
33 | fontFamily?: string;
34 | darkMode?: boolean;
35 | previewImages?: boolean;
36 |
37 | // opengraph metadata
38 | description?: string;
39 | image?: string;
40 | }
41 |
42 | export interface SiteMap {
43 | site: Site;
44 | pageMap: PageMap;
45 | canonicalPageMap: CanonicalPageMap;
46 | }
47 |
48 | export interface CanonicalPageMap {
49 | [canonicalPageId: string]: string;
50 | }
51 |
52 | export interface PageUrlOverridesMap {
53 | // maps from a URL path to the notion page id the page should be resolved to
54 | // (this overrides the built-in URL path generation for these pages)
55 | [pagePath: string]: string;
56 | }
57 |
58 | export interface PageUrlOverridesInverseMap {
59 | // maps from a notion page id to the URL path the page should be resolved to
60 | // (this overrides the built-in URL path generation for these pages)
61 | [pageId: string]: string;
62 | }
63 |
64 | export interface NotionPageInfo {
65 | pageId: string;
66 | title: string;
67 | image: string;
68 | imageObjectPosition: string;
69 | author: string;
70 | authorImage: string;
71 | detail: string;
72 | }
73 |
--------------------------------------------------------------------------------
/src/routes/+layout.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
11 |
12 |
13 |
20 |
--------------------------------------------------------------------------------
/src/routes/+layout.ts:
--------------------------------------------------------------------------------
1 | export const prerender = true;
2 | export const csr = false;
3 |
--------------------------------------------------------------------------------
/src/routes/+page.server.ts:
--------------------------------------------------------------------------------
1 | import { site } from '$lib/config';
2 | import { resolveNotionPage } from '$lib/notion';
3 | import { error } from '@sveltejs/kit';
4 | import { getBlockTitle } from 'notion-utils';
5 |
6 | export async function load() {
7 | const recordMap = await resolveNotionPage();
8 | // console.log('props:', recordMap);
9 |
10 | const keys = Object.keys(recordMap.block) || {};
11 | const block = recordMap.block[keys[0]].value;
12 |
13 | if (block.type.startsWith('collection_view') && block.collection_id) {
14 | const collection = recordMap.collection[block.collection_id].value;
15 | block.format = {
16 | ...block.format,
17 | page_cover: collection.cover || undefined,
18 | page_icon: collection.icon
19 | };
20 | }
21 | const title = getBlockTitle(block, recordMap);
22 | return { title, recordMap, site };
23 | }
24 |
--------------------------------------------------------------------------------
/src/routes/+page.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 | {title}
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/routes/+page.ts:
--------------------------------------------------------------------------------
1 | // since there's no dynamic data here, we can prerender
2 | // it so that it gets served as a static asset in production
3 |
4 | export const prerender = true;
5 |
--------------------------------------------------------------------------------
/src/routes/Header.svelte:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
13 |
14 |
31 |
32 |
37 |
38 |
39 |
128 |
--------------------------------------------------------------------------------
/src/routes/[pageId]/+page.server.ts:
--------------------------------------------------------------------------------
1 | import { domain, site } from '$lib/config';
2 | import { resolveNotionPage } from '$lib/notion';
3 | import { getBlockTitle } from 'notion-utils';
4 |
5 | export async function load({ params }: {params: {pageId: string}}) {
6 | console.log('request pageId:', params)
7 | const recordMap = await resolveNotionPage(domain, params.pageId);
8 | // console.log('props:', recordMap);
9 | const keys = Object.keys(recordMap.block) || {};
10 | const block = recordMap.block[keys[0]].value;
11 | const title = getBlockTitle(block, recordMap);
12 | return { title, recordMap, site };
13 | }
14 |
--------------------------------------------------------------------------------
/src/routes/[pageId]/+page.svelte:
--------------------------------------------------------------------------------
1 |
2 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 | {title}
33 |
34 |
35 |
36 |
--------------------------------------------------------------------------------
/src/routes/[pageId]/+page.ts:
--------------------------------------------------------------------------------
1 | export const prerender = 'auto';
--------------------------------------------------------------------------------
/src/routes/components/Asset.svelte:
--------------------------------------------------------------------------------
1 |
343 |
344 | {#if !invalid}
345 |
346 | {#if component.type === 'element'}
347 |
348 | {:else}
349 |
350 | {/if}
351 | {#if block?.type === 'image'}
352 |
353 | {/if}
354 |
355 | {#if block.type !== 'image'}
356 |
357 | {/if}
358 | {/if}
359 |
--------------------------------------------------------------------------------
/src/routes/components/AssetWrapper.svelte:
--------------------------------------------------------------------------------
1 |
49 |
50 | {#if isURL}
51 | {@const captionHostname = extractHostname(caption)}
52 |
59 |
60 |
61 | {#if block?.properties?.caption && !isURL}
62 |
63 |
64 |
65 | {/if}
66 |
67 |
68 |
69 | {:else}
70 |
71 |
72 | {#if block?.properties?.caption && !isURL}
73 |
74 |
75 |
76 | {/if}
77 |
78 |
79 | {/if}
80 |
--------------------------------------------------------------------------------
/src/routes/components/Block.svelte:
--------------------------------------------------------------------------------
1 |
86 |
87 |
88 |
89 |
90 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/Audio.svelte:
--------------------------------------------------------------------------------
1 |
9 |
10 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/Bookmark.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 | {#if block.properties}
21 | {@const link = block.properties.link}
22 | {#if link && link[0]?.[0]}
23 | {@const title = getTitle(link)}
24 |
64 | {/if}
65 | {/if}
66 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/Callout.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
22 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/Code.svelte:
--------------------------------------------------------------------------------
1 |
66 |
67 | {@html htmlStr}
71 | {#if caption}
72 |
73 |
74 |
75 | {/if}
76 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/Column.svelte:
--------------------------------------------------------------------------------
1 |
15 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/ColumnList.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
6 |
7 |
8 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/Divider.svelte:
--------------------------------------------------------------------------------
1 |
4 |
5 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/EOI.svelte:
--------------------------------------------------------------------------------
1 |
47 |
48 | {#if isValid}
49 |
57 | {#if data.externalImageType}
58 |
59 |
60 |
61 | {/if}
62 |
63 |
{data.title}
64 |
65 | {#if data.owner || data.lastUpdated}
66 |
67 | {#if data.owner}
68 | {data.owner}
69 | {/if}
70 | {#if data.owner && data.lastUpdated}
71 | •
72 | {/if}
73 | {#if data.lastUpdated}
74 | Updated {data.lastUpdated}
75 | {/if}
76 |
77 | {/if}
78 |
79 |
80 | {/if}
81 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/File.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
29 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/Header.svelte:
--------------------------------------------------------------------------------
1 |
62 | {#if block.properties}
63 | {#if block.format?.toggleable}
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
73 |
74 |
75 |
76 |
77 | {:else }
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 | {/if}
89 | {/if}
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/List.svelte:
--------------------------------------------------------------------------------
1 |
59 |
60 |
61 | {#if block.content}
62 | {#if block.properties}
63 |
64 |
65 |
66 | {/if}
67 |
68 |
69 |
70 | {:else if block.properties}
71 |
72 |
73 |
74 | {/if}
75 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/Quote.svelte:
--------------------------------------------------------------------------------
1 |
7 |
8 | {#if block.properties}
9 | {@const blockColor = block.format?.block_color}
10 |
11 | {/if}
12 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/SyncPointerBlock.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 | {#if referenceBlockId}
15 |
16 | {/if}
17 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/Text.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 | {#if !block.properties && !block.content?.length}
10 |
11 | {:else }
12 |
14 | {#if block.properties?.title}
15 |
16 | {/if}
17 |
18 |
19 |
20 | {/if}
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/ViewPage.svelte:
--------------------------------------------------------------------------------
1 |
69 |
70 |
71 | {#if level === 0}
72 | {@const properties = blockType === 'page' ? block.properties : getCollectionProperties()}
73 |
74 |
75 |
76 |
77 |
78 |
79 | {#if hasPageCover}
80 |
81 |
82 |

88 |
89 |
90 | {/if}
91 |
92 | {#if page_icon}
93 |
94 | {/if}
95 |
96 |
97 |
98 |
99 |
100 | {#if blockType === 'collection_view_page' || (blockType === 'page' && block.parent_table === 'collection')}
101 |
102 | {/if}
103 |
104 | {#if blockType !== 'collection_view_page'}
105 |
106 |
107 |
108 |
109 |
110 | {#if hasAside}
111 |
112 | {/if}
113 |
114 | {/if}
115 |
116 |
117 |
118 |
119 |
120 | {:else}
121 |
126 |
127 |
128 | {/if}
129 |
--------------------------------------------------------------------------------
/src/routes/components/BlockItem/Waiting.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 | empty {block.type}
7 |
--------------------------------------------------------------------------------
/src/routes/components/BlockRender.svelte:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/src/routes/components/Checkbox.svelte:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 | {#if isChecked}
8 |
13 | {:else}
14 |
15 | {/if}
16 |
17 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/Collection.svelte:
--------------------------------------------------------------------------------
1 |
37 |
38 | {#if !isInValid}
39 | {#if block.type === 'page'}
40 |
41 | {:else }
42 |
43 |
44 | {#if viewIds.length > 1 && showCollectionViewDropdown}
45 | {defaultCollectionViewId = id }}
49 | />
50 | {/if}
51 |
52 | {#if showTitle}
53 |
63 | {/if}
64 |
65 |
66 |
71 |
72 | {/if}
73 | {/if}
74 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/CollectionCard.svelte:
--------------------------------------------------------------------------------
1 |
128 |
129 |
133 | {#if coverContent || cover?.type !== 'none'}
134 |
135 | {#if coverContent.type === 'image'}
136 |

142 | {:else if coverContent.type === 'empty'}
143 |
144 | {:else if coverContent.type === 'property'}
145 |
151 | {/if}
152 |
153 | {/if}
154 |
155 |
156 |
157 |
158 |
165 |
166 |
167 | {#each collectionProperties as property (property.key)}
168 |
169 |
177 |
178 | {/each}
179 |
180 |
181 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/CollectionColumnTitle.svelte:
--------------------------------------------------------------------------------
1 |
8 |
9 |
10 |
14 |
15 |
{schema.name}
16 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/CollectionProperty.svelte:
--------------------------------------------------------------------------------
1 |
74 |
75 | {#if isValid}
76 |
77 | {#if schema.type === 'title'}
78 | {#if block && linkToTitlePage}
79 |
84 |
85 |
86 | {:else}
87 |
88 | {/if}
89 | {/if}
90 | {#if componentsMap[schema.type]}
91 |
101 | {/if}
102 |
103 | {/if}
104 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/CollectionPropertyCheckbox.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
14 | {schema.name}
15 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/CollectionPropertyFile.svelte:
--------------------------------------------------------------------------------
1 |
13 |
14 | {#each files as file, index (index)}
15 |
21 |
22 |
23 | {/each}
24 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/CollectionPropertyNumber.svelte:
--------------------------------------------------------------------------------
1 |
58 |
59 | {#if output}
60 |
61 | {:else }
62 |
63 | {/if}
--------------------------------------------------------------------------------
/src/routes/components/Collection/CollectionPropertyTime.svelte:
--------------------------------------------------------------------------------
1 |
29 |
30 | {value}
--------------------------------------------------------------------------------
/src/routes/components/Collection/CollectionPropteryFormula.svelte:
--------------------------------------------------------------------------------
1 |
28 |
29 | {content}
30 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/CollectionRow.svelte:
--------------------------------------------------------------------------------
1 |
46 |
47 |
48 |
49 | {#each propertyIds as propertyId(propertyId)}
50 | {@const schema = schemas[propertyId]}
51 |
52 |
53 |
54 |
55 |
63 |
64 |
65 | {/each}
66 |
67 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/CollectionViewGallery.svelte:
--------------------------------------------------------------------------------
1 |
27 |
28 |
29 |
30 |
31 | {#each blocks as blockItem(blockItem.blockId)}
32 |
41 | {/each}
42 |
43 |
44 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/CollectionViewTabs.svelte:
--------------------------------------------------------------------------------
1 |
10 |
11 |
12 | {#each viewIds as viewId (viewId)}
13 | {@const collectionView = $recordMapStore.collection_view[viewId]?.value}
14 |
27 | {/each}
28 |
29 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/ColletionPropertySelect.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 | {#each values as value, index (index)}
16 |
17 | {value}
18 |
19 | {/each}
20 |
--------------------------------------------------------------------------------
/src/routes/components/Collection/eval-formula.ts:
--------------------------------------------------------------------------------
1 | import type * as types from 'notion-types'
2 | import add from 'date-fns/add/index.js'
3 | import format from 'date-fns/format/index.js'
4 | import getDate from 'date-fns/getDate/index.js'
5 | import getDay from 'date-fns/getDay/index.js'
6 | import getHours from 'date-fns/getHours/index.js'
7 | import getMinutes from 'date-fns/getMinutes/index.js'
8 | import getMonth from 'date-fns/getMonth/index.js'
9 | import getYear from 'date-fns/getYear/index.js'
10 | import intervalToDuration from 'date-fns/intervalToDuration/index.js'
11 | import sub from 'date-fns/sub/index.js'
12 | import { getDateValue, getTextContent } from 'notion-utils'
13 |
14 | export interface EvalFormulaContext {
15 | properties: types.PropertyMap
16 | schema: types.CollectionPropertySchemaMap
17 |
18 | endDate?: boolean
19 | }
20 |
21 | /**
22 | * Evaluates a Notion formula expression to a result value.
23 | *
24 | * All built-in functions and operators are supported.
25 | *
26 | * NOTE: this needs a lot more testing, especially around covering all the different
27 | * function types and coercing different property values correctly.
28 | *
29 | * It does work for many of our test cases, however.
30 | *
31 | * @param formula - Formula to evaluate.
32 | * @param context - Collection context containing property schema and values.
33 | */
34 | export function evalFormula(
35 | formula: types.Formula,
36 | context: EvalFormulaContext
37 | ): types.FormulaResult {
38 | const { endDate, ...ctx } = context
39 |
40 | // TODO: coerce return type using `formula.return_type`
41 | switch (formula?.type) {
42 | case 'symbol':
43 | // TODO: this isn't documented anywhere but seen in the wild
44 | return formula.name === 'true'
45 |
46 | case 'constant': {
47 | const value = formula.value
48 | switch (formula.result_type) {
49 | case 'text':
50 | return value
51 | case 'number':
52 | return parseFloat(value)
53 | default:
54 | return value
55 | }
56 | }
57 |
58 | case 'property': {
59 | const value = ctx.properties[formula.id]
60 | const text = getTextContent(value)
61 |
62 | switch (formula.result_type) {
63 | case 'text':
64 | return text
65 |
66 | case 'number':
67 | return parseFloat(text)
68 |
69 | case 'boolean':
70 | // TODO: handle chceckbox properties
71 | if (typeof text === 'string') {
72 | return text === 'true'
73 | } else {
74 | return !!text
75 | }
76 |
77 | case 'date': {
78 | // console.log('date', text, value)
79 |
80 | const v = getDateValue(value)
81 | if (v) {
82 | if (endDate && v.end_date) {
83 | const date = new Date(v.end_date)
84 | return new Date(
85 | date.getUTCFullYear(),
86 | date.getUTCMonth(),
87 | date.getUTCDate()
88 | )
89 | } else {
90 | const date = new Date(v.start_date)
91 | return new Date(
92 | date.getUTCFullYear(),
93 | date.getUTCMonth(),
94 | date.getUTCDate()
95 | )
96 | }
97 | } else {
98 | return new Date(text)
99 | }
100 | }
101 |
102 | default:
103 | return text
104 | }
105 | }
106 |
107 | case 'operator':
108 | // All operators are exposed as functions, so we handle them the same
109 |
110 | // eslint-disable-next-line no-fallthrough
111 | case 'function':
112 | return evalFunctionFormula(formula, ctx)
113 |
114 | default:
115 | // console.log(formula)
116 | throw new Error(
117 | `invalid or unsupported formula "${(formula as any)?.type}`
118 | )
119 | }
120 | }
121 |
122 | /**
123 | * Evaluates a Notion formula function or operator expression.
124 | *
125 | * Note that all operators are also exposed as functions, so we handle them the same.
126 | *
127 | * @private
128 | */
129 | function evalFunctionFormula(
130 | formula: types.FunctionFormula | types.OperatorFormula,
131 | ctx: EvalFormulaContext
132 | ): types.FormulaResult {
133 | const args = formula?.args
134 |
135 | switch (formula.name) {
136 | // logic
137 | // ------------------------------------------------------------------------
138 |
139 | case 'and':
140 | return evalFormula(args[0], ctx) && evalFormula(args[1], ctx)
141 |
142 | case 'empty':
143 | return !evalFormula(args[0], ctx)
144 |
145 | case 'equal':
146 | // eslint-disable-next-line eqeqeq
147 | return evalFormula(args[0], ctx) == evalFormula(args[1], ctx)
148 |
149 | case 'if':
150 | return evalFormula(args[0], ctx)
151 | ? evalFormula(args[1], ctx)
152 | : evalFormula(args[2], ctx)
153 |
154 | case 'larger':
155 | return evalFormula(args[0], ctx) > evalFormula(args[1], ctx)
156 |
157 | case 'largerEq':
158 | return evalFormula(args[0], ctx) >= evalFormula(args[1], ctx)
159 |
160 | case 'not':
161 | return !evalFormula(args[0], ctx)
162 |
163 | case 'or':
164 | return evalFormula(args[0], ctx) || evalFormula(args[1], ctx)
165 |
166 | case 'smaller':
167 | return evalFormula(args[0], ctx) < evalFormula(args[1], ctx)
168 |
169 | case 'smallerEq':
170 | return evalFormula(args[0], ctx) <= evalFormula(args[1], ctx)
171 |
172 | case 'unequal':
173 | // eslint-disable-next-line eqeqeq
174 | return evalFormula(args[0], ctx) != evalFormula(args[1], ctx)
175 |
176 | // numeric
177 | // ------------------------------------------------------------------------
178 |
179 | case 'abs':
180 | return Math.abs(evalFormula(args[0], ctx) as number)
181 |
182 | case 'add': {
183 | const v0 = evalFormula(args[0], ctx)
184 | const v1 = evalFormula(args[1], ctx)
185 |
186 | if (typeof v0 === 'number') {
187 | return v0 + +v1
188 | } else if (typeof v0 === 'string') {
189 | return v0 + `${v1}`
190 | } else {
191 | // TODO
192 | return v0
193 | }
194 | }
195 |
196 | case 'cbrt':
197 | return Math.cbrt(evalFormula(args[0], ctx) as number)
198 |
199 | case 'ceil':
200 | return Math.ceil(evalFormula(args[0], ctx) as number)
201 |
202 | case 'divide':
203 | return (
204 | (evalFormula(args[0], ctx) as number) /
205 | (evalFormula(args[1], ctx) as number)
206 | )
207 |
208 | case 'exp':
209 | return Math.exp(evalFormula(args[0], ctx) as number)
210 |
211 | case 'floor':
212 | return Math.floor(evalFormula(args[0], ctx) as number)
213 |
214 | case 'ln':
215 | return Math.log(evalFormula(args[0], ctx) as number)
216 |
217 | case 'log10':
218 | return Math.log10(evalFormula(args[0], ctx) as number)
219 |
220 | case 'log2':
221 | return Math.log2(evalFormula(args[0], ctx) as number)
222 |
223 | case 'max': {
224 | const values = args.map((arg) => evalFormula(arg, ctx) as number)
225 | return values.reduce(
226 | (acc, value) => Math.max(acc, value),
227 | Number.NEGATIVE_INFINITY
228 | )
229 | }
230 |
231 | case 'min': {
232 | const values = args.map((arg) => evalFormula(arg, ctx) as number)
233 | return values.reduce(
234 | (acc, value) => Math.min(acc, value),
235 | Number.POSITIVE_INFINITY
236 | )
237 | }
238 |
239 | case 'mod':
240 | return (
241 | (evalFormula(args[0], ctx) as number) %
242 | (evalFormula(args[1], ctx) as number)
243 | )
244 |
245 | case 'multiply':
246 | return (
247 | (evalFormula(args[0], ctx) as number) *
248 | (evalFormula(args[1], ctx) as number)
249 | )
250 |
251 | case 'pow':
252 | return Math.pow(
253 | evalFormula(args[0], ctx) as number,
254 | evalFormula(args[1], ctx) as number
255 | )
256 |
257 | case 'round':
258 | return Math.round(evalFormula(args[0], ctx) as number)
259 |
260 | case 'sign':
261 | return Math.sign(evalFormula(args[0], ctx) as number)
262 |
263 | case 'sqrt':
264 | return Math.sqrt(evalFormula(args[0], ctx) as number)
265 |
266 | case 'subtract':
267 | return (
268 | (evalFormula(args[0], ctx) as number) -
269 | (evalFormula(args[1], ctx) as number)
270 | )
271 |
272 | case 'toNumber':
273 | return parseFloat(evalFormula(args[0], ctx) as string)
274 |
275 | case 'unaryMinus':
276 | return (evalFormula(args[0], ctx) as number) * -1
277 |
278 | case 'unaryPlus':
279 | return parseFloat(evalFormula(args[0], ctx) as string)
280 |
281 | // text
282 | // ------------------------------------------------------------------------
283 |
284 | case 'concat': {
285 | const values = args.map((arg) => evalFormula(arg, ctx) as number)
286 | return values.join('')
287 | }
288 |
289 | case 'contains':
290 | return (evalFormula(args[0], ctx) as string).includes(
291 | evalFormula(args[1], ctx) as string
292 | )
293 |
294 | case 'format': {
295 | const value = evalFormula(args[0], ctx)
296 |
297 | switch (typeof value) {
298 | case 'string':
299 | return value
300 |
301 | case 'object':
302 | if (value instanceof Date) {
303 | return format(value as Date, 'MMM d, YYY')
304 | } else {
305 | // shouldn't ever get here
306 | return `${value}`
307 | }
308 |
309 | case 'number':
310 | default:
311 | return `${value}`
312 | }
313 | }
314 |
315 | case 'join': {
316 | const [delimiterArg, ...restArgs] = args
317 | const delimiter = evalFormula(delimiterArg, ctx) as string
318 | const values = restArgs.map((arg) => evalFormula(arg, ctx) as number)
319 | return values.join(delimiter)
320 | }
321 |
322 | case 'length':
323 | return (evalFormula(args[0], ctx) as string).length
324 |
325 | case 'replace': {
326 | const value = evalFormula(args[0], ctx) as string
327 | const regex = evalFormula(args[1], ctx) as string
328 | const replacement = evalFormula(args[2], ctx) as string
329 | return value.replace(new RegExp(regex), replacement)
330 | }
331 |
332 | case 'replaceAll': {
333 | const value = evalFormula(args[0], ctx) as string
334 | const regex = evalFormula(args[1], ctx) as string
335 | const replacement = evalFormula(args[2], ctx) as string
336 | return value.replace(new RegExp(regex, 'g'), replacement)
337 | }
338 |
339 | case 'slice': {
340 | const value = evalFormula(args[0], ctx) as string
341 | const beginIndex = evalFormula(args[1], ctx) as number
342 | const endIndex = args[2]
343 | ? (evalFormula(args[2], ctx) as number)
344 | : value.length
345 | return value.slice(beginIndex, endIndex)
346 | }
347 |
348 | case 'test': {
349 | const value = evalFormula(args[0], ctx) as string
350 | const regex = evalFormula(args[1], ctx) as string
351 | return new RegExp(regex).test(value)
352 | }
353 |
354 | // date & time
355 | // ------------------------------------------------------------------------
356 |
357 | case 'date':
358 | return getDate(evalFormula(args[0], ctx) as Date)
359 |
360 | case 'dateAdd': {
361 | const date = evalFormula(args[0], ctx) as Date
362 | const number = evalFormula(args[1], ctx) as number
363 | const unit = evalFormula(args[1], ctx) as string
364 | return add(date, { [unit]: number })
365 | }
366 |
367 | case 'dateBetween': {
368 | const date1 = evalFormula(args[0], ctx) as Date
369 | const date2 = evalFormula(args[1], ctx) as Date
370 | const unit = evalFormula(args[1], ctx) as string
371 | return (
372 | intervalToDuration({
373 | start: date2,
374 | end: date1
375 | }) as any
376 | )[unit] as number
377 | }
378 |
379 | case 'dateSubtract': {
380 | const date = evalFormula(args[0], ctx) as Date
381 | const number = evalFormula(args[1], ctx) as number
382 | const unit = evalFormula(args[1], ctx) as string
383 | return sub(date, { [unit]: number })
384 | }
385 |
386 | case 'day':
387 | return getDay(evalFormula(args[0], ctx) as Date)
388 |
389 | case 'end':
390 | return evalFormula(args[0], { ...ctx, endDate: true }) as Date
391 |
392 | case 'formatDate': {
393 | const date = evalFormula(args[0], ctx) as Date
394 | const formatValue = (evalFormula(args[1], ctx) as string).replace(
395 | 'dddd',
396 | 'eeee'
397 | )
398 | return format(date, formatValue)
399 | }
400 |
401 | case 'fromTimestamp':
402 | return new Date(evalFormula(args[0], ctx) as number)
403 |
404 | case 'hour':
405 | return getHours(evalFormula(args[0], ctx) as Date)
406 |
407 | case 'minute':
408 | return getMinutes(evalFormula(args[0], ctx) as Date)
409 |
410 | case 'month':
411 | return getMonth(evalFormula(args[0], ctx) as Date)
412 |
413 | case 'now':
414 | return new Date()
415 |
416 | case 'start':
417 | return evalFormula(args[0], { ...ctx, endDate: false }) as Date
418 |
419 | case 'timestamp':
420 | return (evalFormula(args[0], ctx) as Date).getTime()
421 |
422 | case 'year':
423 | return getYear(evalFormula(args[0], ctx) as Date)
424 |
425 | default:
426 | // console.log(formula)
427 | throw new Error(
428 | `invalid or unsupported function formula "${(formula as any)?.type}`
429 | )
430 | }
431 | }
432 |
--------------------------------------------------------------------------------
/src/routes/components/Equation.svelte:
--------------------------------------------------------------------------------
1 |
15 | {#if value}
16 |
17 |
18 | {value}
19 |
20 |
21 | {/if}
22 |
--------------------------------------------------------------------------------
/src/routes/components/GracefulImage.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 | {#if userInfo}
16 |
17 | {/if}
18 |
--------------------------------------------------------------------------------
/src/routes/components/Header.svelte:
--------------------------------------------------------------------------------
1 |
88 |
89 |
121 |
--------------------------------------------------------------------------------
/src/routes/components/LiteYoutubeEmbed.svelte:
--------------------------------------------------------------------------------
1 |
26 |
27 |
28 |
29 | {
31 | iframeInitialized = true;
32 | }}
33 | class="notion-yt-lite {iframeInitialized ? 'notion-yt-initialized' : ''}"
34 | style={$$props.style}
35 | >
36 |

37 |
38 |
39 |
40 | {#if iframeInitialized}
41 |
55 |
--------------------------------------------------------------------------------
/src/routes/components/PageIcon.svelte:
--------------------------------------------------------------------------------
1 |
44 | {#if isBlockIcon && (iconUrl || icon)}
45 |
47 | {#if iconUrl}
48 |

49 | {:else}
50 |
55 | {icon}
56 |
57 | {/if}
58 |
59 |
60 | {/if}
61 |
62 |
63 |
--------------------------------------------------------------------------------
/src/routes/components/PageTitle.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 | {#if block}
18 | {#if isCollectionBlock && title}
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 | {/if}
27 | {#if !isCollectionBlock && title}
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 | {/if}
36 | {/if}
37 |
--------------------------------------------------------------------------------
/src/routes/components/Pdf.svelte:
--------------------------------------------------------------------------------
1 |
11 |
12 |
13 |
--------------------------------------------------------------------------------
/src/routes/components/RecursiveBlock.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 | {#if block}
18 |
19 | {#each block.content || [] as contentBlockId (contentBlockId)}
20 | {@const contentBlock = $recordMapStore.block[contentBlockId]?.value}
21 |
22 | {/each}
23 |
24 | {/if}
25 |
--------------------------------------------------------------------------------
/src/routes/components/Render.svelte:
--------------------------------------------------------------------------------
1 |
16 |
17 |
18 |
--------------------------------------------------------------------------------
/src/routes/components/Text.svelte:
--------------------------------------------------------------------------------
1 |
39 |
40 | {#each value as [text, decorations], index}
41 | {#if !decorations}
42 |
43 | {:else}
44 | {#each decorations as decorator}
45 |
56 | {/each}
57 | {/if}
58 | {/each}
59 |
--------------------------------------------------------------------------------
/src/routes/components/TextExternalLink.svelte:
--------------------------------------------------------------------------------
1 |
19 |
20 | {#if value}
21 | {#if linkType === 'u'}
22 |
23 |
24 | {/if}
25 | {#if linkedBlock}
26 |
27 |
28 |
29 | {/if}
30 | {/if}
--------------------------------------------------------------------------------
/src/routes/components/TextLink.svelte:
--------------------------------------------------------------------------------
1 |
30 |
31 | {text}
32 |
33 |
--------------------------------------------------------------------------------
/src/routes/components/TextPage.svelte:
--------------------------------------------------------------------------------
1 |
14 |
15 | {#if linkedBlock}
16 |
17 |
18 |
19 | {/if}
20 |
--------------------------------------------------------------------------------
/src/routes/components/TextPlain.svelte:
--------------------------------------------------------------------------------
1 |
12 |
13 | {#if type}
14 | {#if type === 'h'}
15 | {text}
16 | {:else if type === 'c'}
17 | {text}
18 | {:else if type === 'b'}
19 | {text}
20 | {:else if type === 'i'}
21 | {text}
22 | {:else if type === 's'}
23 | {text}
24 | {:else if type === '_'}
25 | {text}
26 | {:else if type === 'm'}
27 | {text}
28 | {:else if type === 'd'}
29 | {#if value?.type === 'date'}
30 | {formatDate(value?.start_date)}
31 | {:else if value?.type === 'daterange'}
32 | {`${formatDate(value.start_date)} → ${formatDate(value.end_date)}`}
33 | {:else }
34 | {text}
35 | {/if}
36 | {:else }
37 | {text}
38 | {/if}
39 |
40 | {:else }
41 | {#if text === ','}
42 |
43 | {:else}
44 | {@html text}
45 | {/if}
46 | {/if}
47 |
48 |
--------------------------------------------------------------------------------
/src/routes/prism-theme.css:
--------------------------------------------------------------------------------
1 | /* prism theme adjustments */
2 |
3 | .notion-code {
4 | background-color: rgba(249, 250, 251, 1);
5 | border: 1px solid rgba(229, 231, 235, 1);
6 | border-radius: 0.375rem;
7 | padding: 1.5em !important;
8 | }
9 |
10 | .dark-mode .notion-code {
11 | background-color: rgba(17, 24, 39, 1);
12 | border-color: rgba(55, 65, 81, 1);
13 | }
14 | .notion code {
15 | color: rgba(31, 41, 55, 1);
16 | border: 0 none !important;
17 | box-shadow: none !important;
18 | background: none !important;
19 | padding: 0 !important;
20 | }
21 | .dark-mode .notion code {
22 | color: rgba(229, 231, 235, 1);
23 | }
24 |
25 | .dark-mode .notion .notion-inline-code {
26 | background: rgb(71, 76, 80) !important;
27 | color: #ff4081;
28 | padding: 0.2em 0.4em !important;
29 | }
30 |
31 | .notion .notion-inline-code {
32 | color: #ff4081;
33 | background: rgba(127, 122, 107, 0.1) !important;
34 | padding: 0.2em 0.4em !important;
35 | }
36 |
37 | .token.cdata,
38 | .token.doctype,
39 | .token.prolog {
40 | color: rgba(55, 65, 81, 1);
41 | }
42 | .token.comment {
43 | color: #5b9b4c;
44 | }
45 | .dark-mode .token.cdata,
46 | .dark-mode .token.doctype,
47 | .dark-mode .token.prolog {
48 | color: rgba(209, 213, 219, 1);
49 | }
50 | .token.punctuation {
51 | color: rgba(55, 65, 81, 1);
52 | }
53 | .dark-mode .token.punctuation {
54 | color: rgba(209, 213, 219, 1);
55 | }
56 | .token.boolean,
57 | .token.constant,
58 | .token.deleted,
59 | .token.number,
60 | .token.property,
61 | .token.symbol,
62 | .token.tag {
63 | color: rgba(16, 185, 129, 1);
64 | }
65 | .token.attr-name,
66 | .token.builtin,
67 | .token.char,
68 | .token.inserted,
69 | .token.selector,
70 | .token.string {
71 | color: rgba(139, 92, 246, 1);
72 | }
73 | .language-css .token.string,
74 | .style .token.string,
75 | .token.entity,
76 | .token.operator,
77 | .token.url {
78 | color: rgba(245, 158, 11, 1);
79 | }
80 | .token.atrule,
81 | .token.attr-value,
82 | .token.keyword {
83 | color: rgba(59, 130, 246, 1);
84 | }
85 | .token.class-name,
86 | .token.function {
87 | color: rgba(236, 72, 153, 1);
88 | }
89 | .token.important,
90 | .token.regex,
91 | .token.variable {
92 | color: rgba(245, 158, 11, 1);
93 | }
94 | code[class*='language-'],
95 | pre[class*='language-'] {
96 | color: rgba(31, 41, 55, 1);
97 | }
98 | .dark-mode code[class*='language-'],
99 | .dark-mode pre[class*='language-'] {
100 | color: rgba(249, 250, 251, 1);
101 | }
102 | pre::-webkit-scrollbar {
103 | display: none;
104 | }
105 | pre {
106 | -ms-overflow-style: none;
107 | scrollbar-width: none;
108 | }
109 | .token.operator,
110 | .token.entity,
111 | .token.url,
112 | .token.variable {
113 | background: none;
114 | }
115 |
116 | pre[class*='language-'] > code {
117 | border-left: 0 none !important;
118 | box-shadow: none !important;
119 | background: none !important;
120 | }
121 |
122 | pre[class*='language-']:before,
123 | pre[class*='language-']:after {
124 | display: none !important;
125 | }
126 |
--------------------------------------------------------------------------------
/src/routes/store.ts:
--------------------------------------------------------------------------------
1 | import type { ExtendedRecordMap } from 'notion-types';
2 | import { writable, get } from 'svelte/store';
3 | import { parsePageId, uuidToId, getCanonicalPageId } from 'notion-utils';
4 | import type { Site } from '$lib/types';
5 | import { site } from '$lib/config';
6 | import { inversePageUrlOverrides } from '$lib/config';
7 | import { base } from '$app/paths';
8 | // export const site = writable();
9 |
10 | function createRecordMap() {
11 | const { subscribe, set, update } = writable();
12 |
13 | return {
14 | subscribe,
15 | update,
16 | init: (obj: ExtendedRecordMap) => set(obj)
17 | // 生成跳转的链接
18 | };
19 | }
20 |
21 | export const recordMapStore = createRecordMap();
22 |
23 | const uuid = true;
24 |
25 | export const actions = {
26 | mapPageUrl: (pageId = '') => {
27 | const pageUuid = parsePageId(pageId, { uuid: true });
28 |
29 | if (uuidToId(pageUuid) === site.rootNotionPageId) {
30 | return '/';
31 | } else {
32 | const cleanPageId = parsePageId(pageId, { uuid: false });
33 | if (!cleanPageId) {
34 | return '/';
35 | }
36 | const override = inversePageUrlOverrides[cleanPageId];
37 | if (override) {
38 | return `/${override}`;
39 | } else {
40 | return `${base ? base : ''}/${getCanonicalPageId(pageUuid, get(recordMapStore), { uuid })}`;
41 | }
42 | }
43 | }
44 | };
45 |
--------------------------------------------------------------------------------
/src/site.config.ts:
--------------------------------------------------------------------------------
1 | import { siteConfig } from './lib/site-config';
2 |
3 | export default siteConfig({
4 | rootNotionPageId: '5e19b09eec9e43c5b4c23031d41fea81',
5 |
6 | // if you want to restrict pages to a single notion workspace (optional)
7 | // (this should be a Notion ID; see the docs for how to extract this)
8 | rootNotionSpaceId: undefined,
9 |
10 | // basic site info (required)
11 | name: 'Svelte Notion Blog',
12 | domain: 'svelte-noiton.test.com',
13 | author: 'svelte-noiton',
14 |
15 | // open graph metadata (optional)
16 | description: 'svelte notion render',
17 |
18 | // default notion icon and cover images for site-wide consistency (optional)
19 | // page-specific values will override these site-wide defaults
20 | defaultPageIcon: null,
21 | defaultPageCover: null,
22 | defaultPageCoverPosition: 0.5,
23 |
24 | isSearchEnabled: undefined,
25 | // pageUrlOverrides: {
26 | // '/foo': '067dd719a912471ea9a3ac10710e7fdf',
27 | // '/bar': '0be6efce9daf42688f65c76b89f8eb27'
28 | // }
29 | pageUrlOverrides: undefined,
30 |
31 | // whether to use the default notion navigation style or a custom one with links to
32 | // important pages
33 | navigationStyle: 'custom',
34 | // navigationStyle: 'custom',
35 | // navigationLinks: [
36 | // {
37 | // title: 'About',
38 | // pageId: 'f1199d37579b41cbabfc0b5174f4256a'
39 | // },
40 | // {
41 | // title: 'Contact',
42 | // pageId: '6a29ebcb935a4f0689fe661ab5f3b8d1'
43 | // }
44 | // ]
45 |
46 | });
47 |
--------------------------------------------------------------------------------
/static/.nojekyll:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiodot/svelte-notion/c17d3c2ed6017c6143a46eb802dbd007f890df56/static/.nojekyll
--------------------------------------------------------------------------------
/static/404.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 |
7 |
8 | Document
9 |
10 |
11 |
12 | 404 Not Found
13 |
14 |
15 |
--------------------------------------------------------------------------------
/static/favicon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/tiodot/svelte-notion/c17d3c2ed6017c6143a46eb802dbd007f890df56/static/favicon.png
--------------------------------------------------------------------------------
/static/robots.txt:
--------------------------------------------------------------------------------
1 | # https://www.robotstxt.org/robotstxt.html
2 | User-agent: *
3 | Disallow:
4 |
--------------------------------------------------------------------------------
/svelte.config.js:
--------------------------------------------------------------------------------
1 | import adapter from '@sveltejs/adapter-static';
2 | import { vitePreprocess } from '@sveltejs/kit/vite';
3 | const dev = process.argv.includes('dev');
4 | /** @type {import('@sveltejs/kit').Config} */
5 | const config = {
6 | compilerOptions: {
7 | css: "external",
8 | },
9 | // Consult https://kit.svelte.dev/docs/integrations#preprocessors
10 | // for more information about preprocessors
11 | preprocess: vitePreprocess(),
12 |
13 | kit: {
14 | // adapter-auto only supports some environments, see https://kit.svelte.dev/docs/adapter-auto for a list.
15 | // If your environment is not supported or you settled on a specific environment, switch out the adapter.
16 | // See https://kit.svelte.dev/docs/adapters for more information about adapters.
17 | adapter: adapter({
18 | fallback: '404.html',
19 | }),
20 | paths: {
21 | base: dev ? '' : '/svelte-notion',
22 | relative: false,
23 | }
24 | }
25 | };
26 |
27 | export default config;
28 |
--------------------------------------------------------------------------------
/tsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "extends": "./.svelte-kit/tsconfig.json",
3 | "compilerOptions": {
4 | "allowJs": true,
5 | "checkJs": true,
6 | "esModuleInterop": true,
7 | "forceConsistentCasingInFileNames": true,
8 | "resolveJsonModule": true,
9 | "skipLibCheck": true,
10 | "sourceMap": true,
11 | "strict": true
12 | }
13 | // Path aliases are handled by https://kit.svelte.dev/docs/configuration#alias
14 | //
15 | // If you want to overwrite includes/excludes, make sure to copy over the relevant includes/excludes
16 | // from the referenced tsconfig.json - TypeScript does not merge them in
17 | }
18 |
--------------------------------------------------------------------------------
/vite.config.ts:
--------------------------------------------------------------------------------
1 | import { sveltekit } from '@sveltejs/kit/vite';
2 | import { defineConfig } from 'vite';
3 |
4 | export default defineConfig({
5 | plugins: [sveltekit()],
6 | define: {
7 | "process.env": {
8 | DEBUG: true,
9 | }
10 | }
11 | });
12 |
--------------------------------------------------------------------------------