From 0270ccf9248f8f2ee438c5450dd9073a9578e13e Mon Sep 17 00:00:00 2001 From: LebCit Date: Wed, 6 Dec 2023 18:41:54 +0200 Subject: [PATCH] Add files' option to publish or save as draft Allows publishing pages and posts right away or save to publish later --- functions/blog-doc.js | 12 ++-- functions/sitemap.js | 4 +- index.js | 2 + routes/admin/adminCreateRoute.js | 13 +++- routes/admin/adminPreviewRoute.js | 84 +++++++++++++++++++++++ routes/admin/adminRoute.js | 11 ++- routes/archiveRoute.js | 2 +- routes/mainRoute.js | 4 +- routes/markdownRoute.js | 8 +-- routes/rssRoute.js | 2 +- routes/searchRoute.js | 2 +- views/admin/components/publishSelect.html | 30 ++++++++ views/admin/layouts/adminCreate.html | 2 + views/admin/layouts/adminUpdate.html | 2 + 14 files changed, 159 insertions(+), 19 deletions(-) create mode 100644 routes/admin/adminPreviewRoute.js create mode 100644 views/admin/components/publishSelect.html diff --git a/functions/blog-doc.js b/functions/blog-doc.js index 31df429..427d2f3 100644 --- a/functions/blog-doc.js +++ b/functions/blog-doc.js @@ -71,6 +71,10 @@ class Blog_Doc { const contents = new TextDecoder().decode(buffer) const fileData = parseFrontMatter(contents) // Parse the front-matter of the file + // Since v5.3.0 to allow publishing now or later + Object.hasOwn(fileData.frontmatter, "published") + ? fileData.frontmatter.published + : (fileData.frontmatter.published = "true") const filePath = `${file}` // Get the file path const fileDir = filePath.split("/")[0] // Get the file directory const obj = { 0: fileName, 1: fileData } // Create the object that holds the file data @@ -129,7 +133,7 @@ class Blog_Doc { // Method to count the occurrence of each tag from the posts front-matter. async postsByTagCount() { - const posts = await getPosts() + const posts = (await getPosts()).filter((post) => post[1].frontmatter.published == "true") // Create an array of the tags from all the posts and sort them alphabetically const tagsArray = posts.flatMap((post) => post[1].frontmatter.tags).sort() @@ -142,7 +146,7 @@ class Blog_Doc { // Method to return the posts of a particular tag. async postsByTagList(tag) { - const posts = await getPosts() + const posts = (await getPosts()).filter((post) => post[1].frontmatter.published == "true") // Filter the posts to retrieve an array of post(s) including the requested tag // Check if the post have tags, otherwise define them as an empty array const postsByTagArray = posts.filter((post) => @@ -154,7 +158,7 @@ class Blog_Doc { // Method to return the previous and next posts of a particular post. async prevNext(filename) { - const posts = await getPosts() + const posts = (await getPosts()).filter((post) => post[1].frontmatter.published == "true") // Get the index of each post in the posts array by it's filename const actualPostIndex = posts.findIndex((post) => post[0] === filename) // Get the previous post index while the actual post index is smaller than the posts array length - 1 (posts array length - 1 is the index of the last post) @@ -180,7 +184,7 @@ class Blog_Doc { // Method to return the related posts of a particular post. async relatedPosts(filename) { - const posts = await getPosts() + const posts = (await getPosts()).filter((post) => post[1].frontmatter.published == "true") // Get the post in the posts array by it's filename const actualPost = posts.find((post) => post[0] === filename) // Get the related posts from the front-matter of the actual post diff --git a/functions/sitemap.js b/functions/sitemap.js index 5a66a06..06f838f 100644 --- a/functions/sitemap.js +++ b/functions/sitemap.js @@ -1,8 +1,8 @@ import { getPages, getPosts } from "../functions/blog-doc.js" import { getSettings } from "../functions/settings.js" -const pages = await getPages() -const posts = await getPosts() +const pages = (await getPages()).filter((page) => page[1].frontmatter.published == "true") +const posts = (await getPosts()).filter((post) => post[1].frontmatter.published == "true") export async function sitemap() { const settings = await getSettings() diff --git a/index.js b/index.js index 9eb4ea3..e4b2de4 100644 --- a/index.js +++ b/index.js @@ -32,11 +32,13 @@ import { adminRoutes, adminUpdateDelete } from "./routes/admin/adminRoute.js" import { adminCreateRoute } from "./routes/admin/adminCreateRoute.js" import { adminGalleryRoute } from "./routes/admin/adminGalleryRoute.js" import { adminConfigRoute } from "./routes/admin/adminConfigRoute.js" +import { adminPreviewRoute } from "./routes/admin/adminPreviewRoute.js" adminRoutes(app) adminCreateRoute(app) adminUpdateDelete(app) adminGalleryRoute(app) adminConfigRoute(app) +adminPreviewRoute(app) // Routes import { markdownRoute } from "./routes/markdownRoute.js" diff --git a/routes/admin/adminCreateRoute.js b/routes/admin/adminCreateRoute.js index d80cc43..9adb4b4 100644 --- a/routes/admin/adminCreateRoute.js +++ b/routes/admin/adminCreateRoute.js @@ -71,12 +71,14 @@ export const adminCreateRoute = (app) => { postDescription, postImage, postTags, + published, } = fields const pageContents = `--- title : ${pageTitle} description: ${pageDescription} featuredImage: ${pageImage} +published: ${published} --- ${fileContents}` @@ -86,12 +88,15 @@ date: ${postDate.split("-").join("/")} description: ${postDescription} featuredImage: ${postImage} tags: [${postTags}] +published: ${published} --- ${fileContents}` const utf8Encoder = new TextEncoder() if (fileType === "page") { + const path = published == "true" ? "/pages" : "/admin-preview-page" + const createdPageName = pageTitle .toLowerCase() .replace(/[^a-zA-Z0-9-_ ]/g, "") // Remove special characters except hyphen and underscore @@ -103,9 +108,12 @@ ${fileContents}` const utf8Array = utf8Encoder.encode(pageContents) await drive.put(`pages/${createdPageName}.md`, { data: utf8Array }) - res.writeHead(302, { Location: `/pages/${createdPageName}` }) + res.writeHead(302, { Location: `${path}/${createdPageName}` }) res.end() + return } else { + const path = published == "true" ? "/posts" : "/admin-preview-post" + const createdPostName = postTitle .toLowerCase() .replace(/[^a-zA-Z0-9-_ ]/g, "") // Remove special characters except hyphen and underscore @@ -117,8 +125,9 @@ ${fileContents}` const utf8Array = utf8Encoder.encode(postContents) await drive.put(`posts/${createdPostName}.md`, { data: utf8Array }) - res.writeHead(302, { Location: `/posts/${createdPostName}` }) + res.writeHead(302, { Location: `${path}/${createdPostName}` }) res.end() + return } }) }) diff --git a/routes/admin/adminPreviewRoute.js b/routes/admin/adminPreviewRoute.js new file mode 100644 index 0000000..e4d42dc --- /dev/null +++ b/routes/admin/adminPreviewRoute.js @@ -0,0 +1,84 @@ +import { getPages, getPosts, prevNext } from "../../functions/blog-doc.js" +import { initializeApp } from "../../functions/initialize.js" +import { idsInHeadings } from "../../functions/helpers.js" +import { getSettings } from "../../functions/settings.js" +import { marked } from "marked" + +const { eta } = initializeApp() + +// Markdown Route +export function adminPreviewRoute(app) { + /** + * Due to Velocy architecture, a route without a defined start point cannot be reached. + * This is why two routes are created, one for the pages and the other for posts. + * In short, the following route "/:folder/:filename" doesn't work in Velocy. + */ + app.get("/admin-preview-page/:filename", async (req, res) => { + const settings = await getSettings() + + const pages = await getPages() + const unpublishedPages = pages.filter((page) => page[1].frontmatter.published == "false") + const currentFile = unpublishedPages.find((file) => file.path === `pages/${req.params.filename}.md`) + + if (currentFile) { + const fileData = currentFile[1].frontmatter + fileData.favicon = settings.favicon + const fileContent = marked.parse(currentFile[1].content) + const response = eta.render(`themes/${settings.currentTheme}/layouts/base.html`, { + // Passing Route data + mdRoute: true, + // Passing Markdown file data + data: fileData, + content: settings.addIdsToHeadings ? idsInHeadings(fileContent) : fileContent, + // Passing data to edit the file + editable: true, + filename: req.params.filename, + // Passing needed settings for the template + siteTitle: settings.siteTitle, + menuLinks: settings.menuLinks, + footerCopyright: settings.footerCopyright, + }) + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(response) + } else { + // Proceed to the 404 route if no file is found + res.writeHead(302, { Location: "/404" }) + res.end() + } + }) + + app.get("/admin-preview-post/:filename", async (req, res) => { + const settings = await getSettings() + + const posts = await getPosts() + const unpublishedPosts = posts.filter((post) => post[1].frontmatter.published == "false") + const currentFile = unpublishedPosts.find((file) => file.path === `posts/${req.params.filename}.md`) + + if (currentFile) { + const fileData = currentFile[1].frontmatter + fileData.favicon = settings.favicon + const fileContent = marked.parse(currentFile[1].content) + const response = eta.render(`themes/${settings.currentTheme}/layouts/base.html`, { + // Passing Route data + mdRoute: true, + // Passing Markdown file data + data: fileData, + content: settings.addIdsToHeadings ? idsInHeadings(fileContent) : fileContent, + prevNext: currentFile.dir === "posts" ? await prevNext(`${req.params.filename}.md`) : null, + // Passing data to edit the file + editable: true, + filename: req.params.filename, + // Passing needed settings for the template + siteTitle: settings.siteTitle, + menuLinks: settings.menuLinks, + footerCopyright: settings.footerCopyright, + }) + res.writeHead(200, { "Content-Type": "text/html" }) + res.end(response) + } else { + // Proceed to the 404 route if no file is found + res.writeHead(302, { Location: "/404" }) + res.end() + } + }) +} diff --git a/routes/admin/adminRoute.js b/routes/admin/adminRoute.js index 073637c..f69d7ca 100644 --- a/routes/admin/adminRoute.js +++ b/routes/admin/adminRoute.js @@ -133,6 +133,7 @@ const adminUpdateDelete = (app) => { postImage, postTags, fileContents, + published, } = fields const updatedFile = filePath.split("/").pop().replace(".md", "") @@ -142,14 +143,17 @@ const adminUpdateDelete = (app) => { title: ${pageTitle} description: ${pageDescription} featuredImage: ${pageImage} +published: ${published} --- ${fileContents}` const utf8Encoder = new TextEncoder() const utf8Array = utf8Encoder.encode(pageContents) + const path = published == "true" ? "/pages" : "/admin-preview-page" await drive.put(`${filePath}`, { data: utf8Array }) - res.writeHead(302, { Location: `/pages/${updatedFile}` }) + + res.writeHead(302, { Location: `${path}/${updatedFile}` }) res.end() return } else { @@ -159,14 +163,17 @@ date: ${postDate.split("-").join("/")} description: ${postDescription} featuredImage: ${postImage} tags: [${postTags}] +published: ${published} --- ${fileContents}` const utf8Encoder = new TextEncoder() const utf8Array = utf8Encoder.encode(postContents) + const path = published == "true" ? "/posts" : "/admin-preview-post" await drive.put(`${filePath}`, { data: utf8Array }) - res.writeHead(302, { Location: `/posts/${updatedFile}` }) + + res.writeHead(302, { Location: `${path}/${updatedFile}` }) res.end() return } diff --git a/routes/archiveRoute.js b/routes/archiveRoute.js index 28457a9..6532a82 100644 --- a/routes/archiveRoute.js +++ b/routes/archiveRoute.js @@ -9,7 +9,7 @@ export function archiveRoute(app) { app.get("/posts", async (req, res) => { const settings = await getSettings() - const posts = await getPosts() + const posts = (await getPosts()).filter((post) => post[1].frontmatter.published == "true") const data = { title: "Archive", diff --git a/routes/mainRoute.js b/routes/mainRoute.js index 2438de9..0d6c2bf 100644 --- a/routes/mainRoute.js +++ b/routes/mainRoute.js @@ -9,7 +9,7 @@ export function mainRoute(app) { app.get("/", async (req, res) => { const settings = await getSettings() - const posts = await getPosts() + const posts = (await getPosts()).filter((post) => post[1].frontmatter.published == "true") const paginatedPosts = paginator(posts, 1, settings.postsPerPage) // Paginate all the posts. Set the first page to 1 and X posts per page. const newestPosts = paginatedPosts.data // Get the first X posts. const lastPage = paginatedPosts.total_pages - 1 // Get the last page number by removing 1 from the total number of pages. @@ -47,7 +47,7 @@ export function mainRoute(app) { app.get("/page/:actualBlogPage", async (req, res) => { const settings = await getSettings() - const posts = await getPosts() + const posts = (await getPosts()).filter((post) => post[1].frontmatter.published == "true") const paginatedPosts = paginator(posts, 1, settings.postsPerPage) // Paginate all the posts. Set the first page to 1 and X posts per page. const lastPage = paginatedPosts.total_pages - 1 // Get the last page number by removing 1 from the total number of pages. diff --git a/routes/markdownRoute.js b/routes/markdownRoute.js index 09c568e..6ff2b0a 100644 --- a/routes/markdownRoute.js +++ b/routes/markdownRoute.js @@ -17,8 +17,8 @@ export function markdownRoute(app) { const settings = await getSettings() const pages = await getPages() - - const currentFile = pages.find((file) => file.path === `pages/${req.params.filename}.md`) + const publishedPages = pages.filter((page) => page[1].frontmatter.published == "true") + const currentFile = publishedPages.find((file) => file.path === `pages/${req.params.filename}.md`) if (currentFile) { const fileData = currentFile[1].frontmatter @@ -51,8 +51,8 @@ export function markdownRoute(app) { const settings = await getSettings() const posts = await getPosts() - - const currentFile = posts.find((file) => file.path === `posts/${req.params.filename}.md`) + const publishedPosts = posts.filter((post) => post[1].frontmatter.published == "true") + const currentFile = publishedPosts.find((file) => file.path === `posts/${req.params.filename}.md`) if (currentFile) { const fileData = currentFile[1].frontmatter diff --git a/routes/rssRoute.js b/routes/rssRoute.js index 80610d9..cfff7f7 100644 --- a/routes/rssRoute.js +++ b/routes/rssRoute.js @@ -10,7 +10,7 @@ export function rssRoute(app) { const settings = await getSettings() // Get the posts array - const posts = await getPosts() + const posts = (await getPosts()).filter((post) => post[1].frontmatter.published == "true") const response = eta.render(`themes/${settings.currentTheme}/layouts/rss.html`, { // Passing needed settings for the template diff --git a/routes/searchRoute.js b/routes/searchRoute.js index 2ea96e3..8ca12a1 100644 --- a/routes/searchRoute.js +++ b/routes/searchRoute.js @@ -6,7 +6,7 @@ import { initializeApp } from "../functions/initialize.js" const { eta } = initializeApp() export async function searchRoute(app) { - const posts = await getPosts() + const posts = (await getPosts()).filter((post) => post[1].frontmatter.published == "true") // Render the search form on the search route app.get("/search", async (req, res) => { diff --git a/views/admin/components/publishSelect.html b/views/admin/components/publishSelect.html new file mode 100644 index 0000000..83bca1a --- /dev/null +++ b/views/admin/components/publishSelect.html @@ -0,0 +1,30 @@ +<% /* Since v5.3.0 to allow publishing now or later */ %> +
+ + + +
diff --git a/views/admin/layouts/adminCreate.html b/views/admin/layouts/adminCreate.html index b744fa1..da9e7a2 100644 --- a/views/admin/layouts/adminCreate.html +++ b/views/admin/layouts/adminCreate.html @@ -143,6 +143,8 @@ > + + <%~ include('../components/publishSelect.html', it) %> + + <%~ include('../components/publishSelect.html', it) %>