feat: import posts from file

This commit is contained in:
DIYgod 2022-09-10 02:31:42 +01:00
parent f1d1f0ebcf
commit 9dd09b2cbb
No known key found for this signature in database
GPG Key ID: D159328F47A80DCA
5 changed files with 163 additions and 33 deletions

View File

@ -58,6 +58,7 @@
"form-data": "^4.0.0",
"idb-keyval": "^6.2.0",
"ioredis": "^5.2.2",
"js-yaml": "^4.1.0",
"mailgun.js": "^5.2.2",
"mdast-util-to-string": "^3.1.0",
"nanoid": "^3.3.3",
@ -77,9 +78,11 @@
"rehype-raw": "^6.1.1",
"rehype-sanitize": "^5.0.1",
"rehype-stringify": "^9.0.3",
"remark-frontmatter": "^4.0.1",
"remark-gfm": "^3.0.1",
"remark-parse": "^10.0.1",
"remark-rehype": "^10.1.0",
"remark-stringify": "^10.0.2",
"sortablejs": "^1.15.0",
"ua-parser-js": "^1.0.2",
"unidata.js": "^0.6.21",
@ -104,6 +107,7 @@
"@types/async-busboy": "^1.1.1",
"@types/busboy": "^1.5.0",
"@types/cookie": "^0.5.1",
"@types/js-yaml": "^4.0.5",
"@types/mjml": "^4.7.0",
"@types/node": "^17.0.31",
"@types/pica": "^9.0.0",

View File

@ -35,6 +35,7 @@ specifiers:
'@types/async-busboy': ^1.1.1
'@types/busboy': ^1.5.0
'@types/cookie': ^0.5.1
'@types/js-yaml': ^4.0.5
'@types/mjml': ^4.7.0
'@types/node': ^17.0.31
'@types/pica': ^9.0.0
@ -70,6 +71,7 @@ specifiers:
husky: ^8.0.1
idb-keyval: ^6.2.0
ioredis: ^5.2.2
js-yaml: ^4.1.0
lint-staged: ^13.0.3
mailgun.js: ^5.2.2
mdast-util-to-string: ^3.1.0
@ -93,9 +95,11 @@ specifiers:
rehype-raw: ^6.1.1
rehype-sanitize: ^5.0.1
rehype-stringify: ^9.0.3
remark-frontmatter: ^4.0.1
remark-gfm: ^3.0.1
remark-parse: ^10.0.1
remark-rehype: ^10.1.0
remark-stringify: ^10.0.2
sortablejs: ^1.15.0
tailwindcss: ^3.0.24
typescript: ^4.5.5
@ -153,6 +157,7 @@ dependencies:
form-data: 4.0.0
idb-keyval: 6.2.0
ioredis: 5.2.2
js-yaml: 4.1.0
mailgun.js: 5.2.2
mdast-util-to-string: 3.1.0
nanoid: 3.3.4
@ -172,9 +177,11 @@ dependencies:
rehype-raw: 6.1.1
rehype-sanitize: 5.0.1
rehype-stringify: 9.0.3
remark-frontmatter: 4.0.1
remark-gfm: 3.0.1
remark-parse: 10.0.1
remark-rehype: 10.1.0
remark-stringify: 10.0.2
sortablejs: 1.15.0
ua-parser-js: 1.0.2
unidata.js: 0.6.21
@ -199,6 +206,7 @@ devDependencies:
'@types/async-busboy': 1.1.1
'@types/busboy': 1.5.0
'@types/cookie': 0.5.1
'@types/js-yaml': 4.0.5
'@types/mjml': 4.7.0
'@types/node': 17.0.45
'@types/pica': 9.0.1
@ -2078,6 +2086,10 @@ packages:
'@types/unist': 2.0.6
dev: false
/@types/js-yaml/4.0.5:
resolution: {integrity: sha512-FhpRzf927MNQdRZP0J5DLIdTXhjLYzeUTmLAu69mnVksLH9CJY3IuSeEgbKUki7GQZm0WqDkGzyxju2EZGD2wA==}
dev: true
/@types/json-schema/7.0.11:
resolution: {integrity: sha512-wOuvG1SN4Us4rez+tylwwwCV1psiNVOkJeM3AUWUNWg/jDQY2+HE/444y5gc+jBmRqASOm2Oeh5c1axHobwRKQ==}
dev: true
@ -2993,7 +3005,6 @@ packages:
/argparse/2.0.1:
resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==}
dev: true
/aria-hidden/1.1.3:
resolution: {integrity: sha512-RhVWFtKH5BiGMycI72q2RAFMLQi8JP9bLuQXgR5a8Znp7P5KOIADSJeyfI8PCVxLEp067B2HbP5JIiI/PXIZeA==}
@ -5080,6 +5091,12 @@ packages:
reusify: 1.0.4
dev: true
/fault/2.0.1:
resolution: {integrity: sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==}
dependencies:
format: 0.2.2
dev: false
/fetch-blob/2.1.2:
resolution: {integrity: sha512-YKqtUDwqLyfyMnmbw8XD6Q8j9i/HggKtPEI+pZ1+8bvheBu78biSmNaXWusx1TauGqtUUGx/cBb1mKdq2rLYow==}
engines: {node: ^10.17.0 || >=12.3.0}
@ -5192,6 +5209,11 @@ packages:
mime-types: 2.1.35
dev: false
/format/0.2.2:
resolution: {integrity: sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==}
engines: {node: '>=0.4.x'}
dev: false
/fraction.js/4.2.0:
resolution: {integrity: sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA==}
dev: true
@ -6195,7 +6217,6 @@ packages:
hasBin: true
dependencies:
argparse: 2.0.1
dev: true
/jsesc/2.5.2:
resolution: {integrity: sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==}
@ -6594,6 +6615,12 @@ packages:
- supports-color
dev: false
/mdast-util-frontmatter/1.0.0:
resolution: {integrity: sha512-7itKvp0arEVNpCktOET/eLFAYaZ+0cNjVtFtIPxgQ5tV+3i+D4SDDTjTzPWl44LT59PC+xdx+glNTawBdF98Mw==}
dependencies:
micromark-extension-frontmatter: 1.0.0
dev: false
/mdast-util-gfm-autolink-literal/1.0.2:
resolution: {integrity: sha512-FzopkOd4xTTBeGXhXSBU0OCDDh5lUj2rd+HQqG92Ld+jL4lpUfgX2AT2OHAVP9aEeDKp7G92fuooSZcYJA3cRg==}
dependencies:
@ -6743,6 +6770,14 @@ packages:
uvu: 0.5.4
dev: false
/micromark-extension-frontmatter/1.0.0:
resolution: {integrity: sha512-EXjmRnupoX6yYuUJSQhrQ9ggK0iQtQlpi6xeJzVD5xscyAI+giqco5fdymayZhJMbIFecjnE2yz85S9NzIgQpg==}
dependencies:
fault: 2.0.1
micromark-util-character: 1.1.0
micromark-util-symbol: 1.0.1
dev: false
/micromark-extension-gfm-autolink-literal/1.0.3:
resolution: {integrity: sha512-i3dmvU0htawfWED8aHMMAzAVp/F0Z+0bPh3YrbTPPL1v4YAlCZpy5rBO5p0LPYiZo0zFVkoYh7vDU7yQSiCMjg==}
dependencies:
@ -8222,6 +8257,15 @@ packages:
unified: 10.1.2
dev: false
/remark-frontmatter/4.0.1:
resolution: {integrity: sha512-38fJrB0KnmD3E33a5jZC/5+gGAC2WKNiPw1/fdXJvijBlhA7RCsvJklrYJakS0HedninvaCYW8lQGf9C918GfA==}
dependencies:
'@types/mdast': 3.0.10
mdast-util-frontmatter: 1.0.0
micromark-extension-frontmatter: 1.0.0
unified: 10.1.2
dev: false
/remark-gfm/3.0.1:
resolution: {integrity: sha512-lEFDoi2PICJyNrACFOfDD3JlLkuSbOa5Wd8EPt06HUdptv8Gn0bxYTdbU/XXQ3swAPkEaGxxPN9cbnMHvVu1Ig==}
dependencies:
@ -8252,6 +8296,14 @@ packages:
unified: 10.1.2
dev: false
/remark-stringify/10.0.2:
resolution: {integrity: sha512-6wV3pvbPvHkbNnWB0wdDvVFHOe1hBRAx1Q/5g/EpH4RppAII6J8Gnwe7VbHuXaoKIF6LAg6ExTel/+kNqSQ7lw==}
dependencies:
'@types/mdast': 3.0.10
mdast-util-to-markdown: 1.3.0
unified: 10.1.2
dev: false
/remove-accents/0.4.2:
resolution: {integrity: sha512-7pXIJqJOq5tFgG1A2Zxti3Ht8jJF337m4sowbuHsW30ZnkQFnDzy9qBNhgzX8ZLW4+UBcXiiR7SwR6pokHsxiA==}
dev: false

View File

@ -21,6 +21,8 @@ import { delStorage, getStorage, setStorage } from "~/lib/storage"
import { useQueryClient } from "@tanstack/react-query"
import { Button } from "../ui/Button"
import { UniLink } from "../ui/UniLink"
import { nanoid } from "nanoid"
import { renderPageContent } from "~/markdown"
export const PagesManager: React.FC<{
isPost: boolean
@ -242,30 +244,90 @@ export const PagesManager: React.FC<{
<header className="mb-8">
<div className="flex justify-between items-center mb-4">
<h2 className="text-2xl font-bold">{title}</h2>
<Button
className={clsx(`space-x-2 inline-flex`)}
onClick={() =>
router.push(
`/dashboard/${subdomain}/editor?type=${
isPost ? "post" : "page"
}`,
)
}
>
<svg
className="w-5 h-5"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
<div className="flex justify-center items-center">
<Button
className={clsx(`space-x-2 inline-flex mr-4`)}
onClick={() =>
router.push(
`/dashboard/${subdomain}/editor?type=${
isPost ? "post" : "page"
}`,
)
}
>
<path d="M12 20h9"></path>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
</svg>
<span>New {isPost ? "Post" : "Page"}</span>
</Button>
<svg
className="w-4 h-4"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
>
<path d="M12 20h9"></path>
<path d="M16.5 3.5a2.121 2.121 0 0 1 3 3L7 19l-4 1 1-4L16.5 3.5z"></path>
</svg>
<span>New {isPost ? "Post" : "Page"}</span>
</Button>
<Button
className={clsx(`space-x-2 inline-flex`)}
onClick={() => {
const input = document.createElement("input")
input.type = "file"
input.accept = "text/markdown"
input.addEventListener("change", async (e: any) => {
const file = e.target?.files?.[0]
const reader = new FileReader()
reader.readAsText(file, "UTF-8")
reader.onload = async (evt) => {
if (evt.target?.result) {
const pageContent = await renderPageContent(
evt.target.result as string,
)
const id = nanoid()
const key = `draft-${subdomain}-local-${id}`
const tags =
pageContent.frontMatter.tags ||
pageContent.frontMatter.categories
setStorage(key, {
date: +new Date(),
values: {
content: evt.target.result,
published: false,
publishedAt: (
pageContent.frontMatter.date || new Date()
).toISOString(),
slug:
pageContent.frontMatter.permalink ||
file.name.split(".").slice(0, -1).join("."),
tags: tags?.join?.(", ") || tags,
title: pageContent.frontMatter.title,
},
isPost: isPost,
})
queryClient.invalidateQueries([
"getPagesBySite",
subdomain,
])
router.push(
`/dashboard/${subdomain}/editor?id=local-${id}&type=${
isPost ? "post" : "page"
}`,
)
}
}
reader.onerror = (evt) => {
toast.error("Error reading file")
}
})
input.click()
}}
>
<span className="i-bxs:duplicate inline-block"></span>
<span>Import</span>
</Button>
</div>
</div>
<div className="text-sm text-zinc-500 leading-relaxed">
{description}

View File

@ -1,6 +1,7 @@
import { unified } from "unified"
import remarkParse from "remark-parse"
import remarkRehype from "remark-rehype"
import remarkFrontmatter from "remark-frontmatter"
import rehypeStringify from "rehype-stringify"
import remarkGfm from "remark-gfm"
import rehypeSanitize, { defaultSchema } from "rehype-sanitize"
@ -13,15 +14,18 @@ import { rehypeTable } from "./rehype-table"
import { allowedBlockquoteAttrs, remarkCallout } from "./remark-callout"
import { rehypeExternalLink } from "./rehyper-external-link"
import { rehypeWrapCode } from "./rehype-wrap-code"
import jsYaml from "js-yaml"
export type MarkdownEnv = {
excerpt: string
frontMatter: Record<string, any>
__internal: Record<string, any>
}
export type Rendered = {
contentHTML: string
excerpt: string
frontMatter: Record<string, any>
}
refractor.alias("html", ["svelte", "vue"])
@ -29,10 +33,23 @@ refractor.alias("html", ["svelte", "vue"])
const rehypePrism = rehypePrismGenerator(refractor)
export const renderPageContent = async (content: string): Promise<Rendered> => {
const env: MarkdownEnv = { excerpt: "", __internal: {} }
const env: MarkdownEnv = { excerpt: "", __internal: {}, frontMatter: {} }
const result = await unified()
.use(remarkParse)
.use(rehypeStringify)
.use(remarkFrontmatter, ["yaml"])
.use(() => (tree) => {
const yaml = tree.children.find((node) => node.type === "yaml")
try {
env.frontMatter = jsYaml.load((yaml as any)?.value) as Record<
string,
any
>
} catch (e) {
console.log(e)
}
})
.use(remarkGfm, {})
.use(remarkExcerpt, { env })
.use(remarkCallout)
@ -71,12 +88,11 @@ export const renderPageContent = async (content: string): Promise<Rendered> => {
ignoreMissing: true,
})
.use(rehypeTable)
.use(rehypeStringify)
.use(rehypeExternalLink)
.use(rehypeWrapCode)
.process(content)
const contentHTML = result.toString()
return { contentHTML, excerpt: env.excerpt }
return { contentHTML, excerpt: env.excerpt, frontMatter: env.frontMatter }
}

View File

@ -43,11 +43,6 @@ export default function SubdomainEditor() {
if (!pageId) {
const key = `draft-${subdomain}-local-${nanoid()}`
setDraftKey(key)
setStorage(key, {
date: +new Date(),
isPost: isPost,
values,
})
queryClient.invalidateQueries(["getPagesBySite", subdomain])
} else {
setDraftKey(`draft-${subdomain}-${pageId}`)
@ -184,6 +179,7 @@ export default function SubdomainEditor() {
})
useEffect(() => {
console.log("!page.data", page.data)
if (!page.data || !draftKey) return
const local = getStorage(draftKey)