diff --git a/prisma/migrations/20220508145033_init/migration.sql b/prisma/migrations/20220508145033_init/migration.sql
deleted file mode 100644
index 2725857e..00000000
--- a/prisma/migrations/20220508145033_init/migration.sql
+++ /dev/null
@@ -1,146 +0,0 @@
--- CreateEnum
-CREATE TYPE "MembershipRole" AS ENUM ('OWNER', 'ADMIN', 'SUBSCRIBER');
-
--- CreateEnum
-CREATE TYPE "PageType" AS ENUM ('POST', 'PAGE');
-
--- CreateTable
-CREATE TABLE "users" (
- "id" TEXT NOT NULL,
- "name" TEXT NOT NULL,
- "username" TEXT NOT NULL,
- "email" TEXT NOT NULL,
- "emailVerified" TIMESTAMP(3),
- "avatar" TEXT,
- "bio" TEXT,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updatedAt" TIMESTAMP(3) NOT NULL,
- "deletedAt" TIMESTAMP(3),
-
- CONSTRAINT "users_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "login_tokens" (
- "id" TEXT NOT NULL,
- "email" TEXT NOT NULL,
- "expiresAt" TIMESTAMP(3) NOT NULL,
-
- CONSTRAINT "login_tokens_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "access_tokens" (
- "id" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "name" TEXT NOT NULL,
- "token" TEXT NOT NULL,
- "userId" TEXT NOT NULL,
- "publicId" TEXT,
- "publicIdExpiresAt" TIMESTAMP(3),
-
- CONSTRAINT "access_tokens_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "memberships" (
- "id" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "userId" TEXT NOT NULL,
- "siteId" TEXT NOT NULL,
- "role" "MembershipRole" NOT NULL,
- "acceptedAt" TIMESTAMP(3),
- "lastSwitchedTo" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "config" JSONB NOT NULL DEFAULT '{}',
-
- CONSTRAINT "memberships_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "sites" (
- "id" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updatedAt" TIMESTAMP(3) NOT NULL,
- "name" TEXT NOT NULL,
- "icon" TEXT,
- "subdomain" TEXT NOT NULL,
- "primaryDomainId" TEXT,
- "description" TEXT,
- "twitter" TEXT,
- "deletedAt" TIMESTAMP(3),
-
- CONSTRAINT "sites_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "pages" (
- "id" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updatedAt" TIMESTAMP(3) NOT NULL,
- "contentUpdatedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "publishedAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "published" BOOLEAN NOT NULL DEFAULT false,
- "type" "PageType" NOT NULL DEFAULT E'POST',
- "title" TEXT NOT NULL,
- "content" TEXT NOT NULL,
- "excerpt" TEXT NOT NULL,
- "format" TEXT NOT NULL DEFAULT E'markdown',
- "slug" TEXT NOT NULL,
- "siteId" TEXT NOT NULL,
- "deletedAt" TIMESTAMP(3),
-
- CONSTRAINT "pages_pkey" PRIMARY KEY ("id")
-);
-
--- CreateTable
-CREATE TABLE "domains" (
- "id" TEXT NOT NULL,
- "createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
- "updatedAt" TIMESTAMP(3) NOT NULL,
- "siteId" TEXT NOT NULL,
- "domain" TEXT NOT NULL,
-
- CONSTRAINT "domains_pkey" PRIMARY KEY ("id")
-);
-
--- CreateIndex
-CREATE UNIQUE INDEX "users_username_key" ON "users"("username");
-
--- CreateIndex
-CREATE UNIQUE INDEX "users_email_key" ON "users"("email");
-
--- CreateIndex
-CREATE UNIQUE INDEX "access_tokens_token_key" ON "access_tokens"("token");
-
--- CreateIndex
-CREATE UNIQUE INDEX "access_tokens_publicId_key" ON "access_tokens"("publicId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "sites_subdomain_key" ON "sites"("subdomain");
-
--- CreateIndex
-CREATE UNIQUE INDEX "sites_primaryDomainId_key" ON "sites"("primaryDomainId");
-
--- CreateIndex
-CREATE UNIQUE INDEX "pages_siteId_slug_key" ON "pages"("siteId", "slug");
-
--- CreateIndex
-CREATE UNIQUE INDEX "domains_domain_key" ON "domains"("domain");
-
--- AddForeignKey
-ALTER TABLE "access_tokens" ADD CONSTRAINT "access_tokens_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "memberships" ADD CONSTRAINT "memberships_userId_fkey" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "memberships" ADD CONSTRAINT "memberships_siteId_fkey" FOREIGN KEY ("siteId") REFERENCES "sites"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "sites" ADD CONSTRAINT "sites_primaryDomainId_fkey" FOREIGN KEY ("primaryDomainId") REFERENCES "domains"("id") ON DELETE NO ACTION ON UPDATE NO ACTION;
-
--- AddForeignKey
-ALTER TABLE "pages" ADD CONSTRAINT "pages_siteId_fkey" FOREIGN KEY ("siteId") REFERENCES "sites"("id") ON DELETE CASCADE ON UPDATE CASCADE;
-
--- AddForeignKey
-ALTER TABLE "domains" ADD CONSTRAINT "domains_siteId_fkey" FOREIGN KEY ("siteId") REFERENCES "sites"("id") ON DELETE CASCADE ON UPDATE CASCADE;
diff --git a/prisma/migrations/20220511093602_subscribe_feature/migration.sql b/prisma/migrations/20220511093602_subscribe_feature/migration.sql
deleted file mode 100644
index c71318ef..00000000
--- a/prisma/migrations/20220511093602_subscribe_feature/migration.sql
+++ /dev/null
@@ -1,5 +0,0 @@
--- AlterTable
-ALTER TABLE "login_tokens" ADD COLUMN "subscribeForm" JSONB;
-
--- AlterTable
-ALTER TABLE "pages" ADD COLUMN "subscribersNotifiedAt" TIMESTAMP(3);
diff --git a/prisma/migrations/20220512161018_add_site_navigation/migration.sql b/prisma/migrations/20220512161018_add_site_navigation/migration.sql
deleted file mode 100644
index 0eeef722..00000000
--- a/prisma/migrations/20220512161018_add_site_navigation/migration.sql
+++ /dev/null
@@ -1,2 +0,0 @@
--- AlterTable
-ALTER TABLE "sites" ADD COLUMN "navigation" JSONB;
diff --git a/prisma/migrations/20220513133748_auto_excerpt/migration.sql b/prisma/migrations/20220513133748_auto_excerpt/migration.sql
deleted file mode 100644
index 087c4607..00000000
--- a/prisma/migrations/20220513133748_auto_excerpt/migration.sql
+++ /dev/null
@@ -1,3 +0,0 @@
--- AlterTable
-ALTER TABLE "pages" ADD COLUMN "autoExcerpt" TEXT,
-ALTER COLUMN "excerpt" DROP NOT NULL;
diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml
deleted file mode 100644
index fbffa92c..00000000
--- a/prisma/migrations/migration_lock.toml
+++ /dev/null
@@ -1,3 +0,0 @@
-# Please do not edit this file manually
-# It should be added in your version-control system (i.e. Git)
-provider = "postgresql"
\ No newline at end of file
diff --git a/prisma/schema.prisma b/prisma/schema.prisma
index 8ea9c1cb..b9308044 100644
--- a/prisma/schema.prisma
+++ b/prisma/schema.prisma
@@ -112,6 +112,7 @@ model Page {
type PageType @default(POST)
title String
content String
+ contentHTML String
excerpt String?
autoExcerpt String?
format String @default("markdown")
diff --git a/src/components/site/SitePage.tsx b/src/components/site/SitePage.tsx
index 5cde4494..da925644 100644
--- a/src/components/site/SitePage.tsx
+++ b/src/components/site/SitePage.tsx
@@ -6,7 +6,7 @@ export const SitePage: React.FC<{
type: PageType
publishedAt: string
title: string
- content: string
+ contentHTML: string
}
}> = ({ page }) => {
return (
@@ -25,7 +25,7 @@ export const SitePage: React.FC<{
>
)
diff --git a/src/components/ui/Editor.tsx b/src/components/ui/Editor.tsx
index 9fb16d70..8210f528 100644
--- a/src/components/ui/Editor.tsx
+++ b/src/components/ui/Editor.tsx
@@ -96,7 +96,8 @@ export const useEditor = ({
view.destroy()
setView(null)
}
- }, [onChange, placeholder, onDropFile])
+ // eslint-disable-next-line react-hooks/exhaustive-deps
+ }, [placeholder, onDropFile])
// Update view state when `value` changed
useEffect(() => {
diff --git a/src/components/ui/Input.tsx b/src/components/ui/Input.tsx
index 0cbdb031..12fbee71 100644
--- a/src/components/ui/Input.tsx
+++ b/src/components/ui/Input.tsx
@@ -1,22 +1,35 @@
import clsx from "clsx"
-import { DetailedHTMLProps, forwardRef, InputHTMLAttributes } from "react"
+import { forwardRef } from "react"
-type Props = DetailedHTMLProps<
- InputHTMLAttributes,
- HTMLInputElement
-> & {
+type InputProps = {
label?: string
addon?: string
isBlock?: boolean
error?: string
help?: React.ReactNode
-}
+ multiline?: TMultiline
+} & React.ComponentPropsWithRef
-export const Input = forwardRef(function Input(
- { label, addon, className, isBlock, error, help, ...inputProps },
- ref
+export const Input = forwardRef(function Input<
+ TMutliline extends boolean = false
+>(
+ {
+ label,
+ addon,
+ className,
+ isBlock,
+ error,
+ help,
+ multiline,
+ ...inputProps
+ }: InputProps,
+ ref: TMutliline extends true
+ ? React.ForwardedRef
+ : React.ForwardedRef
) {
const hasAddon = !!addon
+ const Component = (multiline ? "textarea" : "input") as any
+
return (
{label && (
@@ -25,9 +38,9 @@ export const Input = forwardRef
(function Input(
)}
- (function Input(
)}
)
-})
+}) as (
+ props: InputProps
+) => JSX.Element
diff --git a/src/hooks/useSignedJwt.ts b/src/hooks/useSignedJwt.ts
deleted file mode 100644
index a8467016..00000000
--- a/src/hooks/useSignedJwt.ts
+++ /dev/null
@@ -1,8 +0,0 @@
-import { trpc } from "~/lib/trpc"
-
-export const useSignedJwt = () => {
- const jwt = trpc.useQuery(["user.getSignedJwt"], {
- refetchInterval: 60_000,
- })
- return jwt.data
-}
diff --git a/src/hooks/useUploadFile.ts b/src/hooks/useUploadFile.ts
index 18a43859..9fb499df 100644
--- a/src/hooks/useUploadFile.ts
+++ b/src/hooks/useUploadFile.ts
@@ -1,15 +1,13 @@
import { useCallback } from "react"
import { R2_URL } from "~/lib/env"
-import { useSignedJwt } from "./useSignedJwt"
+import { trpc } from "~/lib/trpc"
export const useUploadFile = () => {
- const signedJwt = useSignedJwt()
+ const { fetchQuery } = trpc.useContext()
const uploadFile = useCallback(
async (blob, filename) => {
- if (!signedJwt) {
- throw new Error("failed to retrieve signed jwt, please try again")
- }
+ const jwt = await fetchQuery(["user.getSignedJwt"], {})
const form = new FormData()
form.append("file", blob, filename)
@@ -17,7 +15,7 @@ export const useUploadFile = () => {
body: form,
method: "post",
headers: {
- authorization: `Bearer ${signedJwt}`,
+ authorization: `Bearer ${jwt}`,
},
})
if (!res.ok) {
@@ -26,7 +24,7 @@ export const useUploadFile = () => {
const data = await res.json()
return data
},
- [signedJwt]
+ [fetchQuery]
)
return uploadFile
diff --git a/src/lib/mailgun.server.ts b/src/lib/mailgun.server.ts
index bdfce9dd..8801ba1a 100644
--- a/src/lib/mailgun.server.ts
+++ b/src/lib/mailgun.server.ts
@@ -98,7 +98,7 @@ export const sendLoginEmail = async (payload: {
}
export const sendEmailForNewPost = async (payload: {
- post: { slug: string; title: string; content: string }
+ post: { slug: string; title: string; contentHTML: string }
site: Site
subscribers: { id: string; email: string }[]
}) => {
@@ -109,7 +109,7 @@ export const sendEmailForNewPost = async (payload: {
${payload.post.title}
- ${payload.post.content}
+ ${payload.post.contentHTML}
Unsubscribe (no login required)
diff --git a/src/lib/markdown.server.ts b/src/lib/markdown.server.ts
deleted file mode 100644
index d4670af2..00000000
--- a/src/lib/markdown.server.ts
+++ /dev/null
@@ -1,73 +0,0 @@
-import Markdown from "markdown-it"
-import Prism from "prismjs"
-import loadLanguages from "prismjs/components/index"
-import { getUserContentsUrl } from "./user-contents"
-
-const isExternLink = (url: string) => /^https?:\/\//.test(url)
-
-const handleImages = (md: Markdown) => {
- const imageRule = md.renderer.rules.image!
- md.renderer.rules.image = (tokens, idx, options, env, self) => {
- const token = tokens[idx]
- const url = token.attrGet("src")
-
- // Don't allow images from other domains.
- if (!url || isExternLink(url)) {
- return ""
- }
-
- token.attrSet("src", getUserContentsUrl(url))
- return imageRule(tokens, idx, options, env, self)
- }
-}
-
-const codeBlock = (md: Markdown) => {
- const highlight = (code: string, lang: string) => {
- if (lang === "vue" || lang === "svelte") {
- lang = "html"
- }
- loadLanguages(lang)
- const grammer = Prism.languages[lang]
- const html = grammer
- ? `
${Prism.highlight(code, grammer, lang)}
`
- : `${md.utils.escapeHtml(code)}
`
-
- return html
- }
-
- md.renderer.rules.fence = (tokens, idx, options, env, self) => {
- const token = tokens[idx]
- const code = highlight(token.content, token.info)
- return `${code}
`
- }
-}
-
-export const renderPageContent = async (content: string) => {
- const md = new Markdown({
- html: false,
- linkify: true,
- })
-
- md.use(handleImages)
- md.use(codeBlock)
-
- const html = md.render(content)
- return { html }
-}
-
-const stripHTML = (html: string) => {
- return html.replace(/<(?:.|\n)*?>/gm, "")
-}
-
-export const getAutoExcerpt = (content: string) => {
- const indexOfMore = content.indexOf("")
- const md = new Markdown({
- html: false,
- linkify: true,
- })
- if (indexOfMore > -1) {
- content = content.substring(0, indexOfMore)
- return stripHTML(md.render(content))
- }
- return stripHTML(md.render(content)).slice(0, 400)
-}
diff --git a/src/lib/trpc.ts b/src/lib/trpc.ts
index 27b1bb0c..096014ab 100644
--- a/src/lib/trpc.ts
+++ b/src/lib/trpc.ts
@@ -17,6 +17,14 @@ export const wrapTrpc = ({ ssr }: { ssr?: boolean } = {}) => {
credentials: "same-origin",
})
},
+
+ queryClientConfig: {
+ defaultOptions: {
+ queries: {
+ // refetchOnWindowFocus: false,
+ },
+ },
+ },
}
},
ssr,
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 51d97633..4e75d9cd 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -1,8 +1,9 @@
-import { IS_PROD } from "./constants"
-import { OUR_DOMAIN } from "./env"
-
type Truthy = T extends false | "" | 0 | null | undefined ? never : T // from lodash
export function truthy(value: T): value is Truthy {
return Boolean(value)
}
+
+export function stripHTML(html: string) {
+ return html.replace(/<(?:.|\n)*?>/gm, "")
+}
diff --git a/src/markdown/index.ts b/src/markdown/index.ts
new file mode 100644
index 00000000..fd81dc8c
--- /dev/null
+++ b/src/markdown/index.ts
@@ -0,0 +1,25 @@
+import Markdown from "markdown-it"
+import { pluginCodeBlock } from "./plugin-code-block"
+import { pluginExcerpt } from "./plugin-excerpt"
+import { pluginImage } from "./plugin-image"
+
+export type MarkdownEnv = {
+ excerpt: string
+ __internal: Record
+}
+
+export const renderPageContent = async (content: string) => {
+ const md = new Markdown({
+ html: false,
+ linkify: true,
+ })
+
+ md.use(pluginImage)
+ md.use(pluginCodeBlock)
+ md.use(pluginExcerpt)
+
+ const env: MarkdownEnv = { excerpt: "", __internal: {} }
+ const contentHTML = md.render(content, env)
+
+ return { content, contentHTML, env }
+}
diff --git a/src/markdown/plugin-code-block.ts b/src/markdown/plugin-code-block.ts
new file mode 100644
index 00000000..7554ba1a
--- /dev/null
+++ b/src/markdown/plugin-code-block.ts
@@ -0,0 +1,24 @@
+import Markdown from "markdown-it"
+import Prism from "prismjs"
+import loadLanguages from "prismjs/components/index"
+
+export const pluginCodeBlock = (md: Markdown) => {
+ const highlight = (code: string, lang: string) => {
+ if (lang === "vue" || lang === "svelte") {
+ lang = "html"
+ }
+ loadLanguages(lang)
+ const grammer = Prism.languages[lang]
+ const html = grammer
+ ? `${Prism.highlight(code, grammer, lang)}
`
+ : `${md.utils.escapeHtml(code)}
`
+
+ return html
+ }
+
+ md.renderer.rules.fence = (tokens, idx, options, env, self) => {
+ const token = tokens[idx]
+ const code = highlight(token.content, token.info)
+ return `${code}
`
+ }
+}
diff --git a/src/markdown/plugin-excerpt.ts b/src/markdown/plugin-excerpt.ts
new file mode 100644
index 00000000..89f68453
--- /dev/null
+++ b/src/markdown/plugin-excerpt.ts
@@ -0,0 +1,29 @@
+import MarkdownIt from "markdown-it"
+import { stripHTML } from "~/lib/utils"
+import { MarkdownEnv } from "."
+
+export const pluginExcerpt = (md: MarkdownIt) => {
+ md.renderer.rules.paragraph_close = (
+ tokens,
+ idx,
+ options,
+ env: MarkdownEnv,
+ self
+ ) => {
+ if (!env.__internal.excerpted) {
+ env.__internal.excerpted = true
+ let startIndex = 0
+ for (const [index, token] of tokens.entries()) {
+ if (token.type === "paragraph_open") {
+ startIndex = index
+ break
+ }
+ }
+ env.excerpt = stripHTML(
+ self.render(tokens.slice(startIndex, idx + 1), options, env)
+ )
+ }
+
+ return self.renderToken(tokens, idx, options)
+ }
+}
diff --git a/src/markdown/plugin-image.ts b/src/markdown/plugin-image.ts
new file mode 100644
index 00000000..515da6b5
--- /dev/null
+++ b/src/markdown/plugin-image.ts
@@ -0,0 +1,35 @@
+import Markdown from "markdown-it"
+import { R2_URL } from "~/lib/env"
+import { getUserContentsUrl } from "~/lib/user-contents"
+
+const isExternLink = (url: string) => /^https?:\/\//.test(url)
+
+const ALLOW_IMAGE_ORIGINS = [
+ "user-images.githubusercontent.com",
+ "cdn.jsdelivr.net",
+ "images.unsplash.com",
+ R2_URL.replace(/^https?\:\/\//, ""),
+]
+
+export const pluginImage = (md: Markdown) => {
+ const imageRule = md.renderer.rules.image!
+ md.renderer.rules.image = (tokens, idx, options, env, self) => {
+ const token = tokens[idx]
+ const url = token.attrGet("src")
+
+ if (!url) {
+ return ""
+ }
+
+ if (isExternLink(url)) {
+ const { hostname } = new URL(url)
+ if (!ALLOW_IMAGE_ORIGINS.includes(hostname)) {
+ throw new Error(`Image from ${hostname} is not allowed`)
+ }
+ return imageRule(tokens, idx, options, env, self)
+ }
+
+ token.attrSet("src", getUserContentsUrl(url))
+ return imageRule(tokens, idx, options, env, self)
+ }
+}
diff --git a/src/models/page.model.ts b/src/models/page.model.ts
index 94c27371..afa36a14 100644
--- a/src/models/page.model.ts
+++ b/src/models/page.model.ts
@@ -9,11 +9,12 @@ import {
} from "~/lib/db.server"
import { type Gate } from "~/lib/gate.server"
import { sendEmailForNewPost } from "~/lib/mailgun.server"
-import { getAutoExcerpt, renderPageContent } from "~/lib/markdown.server"
+import { renderPageContent } from "~/markdown"
import { notFound } from "~/lib/server-side-props"
import { PageVisibilityEnum } from "~/lib/types"
import { isUUID } from "~/lib/uuid"
import { getSite } from "./site.model"
+import { stripHTML } from "~/lib/utils"
const checkPageSlug = async ({
slug,
@@ -84,6 +85,7 @@ export async function createOrUpdatePage(
},
},
content: "",
+ contentHTML: "",
excerpt: "",
autoExcerpt: "",
},
@@ -103,7 +105,9 @@ export async function createOrUpdatePage(
const slug = input.slug || page.slug
await checkPageSlug({ slug, excludePage: page.id, siteId: page.siteId })
- const autoExcerpt = input.content ? getAutoExcerpt(input.content) : undefined
+ const rendered = input.content
+ ? await renderPageContent(input.content)
+ : undefined
const updated = await prismaPrimary.page.update({
where: {
@@ -114,10 +118,11 @@ export async function createOrUpdatePage(
content: input.content,
published: input.published,
publishedAt: input.publishedAt && new Date(input.publishedAt),
- excerpt: input.excerpt,
+ excerpt: input.excerpt && stripHTML(input.excerpt),
slug,
type: input.isPost ? "POST" : "PAGE",
- autoExcerpt,
+ autoExcerpt: rendered?.env.excerpt,
+ contentHTML: rendered?.contentHTML,
},
})
@@ -236,7 +241,6 @@ export async function getPage(
/** page slug or id, `site` is needed when `page` is a slug */
page: string
site?: string
- renderContent?: boolean
}
) {
const site = input.site ? await getSite(input.site) : null
@@ -274,11 +278,6 @@ export async function getPage(
}
}
- if (input.renderContent) {
- const rendered = await renderPageContent(page.content)
- page.content = rendered.html
- }
-
return page
}
@@ -288,7 +287,7 @@ export const notifySubscribersForNewPost = async (
pageId: string
}
) => {
- const page = await getPage(gate, { page: input.pageId, renderContent: true })
+ const page = await getPage(gate, { page: input.pageId })
const site = await getSite(page.siteId)
if (!gate.allows({ type: "can-notify-site-subscribers", site })) {
diff --git a/src/models/site.model.ts b/src/models/site.model.ts
index 7c325d84..f57438fb 100644
--- a/src/models/site.model.ts
+++ b/src/models/site.model.ts
@@ -8,6 +8,7 @@ import { SiteNavigationItem, SubscribeFormData } from "~/lib/types"
import { nanoid } from "nanoid"
import { getMembership } from "./membership"
import { checkReservedWords } from "~/lib/reserved-words"
+import { renderPageContent } from "~/markdown"
export const checkSubdomain = async ({
subdomain,
@@ -165,6 +166,9 @@ export async function createSite(
url: "/archives",
},
]
+ const aboutPage = await renderPageContent(
+ `My name is ${payload.name} and I'm a new site.`
+ )
const site = await prismaPrimary.site.create({
data: {
name: payload.name,
@@ -185,8 +189,9 @@ export async function createSite(
title: "About",
slug: "about",
excerpt: "",
- autoExcerpt: "",
- content: `My name is ${payload.name} and I'm a new site.`,
+ autoExcerpt: aboutPage.env.excerpt,
+ content: aboutPage.content,
+ contentHTML: aboutPage.contentHTML,
published: true,
publishedAt: new Date(),
type: PageType.PAGE,
diff --git a/src/pages/_site/[site]/[page].tsx b/src/pages/_site/[site]/[page].tsx
index 09e8015c..985f686c 100644
--- a/src/pages/_site/[site]/[page].tsx
+++ b/src/pages/_site/[site]/[page].tsx
@@ -23,7 +23,6 @@ export const getServerSideProps: GetServerSideProps = serverSidePropsHandler(
ssg.fetchQuery("site.page", {
site: domainOrSubdomain,
page: pageSlug,
- renderContent: true,
}),
ssg.fetchQuery("site.subscription", { site: domainOrSubdomain }),
])
@@ -57,7 +56,7 @@ function SitePagePage({
])
const pageResult = trpc.useQuery([
"site.page",
- { site: domainOrSubdomain, page: pageSlug, renderContent: true },
+ { site: domainOrSubdomain, page: pageSlug },
])
const site = siteResult.data
diff --git a/src/pages/dashboard/[subdomain]/editor.tsx b/src/pages/dashboard/[subdomain]/editor.tsx
index 376a164a..e6cd3bcf 100644
--- a/src/pages/dashboard/[subdomain]/editor.tsx
+++ b/src/pages/dashboard/[subdomain]/editor.tsx
@@ -1,6 +1,6 @@
import clsx from "clsx"
import dayjs from "dayjs"
-import { useCallback, useEffect, useState } from "react"
+import { ChangeEvent, useCallback, useEffect, useState } from "react"
import { DashboardMain } from "~/components/dashboard/DashboardMain"
import { getPageVisibility } from "~/lib/page-helpers"
import { PageVisibilityEnum } from "~/lib/types"
@@ -35,6 +35,7 @@ export default function SubdomainEditor() {
["site.page", { page: pageId!, site: subdomain }],
{
enabled: !!pageId,
+ refetchOnWindowFocus: false,
}
)
const page = pageResult.data
@@ -48,19 +49,24 @@ export default function SubdomainEditor() {
const [values, setValues] = useState({
title: "",
- content: "",
publishedAt: new Date().toISOString(),
published: false,
slug: "",
+ excerpt: "",
})
- const updateValue = (field: string, value: any) => {
- setValues((values) => {
- return {
+ const [content, setContent] = useState("")
+
+ type Values = typeof values
+
+ const updateValue = useCallback(
+ (key: K, value: Values[K]) => {
+ setValues({
...values,
- [field]: value,
- }
- })
- }
+ [key]: value,
+ })
+ },
+ [setValues, values]
+ )
const savePage = (published: boolean) => {
createOrUpdatePage.mutate({
@@ -69,9 +75,46 @@ export default function SubdomainEditor() {
pageId: page?.id,
isPost: isPost,
published,
+ content,
})
}
+ const handleDropFile = useCallback(
+ async (file: File, view: EditorView) => {
+ const toastId = toast.loading("Uploading...")
+ try {
+ if (!file.type.startsWith("image/")) {
+ throw new Error("You can only upload images")
+ }
+
+ const { key } = await uploadFile(file, file.name)
+ toast.success("Uploaded!", {
+ id: toastId,
+ })
+ view.dispatch(
+ view.state.replaceSelection(
+ `\n\n\n\n`
+ )
+ )
+ } catch (error) {
+ if (error instanceof Error) {
+ toast.error(error.message, { id: toastId })
+ }
+ }
+ },
+ [uploadFile]
+ )
+
+ const handleEditorChange = (newValue: string) => {
+ setContent(newValue)
+ }
+
+ const { editorRef, view } = useEditor({
+ value: content,
+ onChange: handleEditorChange,
+ onDropFile: handleDropFile,
+ })
+
useEffect(() => {
if (createOrUpdatePage.isSuccess) {
createOrUpdatePage.reset()
@@ -102,60 +145,16 @@ export default function SubdomainEditor() {
useEffect(() => {
if (!page) return
- setValues((values) => {
- return {
- ...values,
- title: page.title,
- content: page.content,
- publishedAt: page.publishedAt,
- published: page.published,
- slug: page.slug,
- }
+ setValues({
+ title: page.title,
+ publishedAt: page.publishedAt,
+ published: page.published,
+ slug: page.slug,
+ excerpt: page.excerpt || "",
})
+ setContent(page.content)
}, [page])
- const handleEditorContentChange = useCallback(
- (value: string) => updateValue("content", value),
- []
- )
-
- const handleDropFile = useCallback(
- async (file: File, view: EditorView) => {
- const toastId = toast.loading("Uploading...")
- try {
- if (!file.type.startsWith("image/")) {
- throw new Error("You can only upload images")
- }
-
- const { key } = await uploadFile(file, file.name)
- toast.success("Uploaded!", {
- id: toastId,
- })
- view.dispatch(
- view.state.replaceSelection(
- `\n\n\n\n`
- )
- )
- } catch (error) {
- if (error instanceof Error) {
- toast.error(error.message, { id: toastId })
- }
- }
- },
- [uploadFile]
- )
-
- const { editorRef, view } = useEditor({
- value: values.content,
- onChange: handleEditorContentChange,
- onDropFile: handleDropFile,
- placeholder: "Start writing here..",
- })
-
- const focusEditor = () => {
- view?.focus()
- }
-
return (
@@ -187,7 +186,7 @@ export default function SubdomainEditor() {
value={values.title}
onKeyDown={(e) => {
if (e.key === "Enter") {
- focusEditor()
+ view?.focus()
}
}}
onChange={(e) => updateValue("title", e.target.value)}
@@ -211,7 +210,7 @@ export default function SubdomainEditor() {
name="publishAt"
id="publishAt"
value={getInputDatetimeValue(values.publishedAt)}
- onChange={(e) => {
+ onChange={(e: ChangeEvent) => {
updateValue("publishedAt", e.target.value)
}}
help={`This ${
@@ -227,7 +226,9 @@ export default function SubdomainEditor() {
id="slug"
isBlock
placeholder="some-slug"
- onChange={(e) => updateValue("slug", e.target.value)}
+ onChange={(e: ChangeEvent) =>
+ updateValue("slug", e.target.value)
+ }
help={
<>
{values.slug && (
@@ -246,6 +247,21 @@ export default function SubdomainEditor() {
}
/>
+
+ ) => {
+ updateValue("excerpt", e.target.value)
+ }}
+ help="Leave it blank to use auto-generated excerpt"
+ />
+
diff --git a/src/router/site.ts b/src/router/site.ts
index 06a15e35..6f7ab532 100644
--- a/src/router/site.ts
+++ b/src/router/site.ts
@@ -95,12 +95,12 @@ export const siteRouter = createRouter()
input: z.object({
site: z.string(),
page: z.string(),
- renderContent: z.boolean().optional(),
}),
output: z.object({
id: z.string(),
title: z.string(),
content: z.string(),
+ contentHTML: z.string(),
excerpt: z.string().nullable(),
autoExcerpt: z.string().nullable(),
published: z.boolean(),