create-noxion 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +38 -0
- package/bin/index.js +3 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +115 -0
- package/dist/index.js.map +1 -0
- package/dist/scaffold.d.ts +16 -0
- package/dist/scaffold.d.ts.map +1 -0
- package/dist/scaffold.js +42 -0
- package/dist/scaffold.js.map +1 -0
- package/dist/templates/nextjs/.env.example +8 -0
- package/dist/templates/nextjs/app/[slug]/page.tsx +63 -0
- package/dist/templates/nextjs/app/api/revalidate/route.ts +12 -0
- package/dist/templates/nextjs/app/globals.css +53 -0
- package/dist/templates/nextjs/app/home-content.tsx +58 -0
- package/dist/templates/nextjs/app/layout.tsx +35 -0
- package/dist/templates/nextjs/app/not-found.tsx +38 -0
- package/dist/templates/nextjs/app/page.tsx +20 -0
- package/dist/templates/nextjs/app/providers.tsx +45 -0
- package/dist/templates/nextjs/app/robots.ts +7 -0
- package/dist/templates/nextjs/app/sitemap.ts +9 -0
- package/dist/templates/nextjs/app/tag/[tag]/page.tsx +63 -0
- package/dist/templates/nextjs/app/theme-script.tsx +16 -0
- package/dist/templates/nextjs/lib/config.ts +23 -0
- package/dist/templates/nextjs/lib/notion.ts +58 -0
- package/dist/templates/nextjs/next.config.ts +26 -0
- package/dist/templates/nextjs/noxion.config.ts +22 -0
- package/dist/templates/nextjs/package.json +28 -0
- package/dist/templates/nextjs/tsconfig.json +19 -0
- package/package.json +56 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Jiwon
|
|
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.
|
package/README.md
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
# create-noxion
|
|
2
|
+
|
|
3
|
+
CLI scaffolding tool for [Noxion](https://github.com/jiwonme/noxion) — a Notion-powered blog builder.
|
|
4
|
+
|
|
5
|
+
## Usage
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
bun create noxion my-blog
|
|
9
|
+
# or
|
|
10
|
+
npx create-noxion my-blog
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
This scaffolds a complete Next.js blog project in seconds, pre-configured with:
|
|
14
|
+
|
|
15
|
+
- Next.js App Router
|
|
16
|
+
- Notion as CMS (via `@noxion/core`)
|
|
17
|
+
- SEO automation (via `@noxion/adapter-nextjs`)
|
|
18
|
+
- React components (via `@noxion/renderer`)
|
|
19
|
+
- ISR with on-demand revalidation
|
|
20
|
+
- Docker support
|
|
21
|
+
|
|
22
|
+
## After Scaffolding
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
cd my-blog
|
|
26
|
+
# Edit .env with your Notion page ID
|
|
27
|
+
bun run dev
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
Open `http://localhost:3000` — your blog is live.
|
|
31
|
+
|
|
32
|
+
## Documentation
|
|
33
|
+
|
|
34
|
+
See the [full documentation](https://github.com/jiwonme/noxion) for Notion setup, configuration, deployment guides, and more.
|
|
35
|
+
|
|
36
|
+
## License
|
|
37
|
+
|
|
38
|
+
MIT
|
package/bin/index.js
ADDED
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAKA,OAAO,EAAE,eAAe,EAAE,wBAAwB,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AACvF,YAAY,EAAE,eAAe,EAAE,cAAc,EAAE,MAAM,YAAY,CAAC;AAElE,wBAAsB,GAAG,CAAC,IAAI,GAAE,MAAM,EAA0B,GAAG,OAAO,CAAC,IAAI,CAAC,CAiG/E"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,115 @@
|
|
|
1
|
+
import * as p from "@clack/prompts";
|
|
2
|
+
import { join } from "node:path";
|
|
3
|
+
import { scaffoldProject, getTemplateDir } from "./scaffold";
|
|
4
|
+
export { scaffoldProject, resolveTemplateVariables, getTemplateDir } from "./scaffold";
|
|
5
|
+
export async function run(args = process.argv.slice(2)) {
|
|
6
|
+
p.intro("create-noxion");
|
|
7
|
+
const projectNameArg = args[0];
|
|
8
|
+
const flagArgs = parseFlags(args.slice(projectNameArg && !projectNameArg.startsWith("-") ? 1 : 0));
|
|
9
|
+
const isNonInteractive = flagArgs.yes === true;
|
|
10
|
+
let projectName;
|
|
11
|
+
let notionPageId;
|
|
12
|
+
let siteName;
|
|
13
|
+
let siteDescription;
|
|
14
|
+
let author;
|
|
15
|
+
let domain;
|
|
16
|
+
if (isNonInteractive) {
|
|
17
|
+
projectName = projectNameArg || "my-noxion-blog";
|
|
18
|
+
notionPageId = flagArgs["notion-id"] || "";
|
|
19
|
+
siteName = flagArgs["name"] || projectName;
|
|
20
|
+
siteDescription = flagArgs["description"] || "A blog powered by Notion and Noxion";
|
|
21
|
+
author = flagArgs["author"] || "Noxion";
|
|
22
|
+
domain = flagArgs["domain"] || "localhost:3000";
|
|
23
|
+
}
|
|
24
|
+
else {
|
|
25
|
+
const nameResult = await p.text({
|
|
26
|
+
message: "Project name",
|
|
27
|
+
initialValue: projectNameArg || "my-noxion-blog",
|
|
28
|
+
validate: (v) => (!v.trim() ? "Project name is required" : undefined),
|
|
29
|
+
});
|
|
30
|
+
if (p.isCancel(nameResult)) {
|
|
31
|
+
p.cancel("Cancelled.");
|
|
32
|
+
process.exit(0);
|
|
33
|
+
}
|
|
34
|
+
projectName = nameResult;
|
|
35
|
+
const notionResult = await p.text({
|
|
36
|
+
message: "Notion database page ID",
|
|
37
|
+
placeholder: "e.g. abc123def456...",
|
|
38
|
+
validate: (v) => (!v.trim() ? "Notion page ID is required" : undefined),
|
|
39
|
+
});
|
|
40
|
+
if (p.isCancel(notionResult)) {
|
|
41
|
+
p.cancel("Cancelled.");
|
|
42
|
+
process.exit(0);
|
|
43
|
+
}
|
|
44
|
+
notionPageId = notionResult;
|
|
45
|
+
const siteNameResult = await p.text({
|
|
46
|
+
message: "Site name",
|
|
47
|
+
initialValue: projectName,
|
|
48
|
+
});
|
|
49
|
+
if (p.isCancel(siteNameResult)) {
|
|
50
|
+
p.cancel("Cancelled.");
|
|
51
|
+
process.exit(0);
|
|
52
|
+
}
|
|
53
|
+
siteName = siteNameResult;
|
|
54
|
+
const descResult = await p.text({
|
|
55
|
+
message: "Site description",
|
|
56
|
+
initialValue: "A blog powered by Notion and Noxion",
|
|
57
|
+
});
|
|
58
|
+
if (p.isCancel(descResult)) {
|
|
59
|
+
p.cancel("Cancelled.");
|
|
60
|
+
process.exit(0);
|
|
61
|
+
}
|
|
62
|
+
siteDescription = descResult;
|
|
63
|
+
const authorResult = await p.text({
|
|
64
|
+
message: "Author",
|
|
65
|
+
initialValue: "Noxion",
|
|
66
|
+
});
|
|
67
|
+
if (p.isCancel(authorResult)) {
|
|
68
|
+
p.cancel("Cancelled.");
|
|
69
|
+
process.exit(0);
|
|
70
|
+
}
|
|
71
|
+
author = authorResult;
|
|
72
|
+
const domainResult = await p.text({
|
|
73
|
+
message: "Domain",
|
|
74
|
+
initialValue: "localhost:3000",
|
|
75
|
+
});
|
|
76
|
+
if (p.isCancel(domainResult)) {
|
|
77
|
+
p.cancel("Cancelled.");
|
|
78
|
+
process.exit(0);
|
|
79
|
+
}
|
|
80
|
+
domain = domainResult;
|
|
81
|
+
}
|
|
82
|
+
const targetDir = join(process.cwd(), projectName);
|
|
83
|
+
const templateDir = getTemplateDir("nextjs");
|
|
84
|
+
const options = {
|
|
85
|
+
projectName,
|
|
86
|
+
notionPageId,
|
|
87
|
+
siteName,
|
|
88
|
+
siteDescription,
|
|
89
|
+
author,
|
|
90
|
+
domain,
|
|
91
|
+
};
|
|
92
|
+
const spinner = p.spinner();
|
|
93
|
+
spinner.start("Creating project...");
|
|
94
|
+
const result = await scaffoldProject(targetDir, templateDir, options);
|
|
95
|
+
spinner.stop(`Created ${result.files.length} files`);
|
|
96
|
+
p.note([
|
|
97
|
+
`cd ${projectName}`,
|
|
98
|
+
`bun install`,
|
|
99
|
+
`# Add your NOTION_PAGE_ID to .env`,
|
|
100
|
+
`bun run dev`,
|
|
101
|
+
].join("\n"), "Next steps");
|
|
102
|
+
p.outro("Happy blogging!");
|
|
103
|
+
}
|
|
104
|
+
function parseFlags(args) {
|
|
105
|
+
const flags = {};
|
|
106
|
+
for (const arg of args) {
|
|
107
|
+
if (arg.startsWith("--")) {
|
|
108
|
+
const [key, ...valueParts] = arg.slice(2).split("=");
|
|
109
|
+
const value = valueParts.join("=");
|
|
110
|
+
flags[key] = value || true;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return flags;
|
|
114
|
+
}
|
|
115
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export interface ScaffoldOptions {
|
|
2
|
+
projectName: string;
|
|
3
|
+
notionPageId: string;
|
|
4
|
+
siteName: string;
|
|
5
|
+
siteDescription: string;
|
|
6
|
+
author: string;
|
|
7
|
+
domain: string;
|
|
8
|
+
}
|
|
9
|
+
export interface ScaffoldResult {
|
|
10
|
+
directory: string;
|
|
11
|
+
files: string[];
|
|
12
|
+
}
|
|
13
|
+
export declare function resolveTemplateVariables(content: string, options: ScaffoldOptions): string;
|
|
14
|
+
export declare function scaffoldProject(targetDir: string, templateDir: string, options: ScaffoldOptions): Promise<ScaffoldResult>;
|
|
15
|
+
export declare function getTemplateDir(framework: string): string;
|
|
16
|
+
//# sourceMappingURL=scaffold.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.d.ts","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAGA,MAAM,WAAW,eAAe;IAC9B,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,QAAQ,EAAE,MAAM,CAAC;IACjB,eAAe,EAAE,MAAM,CAAC;IACxB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,cAAc;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB,KAAK,EAAE,MAAM,EAAE,CAAC;CACjB;AAED,wBAAgB,wBAAwB,CACtC,OAAO,EAAE,MAAM,EACf,OAAO,EAAE,eAAe,GACvB,MAAM,CAQR;AAED,wBAAsB,eAAe,CACnC,SAAS,EAAE,MAAM,EACjB,WAAW,EAAE,MAAM,EACnB,OAAO,EAAE,eAAe,GACvB,OAAO,CAAC,cAAc,CAAC,CASzB;AA6BD,wBAAgB,cAAc,CAAC,SAAS,EAAE,MAAM,GAAG,MAAM,CAExD"}
|
package/dist/scaffold.js
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
import { mkdir, writeFile, readdir, readFile, stat } from "node:fs/promises";
|
|
2
|
+
import { join, relative } from "node:path";
|
|
3
|
+
export function resolveTemplateVariables(content, options) {
|
|
4
|
+
return content
|
|
5
|
+
.replace(/\{\{PROJECT_NAME\}\}/g, options.projectName)
|
|
6
|
+
.replace(/\{\{NOTION_PAGE_ID\}\}/g, options.notionPageId)
|
|
7
|
+
.replace(/\{\{SITE_NAME\}\}/g, options.siteName)
|
|
8
|
+
.replace(/\{\{SITE_DESCRIPTION\}\}/g, options.siteDescription)
|
|
9
|
+
.replace(/\{\{AUTHOR\}\}/g, options.author)
|
|
10
|
+
.replace(/\{\{DOMAIN\}\}/g, options.domain);
|
|
11
|
+
}
|
|
12
|
+
export async function scaffoldProject(targetDir, templateDir, options) {
|
|
13
|
+
await mkdir(targetDir, { recursive: true });
|
|
14
|
+
const files = await copyTemplateDir(templateDir, targetDir, options);
|
|
15
|
+
return {
|
|
16
|
+
directory: targetDir,
|
|
17
|
+
files,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
async function copyTemplateDir(src, dest, options, files = []) {
|
|
21
|
+
const entries = await readdir(src);
|
|
22
|
+
for (const entry of entries) {
|
|
23
|
+
const srcPath = join(src, entry);
|
|
24
|
+
const destPath = join(dest, entry);
|
|
25
|
+
const info = await stat(srcPath);
|
|
26
|
+
if (info.isDirectory()) {
|
|
27
|
+
await mkdir(destPath, { recursive: true });
|
|
28
|
+
await copyTemplateDir(srcPath, destPath, options, files);
|
|
29
|
+
}
|
|
30
|
+
else {
|
|
31
|
+
const content = await readFile(srcPath, "utf-8");
|
|
32
|
+
const resolved = resolveTemplateVariables(content, options);
|
|
33
|
+
await writeFile(destPath, resolved, "utf-8");
|
|
34
|
+
files.push(relative(dest, destPath));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return files;
|
|
38
|
+
}
|
|
39
|
+
export function getTemplateDir(framework) {
|
|
40
|
+
return join(import.meta.dirname, "templates", framework);
|
|
41
|
+
}
|
|
42
|
+
//# sourceMappingURL=scaffold.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"scaffold.js","sourceRoot":"","sources":["../src/scaffold.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,SAAS,EAAE,OAAO,EAAE,QAAQ,EAAE,IAAI,EAAE,MAAM,kBAAkB,CAAC;AAC7E,OAAO,EAAE,IAAI,EAAE,QAAQ,EAAE,MAAM,WAAW,CAAC;AAgB3C,MAAM,UAAU,wBAAwB,CACtC,OAAe,EACf,OAAwB;IAExB,OAAO,OAAO;SACX,OAAO,CAAC,uBAAuB,EAAE,OAAO,CAAC,WAAW,CAAC;SACrD,OAAO,CAAC,yBAAyB,EAAE,OAAO,CAAC,YAAY,CAAC;SACxD,OAAO,CAAC,oBAAoB,EAAE,OAAO,CAAC,QAAQ,CAAC;SAC/C,OAAO,CAAC,2BAA2B,EAAE,OAAO,CAAC,eAAe,CAAC;SAC7D,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC,MAAM,CAAC;SAC1C,OAAO,CAAC,iBAAiB,EAAE,OAAO,CAAC,MAAM,CAAC,CAAC;AAChD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,eAAe,CACnC,SAAiB,EACjB,WAAmB,EACnB,OAAwB;IAExB,MAAM,KAAK,CAAC,SAAS,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;IAE5C,MAAM,KAAK,GAAG,MAAM,eAAe,CAAC,WAAW,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;IAErE,OAAO;QACL,SAAS,EAAE,SAAS;QACpB,KAAK;KACN,CAAC;AACJ,CAAC;AAED,KAAK,UAAU,eAAe,CAC5B,GAAW,EACX,IAAY,EACZ,OAAwB,EACxB,QAAkB,EAAE;IAEpB,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC;IAEnC,KAAK,MAAM,KAAK,IAAI,OAAO,EAAE,CAAC;QAC5B,MAAM,OAAO,GAAG,IAAI,CAAC,GAAG,EAAE,KAAK,CAAC,CAAC;QACjC,MAAM,QAAQ,GAAG,IAAI,CAAC,IAAI,EAAE,KAAK,CAAC,CAAC;QACnC,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,CAAC,CAAC;QAEjC,IAAI,IAAI,CAAC,WAAW,EAAE,EAAE,CAAC;YACvB,MAAM,KAAK,CAAC,QAAQ,EAAE,EAAE,SAAS,EAAE,IAAI,EAAE,CAAC,CAAC;YAC3C,MAAM,eAAe,CAAC,OAAO,EAAE,QAAQ,EAAE,OAAO,EAAE,KAAK,CAAC,CAAC;QAC3D,CAAC;aAAM,CAAC;YACN,MAAM,OAAO,GAAG,MAAM,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YACjD,MAAM,QAAQ,GAAG,wBAAwB,CAAC,OAAO,EAAE,OAAO,CAAC,CAAC;YAC5D,MAAM,SAAS,CAAC,QAAQ,EAAE,QAAQ,EAAE,OAAO,CAAC,CAAC;YAC7C,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,QAAQ,CAAC,CAAC,CAAC;QACvC,CAAC;IACH,CAAC;IAED,OAAO,KAAK,CAAC;AACf,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,SAAiB;IAC9C,OAAO,IAAI,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,EAAE,WAAW,EAAE,SAAS,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { notFound } from "next/navigation";
|
|
2
|
+
import type { Metadata } from "next";
|
|
3
|
+
import { NotionPage } from "@noxion/renderer";
|
|
4
|
+
import { generateNoxionMetadata, generateNoxionStaticParams, generateBlogPostingLD } from "@noxion/adapter-nextjs";
|
|
5
|
+
import { createNotionClient } from "@noxion/core";
|
|
6
|
+
import { getPostBySlug, getPageRecordMap } from "../../lib/notion";
|
|
7
|
+
import { siteConfig } from "../../lib/config";
|
|
8
|
+
|
|
9
|
+
export const revalidate = 3600;
|
|
10
|
+
|
|
11
|
+
const notion = createNotionClient({
|
|
12
|
+
authToken: process.env.NOTION_TOKEN || undefined,
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
export async function generateStaticParams() {
|
|
16
|
+
if (!siteConfig.rootNotionPageId) return [];
|
|
17
|
+
try {
|
|
18
|
+
return await generateNoxionStaticParams(notion, siteConfig.rootNotionPageId);
|
|
19
|
+
} catch {
|
|
20
|
+
return [];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export async function generateMetadata({
|
|
25
|
+
params,
|
|
26
|
+
}: {
|
|
27
|
+
params: Promise<{ slug: string }>;
|
|
28
|
+
}): Promise<Metadata> {
|
|
29
|
+
const { slug } = await params;
|
|
30
|
+
const post = await getPostBySlug(slug);
|
|
31
|
+
if (!post) return { title: "Not Found" };
|
|
32
|
+
return generateNoxionMetadata(post, siteConfig);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export default async function PostPage({
|
|
36
|
+
params,
|
|
37
|
+
}: {
|
|
38
|
+
params: Promise<{ slug: string }>;
|
|
39
|
+
}) {
|
|
40
|
+
const { slug } = await params;
|
|
41
|
+
const post = await getPostBySlug(slug);
|
|
42
|
+
if (!post) notFound();
|
|
43
|
+
|
|
44
|
+
const recordMap = await getPageRecordMap(post.id);
|
|
45
|
+
const jsonLd = generateBlogPostingLD(post, siteConfig);
|
|
46
|
+
|
|
47
|
+
return (
|
|
48
|
+
<article>
|
|
49
|
+
<script
|
|
50
|
+
type="application/ld+json"
|
|
51
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
52
|
+
/>
|
|
53
|
+
<NotionPage
|
|
54
|
+
recordMap={recordMap}
|
|
55
|
+
rootPageId={post.id}
|
|
56
|
+
fullPage
|
|
57
|
+
previewImages
|
|
58
|
+
showTableOfContents
|
|
59
|
+
mapPageUrl={(pageId: string) => `/${pageId}`}
|
|
60
|
+
/>
|
|
61
|
+
</article>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { revalidatePath } from "next/cache";
|
|
2
|
+
import { createRevalidateHandler } from "@noxion/adapter-nextjs";
|
|
3
|
+
import { siteConfig } from "../../../lib/config";
|
|
4
|
+
|
|
5
|
+
const handler = createRevalidateHandler({
|
|
6
|
+
config: siteConfig,
|
|
7
|
+
revalidatePath,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export async function POST(request: Request) {
|
|
11
|
+
return handler(request as never);
|
|
12
|
+
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
@import "react-notion-x/src/styles.css";
|
|
2
|
+
|
|
3
|
+
*,
|
|
4
|
+
*::before,
|
|
5
|
+
*::after {
|
|
6
|
+
box-sizing: border-box;
|
|
7
|
+
margin: 0;
|
|
8
|
+
padding: 0;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
html {
|
|
12
|
+
font-family: var(--noxion-font-sans, system-ui, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif);
|
|
13
|
+
-webkit-font-smoothing: antialiased;
|
|
14
|
+
-moz-osx-font-smoothing: grayscale;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
body {
|
|
18
|
+
background-color: var(--noxion-background, #fff);
|
|
19
|
+
color: var(--noxion-foreground, #0a0a0a);
|
|
20
|
+
min-height: 100vh;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
[data-theme="dark"] body {
|
|
24
|
+
background-color: var(--noxion-background, #0a0a0a);
|
|
25
|
+
color: var(--noxion-foreground, #fafafa);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
a {
|
|
29
|
+
color: inherit;
|
|
30
|
+
text-decoration: none;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
.notion-page {
|
|
34
|
+
padding: 0 !important;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
.notion-title {
|
|
38
|
+
font-size: 2.25rem !important;
|
|
39
|
+
font-weight: 700 !important;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
.notion-collection-page-properties {
|
|
43
|
+
display: none !important;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
.notion-viewport {
|
|
47
|
+
z-index: -1 !important;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
[data-theme="dark"] .notion {
|
|
51
|
+
--bg-color: var(--noxion-background, #0a0a0a) !important;
|
|
52
|
+
--fg-color: var(--noxion-foreground, #fafafa) !important;
|
|
53
|
+
}
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { PostList, TagFilter, Search, ThemeToggle, useSearch } from "@noxion/renderer";
|
|
4
|
+
import type { PostCardProps } from "@noxion/renderer";
|
|
5
|
+
import { useState, useCallback } from "react";
|
|
6
|
+
|
|
7
|
+
interface HomeContentProps {
|
|
8
|
+
posts: PostCardProps[];
|
|
9
|
+
allTags: string[];
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function HomeContent({ posts, allTags }: HomeContentProps) {
|
|
13
|
+
const [selectedTags, setSelectedTags] = useState<string[]>([]);
|
|
14
|
+
|
|
15
|
+
const tagFiltered = selectedTags.length > 0
|
|
16
|
+
? posts.filter((p) => p.tags.some((t) => selectedTags.includes(t)))
|
|
17
|
+
: posts;
|
|
18
|
+
|
|
19
|
+
const { setQuery, filtered } = useSearch(tagFiltered);
|
|
20
|
+
|
|
21
|
+
const handleToggleTag = useCallback((tag: string) => {
|
|
22
|
+
setSelectedTags((prev) =>
|
|
23
|
+
prev.includes(tag) ? prev.filter((t) => t !== tag) : [...prev, tag]
|
|
24
|
+
);
|
|
25
|
+
}, []);
|
|
26
|
+
|
|
27
|
+
return (
|
|
28
|
+
<div>
|
|
29
|
+
<div
|
|
30
|
+
style={{
|
|
31
|
+
display: "flex",
|
|
32
|
+
alignItems: "center",
|
|
33
|
+
justifyContent: "space-between",
|
|
34
|
+
marginBottom: "1.5rem",
|
|
35
|
+
}}
|
|
36
|
+
>
|
|
37
|
+
<h1 style={{ fontSize: "1.5rem", fontWeight: 700 }}>Posts</h1>
|
|
38
|
+
<ThemeToggle />
|
|
39
|
+
</div>
|
|
40
|
+
|
|
41
|
+
<div style={{ marginBottom: "1.5rem" }}>
|
|
42
|
+
<Search onSearch={setQuery} placeholder="Search posts..." />
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{allTags.length > 0 && (
|
|
46
|
+
<div style={{ marginBottom: "2rem" }}>
|
|
47
|
+
<TagFilter
|
|
48
|
+
tags={allTags}
|
|
49
|
+
selectedTags={selectedTags}
|
|
50
|
+
onToggle={handleToggleTag}
|
|
51
|
+
/>
|
|
52
|
+
</div>
|
|
53
|
+
)}
|
|
54
|
+
|
|
55
|
+
<PostList posts={filtered} />
|
|
56
|
+
</div>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { generateNoxionListMetadata, generateWebSiteLD } from "@noxion/adapter-nextjs";
|
|
3
|
+
import { siteConfig } from "../lib/config";
|
|
4
|
+
import { ThemeScript } from "./theme-script";
|
|
5
|
+
import { Providers } from "./providers";
|
|
6
|
+
import "./globals.css";
|
|
7
|
+
|
|
8
|
+
export function generateMetadata(): Metadata {
|
|
9
|
+
return generateNoxionListMetadata(siteConfig);
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export default function RootLayout({
|
|
13
|
+
children,
|
|
14
|
+
}: {
|
|
15
|
+
children: React.ReactNode;
|
|
16
|
+
}) {
|
|
17
|
+
const jsonLd = generateWebSiteLD(siteConfig);
|
|
18
|
+
|
|
19
|
+
return (
|
|
20
|
+
<html lang={siteConfig.language} suppressHydrationWarning>
|
|
21
|
+
<head>
|
|
22
|
+
<ThemeScript />
|
|
23
|
+
<script
|
|
24
|
+
type="application/ld+json"
|
|
25
|
+
dangerouslySetInnerHTML={{ __html: JSON.stringify(jsonLd) }}
|
|
26
|
+
/>
|
|
27
|
+
</head>
|
|
28
|
+
<body>
|
|
29
|
+
<Providers siteName={siteConfig.name} author={siteConfig.author}>
|
|
30
|
+
{children}
|
|
31
|
+
</Providers>
|
|
32
|
+
</body>
|
|
33
|
+
</html>
|
|
34
|
+
);
|
|
35
|
+
}
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
import Link from "next/link";
|
|
2
|
+
|
|
3
|
+
export default function NotFound() {
|
|
4
|
+
return (
|
|
5
|
+
<div
|
|
6
|
+
style={{
|
|
7
|
+
display: "flex",
|
|
8
|
+
flexDirection: "column",
|
|
9
|
+
alignItems: "center",
|
|
10
|
+
justifyContent: "center",
|
|
11
|
+
minHeight: "50vh",
|
|
12
|
+
textAlign: "center",
|
|
13
|
+
gap: "1rem",
|
|
14
|
+
}}
|
|
15
|
+
>
|
|
16
|
+
<h1 style={{ fontSize: "4rem", fontWeight: 700, color: "var(--noxion-mutedForeground, #737373)" }}>
|
|
17
|
+
404
|
|
18
|
+
</h1>
|
|
19
|
+
<p style={{ fontSize: "1.125rem", color: "var(--noxion-mutedForeground, #737373)" }}>
|
|
20
|
+
This page could not be found.
|
|
21
|
+
</p>
|
|
22
|
+
<Link
|
|
23
|
+
href="/"
|
|
24
|
+
style={{
|
|
25
|
+
marginTop: "1rem",
|
|
26
|
+
padding: "0.5rem 1.5rem",
|
|
27
|
+
borderRadius: "var(--noxion-border-radius, 0.5rem)",
|
|
28
|
+
backgroundColor: "var(--noxion-primary, #2563eb)",
|
|
29
|
+
color: "var(--noxion-primaryForeground, #fff)",
|
|
30
|
+
fontSize: "0.875rem",
|
|
31
|
+
fontWeight: 500,
|
|
32
|
+
}}
|
|
33
|
+
>
|
|
34
|
+
Go Home
|
|
35
|
+
</Link>
|
|
36
|
+
</div>
|
|
37
|
+
);
|
|
38
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { getAllPosts, getAllTags } from "../lib/notion";
|
|
2
|
+
import { HomeContent } from "./home-content";
|
|
3
|
+
|
|
4
|
+
export const revalidate = 3600;
|
|
5
|
+
|
|
6
|
+
export default async function HomePage() {
|
|
7
|
+
const posts = await getAllPosts();
|
|
8
|
+
const allTags = getAllTags(posts);
|
|
9
|
+
|
|
10
|
+
const postCards = posts.map((post) => ({
|
|
11
|
+
title: post.title,
|
|
12
|
+
slug: post.slug,
|
|
13
|
+
date: post.date,
|
|
14
|
+
tags: post.tags,
|
|
15
|
+
coverImage: post.coverImage,
|
|
16
|
+
category: post.category,
|
|
17
|
+
}));
|
|
18
|
+
|
|
19
|
+
return <HomeContent posts={postCards} allTags={allTags} />;
|
|
20
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
|
|
3
|
+
import { NoxionThemeProvider, defaultTheme, Header, Footer } from "@noxion/renderer";
|
|
4
|
+
|
|
5
|
+
interface ProvidersProps {
|
|
6
|
+
siteName: string;
|
|
7
|
+
author: string;
|
|
8
|
+
children: React.ReactNode;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export function Providers({ siteName, author, children }: ProvidersProps) {
|
|
12
|
+
return (
|
|
13
|
+
<NoxionThemeProvider theme={defaultTheme}>
|
|
14
|
+
<div
|
|
15
|
+
style={{
|
|
16
|
+
display: "flex",
|
|
17
|
+
flexDirection: "column",
|
|
18
|
+
minHeight: "100vh",
|
|
19
|
+
}}
|
|
20
|
+
>
|
|
21
|
+
<Header
|
|
22
|
+
siteName={siteName}
|
|
23
|
+
navigation={[
|
|
24
|
+
{ label: "Home", href: "/" },
|
|
25
|
+
]}
|
|
26
|
+
/>
|
|
27
|
+
<main
|
|
28
|
+
style={{
|
|
29
|
+
flex: 1,
|
|
30
|
+
width: "100%",
|
|
31
|
+
maxWidth: "var(--noxion-content-width, 720px)",
|
|
32
|
+
margin: "0 auto",
|
|
33
|
+
padding: "2rem 1.5rem",
|
|
34
|
+
}}
|
|
35
|
+
>
|
|
36
|
+
{children}
|
|
37
|
+
</main>
|
|
38
|
+
<Footer
|
|
39
|
+
siteName={siteName}
|
|
40
|
+
author={author}
|
|
41
|
+
/>
|
|
42
|
+
</div>
|
|
43
|
+
</NoxionThemeProvider>
|
|
44
|
+
);
|
|
45
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import type { MetadataRoute } from "next";
|
|
2
|
+
import { generateNoxionSitemap } from "@noxion/adapter-nextjs";
|
|
3
|
+
import { getAllPosts } from "../lib/notion";
|
|
4
|
+
import { siteConfig } from "../lib/config";
|
|
5
|
+
|
|
6
|
+
export default async function sitemap(): Promise<MetadataRoute.Sitemap> {
|
|
7
|
+
const posts = await getAllPosts();
|
|
8
|
+
return generateNoxionSitemap(posts, siteConfig);
|
|
9
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import type { Metadata } from "next";
|
|
2
|
+
import { getAllPosts, getAllTags } from "../../../lib/notion";
|
|
3
|
+
import { siteConfig } from "../../../lib/config";
|
|
4
|
+
import { HomeContent } from "../../home-content";
|
|
5
|
+
|
|
6
|
+
export const revalidate = 3600;
|
|
7
|
+
|
|
8
|
+
export async function generateStaticParams() {
|
|
9
|
+
const posts = await getAllPosts();
|
|
10
|
+
const tags = getAllTags(posts);
|
|
11
|
+
return tags.map((tag) => ({ tag }));
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export async function generateMetadata({
|
|
15
|
+
params,
|
|
16
|
+
}: {
|
|
17
|
+
params: Promise<{ tag: string }>;
|
|
18
|
+
}): Promise<Metadata> {
|
|
19
|
+
const { tag } = await params;
|
|
20
|
+
const decodedTag = decodeURIComponent(tag);
|
|
21
|
+
return {
|
|
22
|
+
title: `${decodedTag} | ${siteConfig.name}`,
|
|
23
|
+
description: `Posts tagged with "${decodedTag}" on ${siteConfig.name}`,
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export default async function TagPage({
|
|
28
|
+
params,
|
|
29
|
+
}: {
|
|
30
|
+
params: Promise<{ tag: string }>;
|
|
31
|
+
}) {
|
|
32
|
+
const { tag } = await params;
|
|
33
|
+
const decodedTag = decodeURIComponent(tag);
|
|
34
|
+
const posts = await getAllPosts();
|
|
35
|
+
const allTags = getAllTags(posts);
|
|
36
|
+
|
|
37
|
+
const filteredPosts = posts
|
|
38
|
+
.filter((p) => p.tags.includes(decodedTag))
|
|
39
|
+
.map((post) => ({
|
|
40
|
+
title: post.title,
|
|
41
|
+
slug: post.slug,
|
|
42
|
+
date: post.date,
|
|
43
|
+
tags: post.tags,
|
|
44
|
+
coverImage: post.coverImage,
|
|
45
|
+
category: post.category,
|
|
46
|
+
}));
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div>
|
|
50
|
+
<h2
|
|
51
|
+
style={{
|
|
52
|
+
fontSize: "1.25rem",
|
|
53
|
+
fontWeight: 600,
|
|
54
|
+
marginBottom: "1.5rem",
|
|
55
|
+
color: "var(--noxion-mutedForeground, #737373)",
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
Tag: {decodedTag}
|
|
59
|
+
</h2>
|
|
60
|
+
<HomeContent posts={filteredPosts} allTags={allTags} />
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
export function ThemeScript() {
|
|
2
|
+
const script = `
|
|
3
|
+
(function() {
|
|
4
|
+
try {
|
|
5
|
+
var stored = localStorage.getItem('noxion-theme');
|
|
6
|
+
var theme = stored || 'system';
|
|
7
|
+
if (theme === 'system') {
|
|
8
|
+
theme = window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light';
|
|
9
|
+
}
|
|
10
|
+
document.documentElement.dataset.theme = theme;
|
|
11
|
+
} catch (e) {}
|
|
12
|
+
})();
|
|
13
|
+
`;
|
|
14
|
+
|
|
15
|
+
return <script dangerouslySetInnerHTML={{ __html: script }} />;
|
|
16
|
+
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { loadConfig } from "@noxion/core";
|
|
2
|
+
import type { NoxionConfig } from "@noxion/core";
|
|
3
|
+
import noxionConfigInput from "../noxion.config";
|
|
4
|
+
|
|
5
|
+
function createConfig(): NoxionConfig {
|
|
6
|
+
try {
|
|
7
|
+
return loadConfig(noxionConfigInput);
|
|
8
|
+
} catch {
|
|
9
|
+
return {
|
|
10
|
+
rootNotionPageId: noxionConfigInput.rootNotionPageId ?? "",
|
|
11
|
+
name: noxionConfigInput.name ?? "{{SITE_NAME}}",
|
|
12
|
+
domain: noxionConfigInput.domain ?? "{{DOMAIN}}",
|
|
13
|
+
author: noxionConfigInput.author ?? "{{AUTHOR}}",
|
|
14
|
+
description: noxionConfigInput.description ?? "{{SITE_DESCRIPTION}}",
|
|
15
|
+
language: noxionConfigInput.language ?? "en",
|
|
16
|
+
defaultTheme: noxionConfigInput.defaultTheme ?? "system",
|
|
17
|
+
revalidate: noxionConfigInput.revalidate ?? 3600,
|
|
18
|
+
plugins: noxionConfigInput.plugins,
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const siteConfig = createConfig();
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { createNotionClient, fetchBlogPosts, fetchPage, fetchPostBySlug, downloadImages, mapImages } from "@noxion/core";
|
|
2
|
+
import type { BlogPost, ExtendedRecordMap } from "@noxion/core";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { siteConfig } from "./config";
|
|
5
|
+
|
|
6
|
+
const notion = createNotionClient({
|
|
7
|
+
authToken: process.env.NOTION_TOKEN || undefined,
|
|
8
|
+
});
|
|
9
|
+
|
|
10
|
+
export async function getAllPosts(): Promise<BlogPost[]> {
|
|
11
|
+
if (!siteConfig.rootNotionPageId) return [];
|
|
12
|
+
try {
|
|
13
|
+
return await fetchBlogPosts(notion, siteConfig.rootNotionPageId);
|
|
14
|
+
} catch (error) {
|
|
15
|
+
console.error("Failed to fetch blog posts:", error);
|
|
16
|
+
return [];
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export async function getPostBySlug(slug: string): Promise<BlogPost | undefined> {
|
|
21
|
+
if (!siteConfig.rootNotionPageId) return undefined;
|
|
22
|
+
try {
|
|
23
|
+
return await fetchPostBySlug(notion, siteConfig.rootNotionPageId, slug);
|
|
24
|
+
} catch (error) {
|
|
25
|
+
console.error(`Failed to fetch post "${slug}":`, error);
|
|
26
|
+
return undefined;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export async function getPageRecordMap(pageId: string): Promise<ExtendedRecordMap> {
|
|
31
|
+
const recordMap = await fetchPage(notion, pageId);
|
|
32
|
+
|
|
33
|
+
if (process.env.NODE_ENV === "production") {
|
|
34
|
+
try {
|
|
35
|
+
const outputDir = join(process.cwd(), "public");
|
|
36
|
+
const urlMap = await downloadImages(recordMap, outputDir, { concurrency: 5 });
|
|
37
|
+
const localUrlMap: Record<string, string> = {};
|
|
38
|
+
for (const [originalUrl, localPath] of Object.entries(urlMap)) {
|
|
39
|
+
localUrlMap[originalUrl] = `/images/${localPath.split("/images/").pop()}`;
|
|
40
|
+
}
|
|
41
|
+
return mapImages(recordMap, localUrlMap);
|
|
42
|
+
} catch (error) {
|
|
43
|
+
console.error("Image download failed, using original URLs:", error);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return recordMap;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function getAllTags(posts: BlogPost[]): string[] {
|
|
51
|
+
const tagSet = new Set<string>();
|
|
52
|
+
for (const post of posts) {
|
|
53
|
+
for (const tag of post.tags) {
|
|
54
|
+
tagSet.add(tag);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
return [...tagSet].sort();
|
|
58
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import type { NextConfig } from "next";
|
|
2
|
+
|
|
3
|
+
const nextConfig: NextConfig = {
|
|
4
|
+
output: "standalone",
|
|
5
|
+
transpilePackages: [
|
|
6
|
+
"@noxion/core",
|
|
7
|
+
"@noxion/renderer",
|
|
8
|
+
"@noxion/adapter-nextjs",
|
|
9
|
+
"react-notion-x",
|
|
10
|
+
"notion-client",
|
|
11
|
+
"notion-types",
|
|
12
|
+
"notion-utils",
|
|
13
|
+
],
|
|
14
|
+
images: {
|
|
15
|
+
remotePatterns: [
|
|
16
|
+
{ protocol: "https", hostname: "www.notion.so" },
|
|
17
|
+
{ protocol: "https", hostname: "notion.so" },
|
|
18
|
+
{ protocol: "https", hostname: "images.unsplash.com" },
|
|
19
|
+
{ protocol: "https", hostname: "s3.us-west-2.amazonaws.com" },
|
|
20
|
+
{ protocol: "https", hostname: "prod-files-secure.s3.us-west-2.amazonaws.com" },
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
staticPageGenerationTimeout: 300,
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
export default nextConfig;
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { defineConfig, createRSSPlugin } from "@noxion/core";
|
|
2
|
+
|
|
3
|
+
const config = defineConfig({
|
|
4
|
+
rootNotionPageId: process.env.NOTION_PAGE_ID!,
|
|
5
|
+
rootNotionSpaceId: process.env.NOTION_SPACE_ID,
|
|
6
|
+
name: process.env.SITE_NAME ?? "{{SITE_NAME}}",
|
|
7
|
+
domain: process.env.SITE_DOMAIN ?? "{{DOMAIN}}",
|
|
8
|
+
author: process.env.SITE_AUTHOR ?? "{{AUTHOR}}",
|
|
9
|
+
description: process.env.SITE_DESCRIPTION ?? "{{SITE_DESCRIPTION}}",
|
|
10
|
+
language: "en",
|
|
11
|
+
defaultTheme: "system",
|
|
12
|
+
revalidate: 3600,
|
|
13
|
+
revalidateSecret: process.env.REVALIDATE_SECRET,
|
|
14
|
+
plugins: [
|
|
15
|
+
createRSSPlugin({
|
|
16
|
+
feedPath: "/feed.xml",
|
|
17
|
+
limit: 20,
|
|
18
|
+
}),
|
|
19
|
+
],
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
export default config;
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "{{PROJECT_NAME}}",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"private": true,
|
|
5
|
+
"scripts": {
|
|
6
|
+
"dev": "next dev",
|
|
7
|
+
"build": "next build",
|
|
8
|
+
"start": "next start"
|
|
9
|
+
},
|
|
10
|
+
"dependencies": {
|
|
11
|
+
"@noxion/core": "^0.0.1",
|
|
12
|
+
"@noxion/renderer": "^0.0.1",
|
|
13
|
+
"@noxion/adapter-nextjs": "^0.0.1",
|
|
14
|
+
"next": "^16.1.6",
|
|
15
|
+
"react": "^19.1.0",
|
|
16
|
+
"react-dom": "^19.1.0",
|
|
17
|
+
"react-notion-x": "^7.8.2",
|
|
18
|
+
"notion-client": "^7.8.2",
|
|
19
|
+
"prismjs": "^1.30.0",
|
|
20
|
+
"katex": "^0.16.28"
|
|
21
|
+
},
|
|
22
|
+
"devDependencies": {
|
|
23
|
+
"@types/node": "^22.0.0",
|
|
24
|
+
"@types/react": "^19.2.14",
|
|
25
|
+
"@types/react-dom": "^19.2.3",
|
|
26
|
+
"typescript": "^5.7.0"
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
{
|
|
2
|
+
"compilerOptions": {
|
|
3
|
+
"target": "ES2022",
|
|
4
|
+
"module": "ESNext",
|
|
5
|
+
"moduleResolution": "bundler",
|
|
6
|
+
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
|
7
|
+
"jsx": "react-jsx",
|
|
8
|
+
"strict": true,
|
|
9
|
+
"esModuleInterop": true,
|
|
10
|
+
"skipLibCheck": true,
|
|
11
|
+
"forceConsistentCasingInFileNames": true,
|
|
12
|
+
"resolveJsonModule": true,
|
|
13
|
+
"isolatedModules": true,
|
|
14
|
+
"plugins": [{ "name": "next" }],
|
|
15
|
+
"outDir": "./dist"
|
|
16
|
+
},
|
|
17
|
+
"include": ["**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
|
18
|
+
"exclude": ["node_modules", ".next", "out", "dist"]
|
|
19
|
+
}
|
package/package.json
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "create-noxion",
|
|
3
|
+
"version": "0.0.1",
|
|
4
|
+
"description": "CLI scaffolding tool for Noxion projects",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"bin": {
|
|
10
|
+
"create-noxion": "./bin/index.js"
|
|
11
|
+
},
|
|
12
|
+
"exports": {
|
|
13
|
+
".": {
|
|
14
|
+
"import": "./dist/index.js",
|
|
15
|
+
"types": "./dist/index.d.ts"
|
|
16
|
+
}
|
|
17
|
+
},
|
|
18
|
+
"files": [
|
|
19
|
+
"dist",
|
|
20
|
+
"bin",
|
|
21
|
+
"README.md",
|
|
22
|
+
"LICENSE"
|
|
23
|
+
],
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "tsc && rm -rf dist/templates && cp -r src/templates dist/templates",
|
|
26
|
+
"release": "npm publish",
|
|
27
|
+
"lint": "echo 'lint ok'",
|
|
28
|
+
"clean": "rm -rf dist"
|
|
29
|
+
},
|
|
30
|
+
"keywords": [
|
|
31
|
+
"notion",
|
|
32
|
+
"blog",
|
|
33
|
+
"create",
|
|
34
|
+
"scaffold",
|
|
35
|
+
"noxion"
|
|
36
|
+
],
|
|
37
|
+
"repository": {
|
|
38
|
+
"type": "git",
|
|
39
|
+
"url": "https://github.com/jiwonme/noxion.git",
|
|
40
|
+
"directory": "packages/create-noxion"
|
|
41
|
+
},
|
|
42
|
+
"homepage": "https://github.com/jiwonme/noxion#readme",
|
|
43
|
+
"bugs": {
|
|
44
|
+
"url": "https://github.com/jiwonme/noxion/issues"
|
|
45
|
+
},
|
|
46
|
+
"engines": {
|
|
47
|
+
"node": ">=18.0.0"
|
|
48
|
+
},
|
|
49
|
+
"dependencies": {
|
|
50
|
+
"@clack/prompts": "^0.10.0"
|
|
51
|
+
},
|
|
52
|
+
"devDependencies": {
|
|
53
|
+
"typescript": "^5.7.0",
|
|
54
|
+
"@types/bun": "^1.2.0"
|
|
55
|
+
}
|
|
56
|
+
}
|