├── .eslintrc.js
├── .gitignore
├── .gitpod.yml
├── .prettierignore
├── .prettierrc
├── LICENSE
├── README.md
├── blog.config.js
├── components
├── Container.js
├── Footer.js
├── Header.js
├── ImageFallback.js
├── NotePost.js
├── Scripts.js
└── ThemeSwitcher.js
├── dev.sh
├── jsconfig.json
├── lib
└── getBlocksMaps.js
├── next.config.js
├── package.json
├── pages
├── _app.js
├── _document.js
├── api
│ ├── apicache.js
│ ├── htmlrewrite.js
│ └── jsrewrite.js
└── archive.js
├── postcss.config.js
├── public
├── favicon.ico
├── favicon.svg
└── secret_preview.png
├── styles
└── globals.css
├── tailwind.config.js
├── vercel.json
└── yarn.lock
/.eslintrc.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | env: {
3 | browser: true,
4 | es2021: true,
5 | node: true
6 | },
7 | extends: ['plugin:react/recommended', 'next', 'standard'],
8 | parserOptions: {
9 | ecmaFeatures: {
10 | jsx: true
11 | },
12 | ecmaVersion: 12,
13 | sourceType: 'module'
14 | },
15 | plugins: ['react'],
16 | settings: {
17 | react: {
18 | version: 'detect'
19 | }
20 | },
21 | rules: {
22 | 'react/prop-types': 'off',
23 | 'space-before-function-paren': 'off',
24 | 'multiline-ternary': 'off'
25 | },
26 | globals: {
27 | React: true
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
2 |
3 | # dependencies
4 | /node_modules
5 | /.pnp
6 | .pnp.js
7 |
8 | # testing
9 | /coverage
10 |
11 | # next.js
12 | /.next/
13 | /out/
14 |
15 | # production
16 | /build
17 |
18 | # misc
19 | .DS_Store
20 | *.pem
21 |
22 | # debug
23 | npm-debug.log*
24 | yarn-debug.log*
25 | yarn-error.log*
26 |
27 | # local env files
28 | .env
29 | .env.local
30 | .env.development.local
31 | .env.test.local
32 | .env.production.local
33 |
34 | # vercel
35 | .vercel
36 | public/robots.txt
37 | public/sitemap.xml
38 |
--------------------------------------------------------------------------------
/.gitpod.yml:
--------------------------------------------------------------------------------
1 | # This configuration file was automatically generated by Gitpod.
2 | # Please adjust to your needs (see https://www.gitpod.io/docs/config-gitpod-file)
3 | # and commit this file to your remote git repository to share the goodness with others.
4 |
5 | tasks:
6 | - init: yarn install && yarn run build
7 | command: yarn dev
8 |
9 |
10 |
--------------------------------------------------------------------------------
/.prettierignore:
--------------------------------------------------------------------------------
1 | build/
2 | dist/
3 | node_modules/
4 | .next/
5 | .vercel/
6 |
7 | .demo/
8 | .renderer/
9 | pnpm-lock.yaml
--------------------------------------------------------------------------------
/.prettierrc:
--------------------------------------------------------------------------------
1 | {
2 | "singleQuote": true,
3 | "jsxSingleQuote": true,
4 | "semi": false,
5 | "useTabs": false,
6 | "tabWidth": 2,
7 | "bracketSpacing": true,
8 | "arrowParens": "always",
9 | "trailingComma": "none"
10 | }
11 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | MIT License
2 |
3 | Copyright (c) 2022 ZuoLan
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 | # Next-Craft
2 |
3 | Customize your Craft.do share pages domains and manage everything in Craft.do one page!
4 |
5 | ## Demo
6 |
7 | [Next Craft](https://next-craft.vercel.app/)
8 |
9 | [My Blog Notes Page](https://zuolan.me/notes)
10 |
11 | ## Quick Start
12 |
13 | 1. **Duplicate** [this demo config page](https://www.craft.do/s/kQtcWqkv98cHhB) to your space, then **share** your config page.
14 | 2. Star and Fork [Next-Craft](https://github.com/izuolan/next-craft) repo.
15 | 3. Replace your **craftConfigShareUrl** in `blog.config.js` file.
16 | 4. Deploy on [Vercel](https://vercel.com/) or other Hosting Providers.
17 |
18 | More details in [Next Craft](https://next-craft.vercel.app/).
19 |
20 | ## License
21 |
22 | The MIT License.
23 |
--------------------------------------------------------------------------------
/blog.config.js:
--------------------------------------------------------------------------------
1 | const BLOG = {
2 | lang: 'en-US', // ['en-US', 'zh-CN', 'zh-HK', 'zh-TW']
3 | appearance: 'auto', // ['light', 'dark', 'auto'],
4 | font: 'sans-serif', // ['sans-serif', 'serif']
5 | lightBackground: '#FFFFFF', // use hex value, don't forget '#' e.g #fffefc
6 | darkBackground: '#222222', // use hex value, don't forget '#'
7 | autoCollapsedNavBar: false, // The automatically collapsed navigation bar
8 | craftConfigShareUrl: process.env.CRAFT_CONFIG_SHARE_URL
9 | ? process.env.CRAFT_CONFIG_SHARE_URL
10 | : 'https://www.craft.do/s/kQtcWqkv98cHhB', // The link to share your craft config
11 | seo: {
12 | keywords: ['Blog', 'Craft.do', 'Craft Docs', 'Next.js', 'TailwindCSS']
13 | },
14 | analytics: {
15 | provider: '', // Currently support Umami, fill with 'umami' to enable or leave it empty to disable it.
16 | umamiConfig: {
17 | scriptUrl: '', // The url of your Umami script
18 | websiteId: '' // The website id of your Umami instance
19 | }
20 | },
21 | isProd: process.env.VERCEL_ENV === 'production' // distinguish between development and production environment (ref: https://vercel.com/docs/environment-variables#system-environment-variables)
22 | }
23 | module.exports = BLOG
24 |
--------------------------------------------------------------------------------
/components/Container.js:
--------------------------------------------------------------------------------
1 | import Header from '@/components/Header'
2 | import Footer from '@/components/Footer'
3 | import BLOG from '@/blog.config'
4 | import Head from 'next/head'
5 | import PropTypes from 'prop-types'
6 |
7 | const Container = ({ children, siteConfigObj, ...customMeta }) => {
8 | const meta = {
9 | title: siteConfigObj['Site Name'],
10 | description: siteConfigObj['Site Description'],
11 | link: siteConfigObj['Site Link'],
12 | type: 'website',
13 | ...customMeta
14 | }
15 | return (
16 |
17 |
18 |
{meta.title}
19 | {/*
*/}
20 |
21 |
22 | {BLOG.seo.googleSiteVerification && (
23 |
27 | )}
28 | {BLOG.seo.keywords && (
29 |
30 | )}
31 |
32 |
33 |
34 |
35 |
39 | {/*
*/}
51 |
52 |
53 |
54 |
55 | {/*
*/}
67 |
68 |
73 |
74 |
75 | {children}
76 |
77 |
78 |
79 |
80 | )
81 | }
82 |
83 | Container.propTypes = {
84 | children: PropTypes.node
85 | }
86 |
87 | export default Container
88 |
--------------------------------------------------------------------------------
/components/Footer.js:
--------------------------------------------------------------------------------
1 | const Footer = ({ siteConfigObj }) => {
2 | return (
3 |
13 | )
14 | }
15 |
16 | export default Footer
17 |
--------------------------------------------------------------------------------
/components/Header.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState, useRef } from 'react'
2 | import Link from 'next/link'
3 | import BLOG from '@/blog.config'
4 | import { useRouter } from 'next/router'
5 | import {
6 | HomeIcon,
7 | ArchiveIcon,
8 | UserIcon,
9 | MenuIcon
10 | } from '@heroicons/react/outline'
11 | import ThemeSwitcher from './ThemeSwitcher.js'
12 |
13 | const NavBar = ({ siteConfigObj }) => {
14 | const router = useRouter()
15 | const [showMenu, setShowMenu] = useState(false)
16 |
17 | let activeMenu = ''
18 | if (router.query.slug) {
19 | activeMenu = '/' + router.query.slug
20 | } else {
21 | activeMenu = router.pathname
22 | }
23 |
24 | const links = [
25 | {
26 | id: 0,
27 | name: siteConfigObj['Home Menu Text'],
28 | to: '/',
29 | icon: ,
30 | show: true
31 | },
32 | {
33 | id: 1,
34 | name: siteConfigObj['Archive Menu Text'],
35 | to: '/archive',
36 | icon: ,
37 | show: true
38 | },
39 | {
40 | id: 2,
41 | name: siteConfigObj['About Menu Text'],
42 | to: '/about',
43 | icon: ,
44 | show: true
45 | }
46 | ]
47 | return (
48 |
49 |
63 |
64 |
65 |
66 |
67 |
74 | {showMenu && (
75 |
90 | )}
91 |
92 |
93 | )
94 | }
95 |
96 | const Header = ({ navBarTitle, siteConfigObj }) => {
97 | const [showTitle, setShowTitle] = useState(false)
98 | const useSticky = !BLOG.autoCollapsedNavBar
99 | const navRef = useRef(null)
100 | const sentinalRef = useRef([])
101 | const handler = ([entry]) => {
102 | if (navRef && navRef.current && useSticky) {
103 | if (!entry.isIntersecting && entry !== undefined) {
104 | navRef.current?.classList.add('sticky-nav-full')
105 | } else {
106 | navRef.current?.classList.remove('sticky-nav-full')
107 | }
108 | } else {
109 | navRef.current?.classList.add('remove-sticky')
110 | }
111 | }
112 | useEffect(() => {
113 | window.addEventListener('scroll', () => {
114 | if (window.pageYOffset > 100) {
115 | setShowTitle(true)
116 | } else {
117 | setShowTitle(false)
118 | }
119 | })
120 |
121 | const obvserver = new window.IntersectionObserver(handler)
122 | obvserver.observe(sentinalRef.current)
123 | // Don't touch this, I have no idea how it works XD
124 | // return () => {
125 | // if (sentinalRef.current) obvserver.unobserve(sentinalRef.current)
126 | // }
127 | // eslint-disable-next-line react-hooks/exhaustive-deps
128 | }, [sentinalRef])
129 | return (
130 | <>
131 |
132 |
151 | >
152 | )
153 | }
154 |
155 | export default Header
156 |
--------------------------------------------------------------------------------
/components/ImageFallback.js:
--------------------------------------------------------------------------------
1 | import Image from 'next/image'
2 | import { useEffect, useState } from 'react'
3 |
4 | export default function ImageFallback({ src, fallbackSrc, alt, ...rest }) {
5 | const [imgSrc, setImgSrc] = useState(src)
6 |
7 | useEffect(() => {
8 | setImgSrc(src)
9 | }, [src])
10 |
11 | return (
12 | {
17 | if (result.naturalWidth === 0) {
18 | // Broken image
19 | setImgSrc(fallbackSrc)
20 | }
21 | }}
22 | onError={() => {
23 | setImgSrc(fallbackSrc)
24 | }}
25 | />
26 | )
27 | }
28 |
--------------------------------------------------------------------------------
/components/NotePost.js:
--------------------------------------------------------------------------------
1 | import Link from 'next/link'
2 | import ImageFallback from './ImageFallback.js'
3 |
4 | const NotePost = ({ note }) => {
5 | const craftSlug = note.url.slice(23)
6 | return (
7 |
8 |
9 |
16 |
17 |
18 | {note.title}
19 |
20 |
21 |
22 | )
23 | }
24 |
25 | export default NotePost
26 |
--------------------------------------------------------------------------------
/components/Scripts.js:
--------------------------------------------------------------------------------
1 | import Script from 'next/script'
2 | import BLOG from '@/blog.config'
3 |
4 | const Scripts = () => (
5 | <>
6 | {BLOG.analytics.provider === 'umami' && (
7 |
12 | )}
13 | {/* {BLOG.autoCollapsedNavBar === true && (
14 |
29 | )} */}
30 | >
31 | )
32 |
33 | export default Scripts
34 |
--------------------------------------------------------------------------------
/components/ThemeSwitcher.js:
--------------------------------------------------------------------------------
1 | import { useEffect, useState } from 'react'
2 | import { SunIcon, MoonIcon } from '@heroicons/react/outline'
3 | import { useTheme } from 'next-themes'
4 |
5 | const ThemeSwitcher = () => {
6 | const { theme, setTheme } = useTheme()
7 | const [hasMounted, setHasMounted] = useState(false)
8 |
9 | useEffect(() => {
10 | setHasMounted(true)
11 | }, [])
12 | return (
13 | <>
14 |
29 | >
30 | )
31 | }
32 |
33 | export default ThemeSwitcher
34 |
--------------------------------------------------------------------------------
/dev.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 | kill $(ps -ef | grep -v grep | grep yarn | awk '{print $2}') 2>/dev/null
3 | kill $(ps -ef | grep -v grep | grep next | awk '{print $2}') 2>/dev/null
4 | yarn dev
5 |
--------------------------------------------------------------------------------
/jsconfig.json:
--------------------------------------------------------------------------------
1 | {
2 | "compilerOptions": {
3 | "baseUrl": ".",
4 | "paths": {
5 | "@/*": ["./*"],
6 | "@/components/*": ["components/*"],
7 | "@/lib/*": ["lib/*"],
8 | "@/styles/*": ["styles/*"]
9 | }
10 | }
11 | }
12 |
--------------------------------------------------------------------------------
/lib/getBlocksMaps.js:
--------------------------------------------------------------------------------
1 | import BLOG from '@/blog.config'
2 |
3 | // 从 Config 页面的 API 获取两个表格的内容, 并处理成两个 json 返回给 htmlrewrite.js
4 | export async function getBlocksMaps() {
5 | const craftConfigSecret = BLOG.craftConfigShareUrl.slice(23)
6 | const craftConfigApiUrl = 'https://www.craft.do/api/share/' + craftConfigSecret
7 | const init = {
8 | headers: {
9 | 'content-type': 'application/json;charset=UTF-8'
10 | }
11 | }
12 | const configResponse = await fetch(craftConfigApiUrl, init)
13 | const responseJson = await configResponse.json()
14 |
15 | const tables = getAllTables(responseJson)
16 | let pagesMap = []
17 | let siteConfigMap = []
18 |
19 | pagesMap.push(tables[0].cells)
20 | siteConfigMap.push(tables[1].cells)
21 | pagesMap = pagesMap[0]
22 | siteConfigMap = siteConfigMap[0]
23 |
24 | // 转换成对象之后方便查询
25 | const responseObj = {}
26 | responseJson.blocks.forEach(function (v) {
27 | responseObj[v.id] = v
28 | })
29 |
30 | const pagesJson = parsePagesMap(responseObj, pagesMap)
31 | const siteConfigObj = parseSiteConfigMap(responseObj, siteConfigMap)
32 | return { pagesJson, siteConfigObj }
33 | }
34 |
35 | // 获取两个表格的 pluginData 内容, 可以得知单元格之间的关系
36 | function getAllTables(responseJson) {
37 | const pageBlocksLength = responseJson.blocks.length
38 | const tables = []
39 |
40 | for (let i = 0; i < pageBlocksLength; i = i + 1) {
41 | try {
42 | // 判断每一个块的类型, 找出 table 的块, 以便获取表格的 cell 关系
43 | const blockType = responseJson.blocks[i].type
44 | if (blockType === 'table') {
45 | // 获取表格里的 pluginData, 这里面有表格 cell 的关系
46 | tables.push(JSON.parse(responseJson.blocks[i].pluginData))
47 | }
48 | } catch (error) {
49 | console.log('craft.js error: ', error)
50 | }
51 | }
52 | return tables
53 | }
54 |
55 | // 处理 页面表格
56 | function parsePagesMap(responseObj, tableMap) {
57 | const mapJson = []
58 | let titleColumId, craftUrlColumId, slugColumId
59 | for (let i = 0; i < tableMap.length; i++) {
60 | const blockId = tableMap[i].blockId
61 | const content = responseObj[blockId].content
62 | const rowId = tableMap[i].rowId
63 | const columnId = tableMap[i].columnId
64 | if (content === 'Title') {
65 | titleColumId = columnId
66 | } else if (content === 'Craft Share URL') {
67 | craftUrlColumId = columnId
68 | } else if (content === 'Slug') {
69 | slugColumId = columnId
70 | }
71 | const tpl = `{"content":"${content}","blockId":"${blockId}","rowId":"${rowId}","columnId":"${columnId}"}`
72 | mapJson.push(JSON.parse(tpl))
73 | }
74 | // console.log(mapJson)
75 |
76 | const pagesJson = getPagesJson(mapJson, titleColumId, craftUrlColumId, slugColumId)
77 | return pagesJson
78 | }
79 |
80 | // 获得最终可用的 页面 json
81 | function getPagesJson(mapJson, titleColumId, craftUrlColumId, slugColumId) {
82 | const pagesJson = []
83 | for (let i = 0; i < mapJson.length; i++) {
84 | if (mapJson[i].columnId === titleColumId) {
85 | const nowRowId = mapJson[i].rowId
86 | let url, slug
87 |
88 | for (let j = 0; j < mapJson.length; j++) {
89 | if (mapJson[j].rowId === nowRowId && mapJson[j].columnId === craftUrlColumId) {
90 | url = mapJson[j].content
91 | }
92 | if (mapJson[j].rowId === nowRowId && mapJson[j].columnId === slugColumId) {
93 | slug = mapJson[j].content
94 | }
95 | }
96 |
97 | const title = mapJson[i].content
98 | const tpl = `{"title":"${title}","url":"${url}","slug":"${slug}"}`
99 | pagesJson.push(JSON.parse(tpl))
100 | }
101 | }
102 | return pagesJson
103 | }
104 |
105 | // 处理 设置表格
106 | function parseSiteConfigMap(responseObj, tableMap) {
107 | const mapJson = []
108 | let configColumId, valueColumId
109 | for (let i = 0; i < tableMap.length; i++) {
110 | const blockId = tableMap[i].blockId
111 | const content = responseObj[blockId].content
112 | const rowId = tableMap[i].rowId
113 | const columnId = tableMap[i].columnId
114 | if (content === 'Setting Name') {
115 | configColumId = columnId
116 | } else if (content === 'Value') {
117 | valueColumId = columnId
118 | }
119 | const tpl = `{"content":"${content}","blockId":"${blockId}","rowId":"${rowId}","columnId":"${columnId}"}`
120 | mapJson.push(JSON.parse(tpl))
121 | }
122 | // console.log(mapJson)
123 |
124 | const siteConfigObj = getSiteConfigObj(mapJson, configColumId, valueColumId)
125 | return siteConfigObj
126 | }
127 |
128 | // 获得最终可用的 设置 json
129 | function getSiteConfigObj(mapJson, configColumId, valueColumId) {
130 | const siteConfigJson = []
131 | for (let i = 0; i < mapJson.length; i++) {
132 | if (mapJson[i].columnId === configColumId) {
133 | const nowRowId = mapJson[i].rowId
134 | let value
135 |
136 | for (let j = 0; j < mapJson.length; j++) {
137 | if (mapJson[j].rowId === nowRowId && mapJson[j].columnId === valueColumId) {
138 | value = mapJson[j].content
139 | }
140 | }
141 |
142 | const key = mapJson[i].content
143 | const tpl = `{"key":"${key}","value":"${value}"}`
144 | siteConfigJson.push(JSON.parse(tpl))
145 | }
146 | }
147 | const siteConfigObj = {}
148 | siteConfigJson.forEach(function (v) {
149 | siteConfigObj[v.key] = v.value
150 | })
151 | return siteConfigObj
152 | }
153 |
--------------------------------------------------------------------------------
/next.config.js:
--------------------------------------------------------------------------------
1 | module.exports = {
2 | webpack5: true,
3 | eslint: {
4 | dirs: ['components', 'lib', 'pages']
5 | },
6 | webpack: (config, { dev, isServer }) => {
7 | // Replace React with Preact only in client production build
8 | if (!dev && !isServer) {
9 | Object.assign(config.resolve.alias, {
10 | react: 'preact/compat',
11 | 'react-dom/test-utils': 'preact/test-utils',
12 | 'react-dom': 'preact/compat'
13 | })
14 | }
15 | return config
16 | },
17 | i18n: {
18 | locales: ['en', 'zh'],
19 | defaultLocale: 'en',
20 | localeDetection: false
21 | },
22 | images: {
23 | domains: ['api.craft.do'],
24 | },
25 | async rewrites() {
26 | return [
27 | {
28 | source: '/',
29 | destination: '/api/htmlrewrite?pathname=index',
30 | },
31 | {
32 | source: '/b/:slug*',
33 | destination: '/api/htmlrewrite?pathname=index&slug=/b/:slug*',
34 | },
35 | {
36 | source: '/x/:slug*',
37 | destination: '/api/htmlrewrite?pathname=index&slug=/x/:slug*',
38 | },
39 | {
40 | source: '/api/:slug*',
41 | destination: 'https://www.craft.do/api/:slug*',
42 | },
43 | {
44 | source: '/share/static/js/:slug*',
45 | destination: '/api/jsrewrite?url=https://www.craft.do/share/static/js/:slug*',
46 | },
47 | {
48 | source: '/share/static/css/:slug*',
49 | destination: 'https://www.craft.do/share/static/css/:slug*',
50 | },
51 | {
52 | source: '/share/static/fonts/:slug*',
53 | destination: 'https://www.craft.do/share/static/fonts/:slug*',
54 | },
55 | {
56 | source: '/share/static/media/:slug*',
57 | destination: 'https://www.craft.do/share/static/media/:slug*',
58 | },
59 | {
60 | source: '/share/static/craft.webmanifest',
61 | destination: 'https://www.craft.do/share/static/craft.webmanifest',
62 | },
63 | {
64 | source: '/assets/js/analytics2.js',
65 | destination: 'https://www.craft.do/404',
66 | },
67 | {
68 | source: '/:pathname*',
69 | destination: '/api/htmlrewrite?pathname=:pathname*',
70 | }
71 | ]
72 | }
73 | }
74 |
--------------------------------------------------------------------------------
/package.json:
--------------------------------------------------------------------------------
1 | {
2 | "name": "next-craft",
3 | "version": "2.0.0",
4 | "homepage": "https://next-craft.vercel.app",
5 | "license": "MIT",
6 | "repository": {
7 | "type": "git",
8 | "url": "https://github.com/izuolan/next-craft.git"
9 | },
10 | "author": {
11 | "name": "Zuo Lan",
12 | "email": "i@zuolan.me",
13 | "url": "https://zuolan.me"
14 | },
15 | "scripts": {
16 | "dev": "next dev",
17 | "build": "next build",
18 | "start": "next start",
19 | "format": "prettier --write .",
20 | "lint": "next lint"
21 | },
22 | "dependencies": {
23 | "next": "^12.1.0",
24 | "next-themes": "^0.1.1",
25 | "preact": "^10.5.15",
26 | "react": "^17.0.2",
27 | "react-dom": "^17.0.2"
28 | },
29 | "devDependencies": {
30 | "@heroicons/react": "^1.0.5",
31 | "@types/react": "^17.0.39",
32 | "autoprefixer": "^10.4.0",
33 | "eslint": "<8.0.0",
34 | "eslint-config-next": "^12.0.3",
35 | "eslint-config-standard": "^16.0.2",
36 | "eslint-plugin-import": "^2.25.2",
37 | "eslint-plugin-node": "^11.1.0",
38 | "eslint-plugin-promise": "^5.1.1",
39 | "eslint-plugin-react": "^7.26.1",
40 | "postcss": "^8.4.6",
41 | "prettier": "^2.5.1",
42 | "tailwindcss": "^3.0.23"
43 | },
44 | "bugs": {
45 | "url": "https://github.com/izuolan/next-craft/issues",
46 | "email": "i@zuolan.me"
47 | }
48 | }
49 |
--------------------------------------------------------------------------------
/pages/_app.js:
--------------------------------------------------------------------------------
1 | import '@/styles/globals.css'
2 | import Scripts from '@/components/Scripts'
3 | import { ThemeProvider } from 'next-themes'
4 |
5 | function MyApp({ Component, pageProps }) {
6 | return (
7 | <>
8 |
9 |
10 |
11 |
12 | >
13 | )
14 | }
15 |
16 | export default MyApp
17 |
--------------------------------------------------------------------------------
/pages/_document.js:
--------------------------------------------------------------------------------
1 | import Document, { Html, Head, Main, NextScript } from 'next/document'
2 | import BLOG from '@/blog.config'
3 |
4 | class MyDocument extends Document {
5 | static async getInitialProps(ctx) {
6 | const initialProps = await Document.getInitialProps(ctx)
7 | return { ...initialProps }
8 | }
9 |
10 | render() {
11 | return (
12 |
13 |
14 |
15 |
16 | {BLOG.appearance === 'auto' ? (
17 | <>
18 |
23 |
28 | >
29 | ) : (
30 |
38 | )}
39 |
40 |
41 |
42 |
43 |
44 |
45 | )
46 | }
47 | }
48 |
49 | export default MyDocument
50 |
--------------------------------------------------------------------------------
/pages/api/apicache.js:
--------------------------------------------------------------------------------
1 | // const fetch = require('node-fetch')
2 |
3 | module.exports = async (req, res) => {
4 | let { url } = req.query
5 | res.setHeader('Access-Control-Allow-Credentials', true)
6 | res.setHeader('Access-Control-Allow-Origin', '*')
7 | res.setHeader('Access-Control-Allow-Methods', 'GET,OPTIONS,POST,PUT')
8 | res.setHeader('Content-Type', 'application/json; charset=utf-8')
9 | res.setHeader(
10 | 'Access-Control-Allow-Headers',
11 | 'X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version'
12 | )
13 | if (req.query && req.query.url) {
14 | url = 'https://www.craft.do/api/' + req.query.url
15 | }
16 |
17 | try {
18 | const response = await fetch(url)
19 | res.send(await response.json())
20 |
21 | // const originResText = await response.text()
22 | // const modifyResText = originResText
23 | // .replace(/5A942651-8A73-49C7-9B36-0FD047A6D6EC/g, 'test-page')
24 | // .replace(/craftdocs:\/\/open\?blockId=/g, 'https://zuolan.me/notes/page/')
25 | // .replace(/&spaceId=48c91199-cb47-f359-8399-2a12d07b0b02/g, '')
26 | // console.log(modifyResText.toString())
27 | // res.send(await modifyResText.toString())
28 | } catch (e) {
29 | res.send(e)
30 | }
31 | }
32 |
--------------------------------------------------------------------------------
/pages/api/htmlrewrite.js:
--------------------------------------------------------------------------------
1 | import { getBlocksMaps } from '@/lib/getBlocksMaps'
2 | import BLOG from '@/blog.config'
3 |
4 | async function getBlockItem(path) {
5 | const { pagesJson, siteConfigObj } = await getBlocksMaps()
6 |
7 | for (let i = 0; i < pagesJson.length; i++) {
8 | const blockItem = pagesJson[i]
9 | if (path === blockItem.slug) {
10 | return { blockItem, siteConfigObj }
11 | }
12 | }
13 | return { blockItem: null, siteConfigObj }
14 | }
15 |
16 | module.exports = async (req, res) => {
17 | // const { pathname, slug } = req.query
18 | const { pathname } = req.query
19 | let realPath
20 | if (pathname.includes('/b/')) {
21 | realPath = pathname.split('/b/')[0]
22 | } else if (pathname.includes('/x/')) {
23 | realPath = pathname.split('/x/')[0]
24 | } else {
25 | realPath = pathname
26 | }
27 | // console.log('realPath: ', realPath)
28 | // console.log('slug: ', slug)
29 |
30 | const { blockItem, siteConfigObj } = await getBlockItem(realPath)
31 | if (blockItem === null) {
32 | res.statusCode = 404
33 | res.end(
34 | 'Notes Not Found, Make sure you have the correct pathname and check your Craft.do setting page.'
35 | )
36 | return
37 | }
38 | const craftUrl = blockItem.url
39 | // console.log('htmlrewrite craftUrl: ', craftUrl)
40 |
41 | const bodyStr = `
42 |
43 |
44 |
49 |
50 |
51 |
${siteConfigObj['Site Name']}
52 |
53 |
67 |
68 |
79 |
80 |
81 | `
82 |
83 | const response = await fetch(craftUrl)
84 | const originResText = await response.text()
85 | const modifyResText = originResText
86 | .replace('', '')
87 | .replace(
88 | '',
89 | ''
90 | )
91 | .replace(
92 | '',
93 | ''
94 | )
95 | .replace(
96 | '',
97 | ''
98 | )
99 | .replace(
100 | '',
101 | ''
102 | )
103 | .replace(
104 | '',
105 | ''
106 | )
107 | .replace('', bodyStr + '