diff --git a/docs/todo3.md b/docs/todo3.md index 1992173..acf18a9 100644 --- a/docs/todo3.md +++ b/docs/todo3.md @@ -311,7 +311,7 @@ link and primary single color put all components and variants in styleguide daisy ui color-mix() and oklch just to calc hover colors, i dont need it, hardcode it sorted archive like in astro-cactus, route param highlight and link -refactor rss and json feed + refactor rss and json feed ProjectCard and test markdown PostCardSmall extract types from constants @@ -327,5 +327,5 @@ layout bottom padding start writing readme working-notes folder in docs footer commit toast - +draft preview for prod ``` diff --git a/src/config.ts b/src/config.ts index b2279c5..9511442 100644 --- a/src/config.ts +++ b/src/config.ts @@ -1,10 +1,25 @@ // all relative imports in config subtree +import dotenv from 'dotenv'; -import { configSchema } from './schemas/config'; +import { configSchema, nodeEnvValues } from './schemas/config'; import { validateConfig } from './utils/config'; import type { ConfigType } from './types/config'; +/*------------------ load .env file -----------------*/ + +const NODE_ENV = process.env.NODE_ENV; + +if (!nodeEnvValues.includes(NODE_ENV)) { + console.error('Invalid process.env.NODE_ENV: ', NODE_ENV); + throw new Error('Invalid process.env.NODE_ENV'); +} + +const envFileName = `.env.${NODE_ENV}`; +dotenv.config({ path: envFileName }); + +/*-------------------- configData -------------------*/ + const configData: ConfigType = { NODE_ENV: process.env.NODE_ENV, /** without '/' */ diff --git a/src/constants/routes.ts b/src/constants/routes.ts index 67b9e69..f451e5b 100644 --- a/src/constants/routes.ts +++ b/src/constants/routes.ts @@ -17,5 +17,7 @@ export const ROUTES = { }, API: { OG_IMAGES: '/api/open-graph/', + FEED_JSON: '/api/feed.json', + FEED_RSS: '/api/feed.xml', }, } as const; diff --git a/src/modules/common.ts b/src/modules/common.ts index 43eaa75..da8d568 100644 --- a/src/modules/common.ts +++ b/src/modules/common.ts @@ -6,18 +6,22 @@ import type { CollectionEntry, CollectionKey } from 'astro:content'; export interface GetAllEntriesOptions { skipSort?: boolean; + includeDrafts?: boolean; } -/** Sorts by publishDate desc by default. Newest on top. */ +/** + * Sorts by publishDate desc by default. Newest on top. + * Omits drafts by default. + */ export const getAllEntries = async ( collectionName: T, options?: GetAllEntriesOptions ): Promise[]> => { - const { skipSort = false } = options ?? {}; + const { skipSort = false, includeDrafts = false } = options ?? {}; const entries = await getCollection(collectionName, ({ data }) => { const isProdAndDraft = import.meta.env.PROD && data.draft; - return !isProdAndDraft; + return !isProdAndDraft || includeDrafts; }); if (skipSort) return entries; diff --git a/src/modules/feed.ts b/src/modules/feed.ts index 74f8a73..da311ba 100644 --- a/src/modules/feed.ts +++ b/src/modules/feed.ts @@ -1,70 +1,70 @@ -import { createMarkdownProcessor } from '@astrojs/markdown-remark'; - import { Feed } from 'feed'; import { getAllPosts } from '@/modules/post/common'; import { ROUTES } from '@/constants/routes'; import { CONFIG } from '@/config'; +import { renderMarkdown } from '@/utils/markdown'; import type { Item } from 'feed'; -const { SITE_DESCRIPTION, SITE_TITLE, SITE_URL } = CONFIG; +const { SITE_DESCRIPTION, SITE_TITLE, SITE_URL, AUTHOR_NAME, AUTHOR_EMAIL } = CONFIG; -const author = { - name: 'Nemanja Mitic', - email: 'email@email.com', - link: `${SITE_URL}/about`, -}; -const copyright = (date: Date) => `©${date.getFullYear()} copyright text`; +export const getFeed = async (): Promise => { + const author = { + name: AUTHOR_NAME, + email: AUTHOR_EMAIL, + link: `${SITE_URL}${ROUTES.ABOUT}`, + }; -export const feed = new Feed({ - title: SITE_TITLE, - description: SITE_DESCRIPTION, - id: SITE_URL, - link: SITE_URL, - language: 'en', - image: `${SITE_URL}/images/favicons/favicon-32x32.png`, - favicon: `${SITE_URL}/favicon.ico`, - copyright: copyright(new Date()), - updated: new Date(), - feedLinks: { - json: `${SITE_URL}/feed.json`, - rss: `${SITE_URL}/feed.xml`, - }, - author, -}); + const copyright = (date: Date) => + `©${date.getFullYear()} ${AUTHOR_NAME}. All rights reserved.`; -const sortedRawPosts = await getAllPosts(); + const feed = new Feed({ + title: SITE_TITLE, + description: SITE_DESCRIPTION, + id: SITE_URL, + link: SITE_URL, + language: 'en', + image: `${SITE_URL}/images/favicons/favicon-32x32.png`, + favicon: `${SITE_URL}/favicon.ico`, + copyright: copyright(new Date()), + updated: new Date(), + feedLinks: { + json: `${SITE_URL}${ROUTES.API.FEED_JSON}`, + rss: `${SITE_URL}${ROUTES.API.FEED_RSS}`, + }, + author, + }); -const { render: renderMarkdown } = await createMarkdownProcessor({}); + const sortedPosts = await getAllPosts(); -for (const post of sortedRawPosts) { - const match = post.slug.match(/^(?\d{4})-(?\d{2})-(?\d{2})-(?.+)/); - if (!match || !post.slug || post.data.draft) { - continue; - } - const slug = Object.values(match.groups!).join('/'); + for (const post of sortedPosts) { + const { data, body, slug } = post; + const { title, description, publishDate, heroImage, noHero, draft } = data; - const url = `${SITE_URL}${ROUTES.BLOG}${slug}/`; - const { code: description } = await renderMarkdown( - `${post.data.description || ''}\n\n[Continue reading…](${url})` - ); - const { code: content } = await renderMarkdown(post.body); + // omit drafts + if (draft) continue; - const item: Item = { - title: post.data.title, - description, - id: url, - link: url, - date: post.data.publishDate, - published: post.data.publishDate, - author: [author], - copyright: copyright(post.data.publishDate), - content, - }; - if (post.data.heroImage?.src) { - item.image = `${SITE_URL}${post.data.heroImage.src}`; + const url = `${SITE_URL}${ROUTES.BLOG}${slug}/`; + const { code: content } = await renderMarkdown(body); + + const item: Item = { + title, + description, + id: url, + link: url, + date: publishDate, + published: publishDate, + author: [author], + copyright: copyright(publishDate), + content, + ...(noHero ? { image: `${SITE_URL}${heroImage.src}` } : {}), + }; + + feed.addItem(item); } - feed.addItem(item); -} + return feed; +}; + +export const feed = await getFeed(); diff --git a/src/pages/feed.json.ts b/src/pages/api/feed.json.ts similarity index 100% rename from src/pages/feed.json.ts rename to src/pages/api/feed.json.ts diff --git a/src/pages/feed.xml.ts b/src/pages/api/feed.xml.ts similarity index 100% rename from src/pages/feed.xml.ts rename to src/pages/api/feed.xml.ts diff --git a/src/schemas/config.ts b/src/schemas/config.ts index 37bd417..7533880 100644 --- a/src/schemas/config.ts +++ b/src/schemas/config.ts @@ -4,7 +4,11 @@ export const nodeEnvValues = ['development', 'test', 'production'] as const; export const configSchema = z.object({ NODE_ENV: z.enum(nodeEnvValues), - SITE_URL: z.string().url(), + // ensure no trailing slash + SITE_URL: z + .string() + .url() + .regex(/[^\/]$/, 'SITE_URL should not end with a slash'), SITE_TITLE: z.string().min(1), SITE_DESCRIPTION: z.string().min(1), PAGE_SIZE: z.object({ diff --git a/src/utils/config.ts b/src/utils/config.ts index 3cd5a2c..449fd41 100644 --- a/src/utils/config.ts +++ b/src/utils/config.ts @@ -1,23 +1,8 @@ -import dotenv from 'dotenv'; // @ts-expect-error, js lib import treeify from 'object-treeify'; -import { nodeEnvValues } from '../schemas/config'; - import type { ConfigSchemaType, ConfigType } from '../types/config'; -/*------------------ load .env file -----------------*/ - -const NODE_ENV = process.env.NODE_ENV; - -if (!nodeEnvValues.includes(NODE_ENV)) { - console.error('Invalid process.env.NODE_ENV: ', NODE_ENV); - throw new Error('Invalid process.env.NODE_ENV'); -} - -const envFileName = `.env.${NODE_ENV}`; -dotenv.config({ path: envFileName }); - /*------------------ validation -----------------*/ export const validateConfig = (